diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0777fbe6..6020334b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -62,135 +62,135 @@ jobs: bin/k8s key: ${{ steps.cache-k8s-restore.outputs.cache-primary-key }} - e2e-tests: - runs-on: large_runner - steps: - - name: Self Hosted Runner Post Job Cleanup Action - uses: TooMuch4U/actions-clean@v2.2 - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: '${{ github.workspace }}/go.mod' - cache: false - - - name: Get Go environment - run: | - echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV - echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 - with: - path: | - ${{ env.go_cache }} - ${{ env.go_modcache }} - bin/k8s - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- - env: - cache_name: integration-test - - - name: Create k8s Kind Cluster - uses: helm/kind-action@v1 - with: - config: test/e2e/config/kind-config.yaml - - - name: Install internal image registry - run: | - kubectl apply -f test/e2e/config/image-registry.yaml - kubectl wait pod -l app=registry --for condition=Ready --timeout 5m - kubectl wait pod -l app=protected-registry1 --for condition=Ready --timeout 5m - kubectl wait pod -l app=protected-registry2 --for condition=Ready --timeout 5m - - - name: Install external CRDs - run: kubectl apply --server-side -k https://github.com/openfluxcd/artifact//config/crd?ref=v0.1.1 - - - name: Checkout helm-controller - uses: actions/checkout@v4 - with: - repository: openfluxcd/helm-controller - path: helm-controller - - # TODO: Create helm-controller image in public repository to omit rebuilds - - name: Install helm-controller - env: - IMG: localhost:31000/helm-controller:latest - run: | - make -C helm-controller docker-build - make -C helm-controller docker-push - make -C helm-controller install - make -C helm-controller deploy - kubectl wait deployment.apps/helm-controller --for condition=Available --namespace helm-system --timeout 5m - kubectl logs --tail -1 -l app=helm-controller -n helm-system -f --ignore-errors &> helm-controller.log & - - - name: Checkout kustomize-controller - uses: actions/checkout@v4 - with: - repository: openfluxcd/kustomize-controller - path: kustomize-controller - - # TODO: Create kustomize-controller image in public repository to omit rebuilds - - name: Install kustomize-controller - env: - IMG: localhost:31000/kustomize-controller:latest - run: | - make -C kustomize-controller docker-build - make -C kustomize-controller docker-push - make -C kustomize-controller install - make -C kustomize-controller deploy - kubectl wait deployment.apps/kustomize-controller --for condition=Available --namespace kustomize-system --timeout 5m - kubectl logs --tail -1 -l app=kustomize-controller -n kustomize-system -f --ignore-errors &> kustomize-controller.log & - - # TODO: Replace once the release with the 'skipDigestGeneration' field in the component constructor is available - # uses: open-component-model/ocm-setup-action@main - # with: - # version: v0.19.0-rc.1 - - name: Set up cache for ocm (temporarily) - uses: actions/cache@v4 - with: - path: | - ocm/bin - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- - env: - cache_name: dummy-cache - - name: Checkout OCM (temporarily) - uses: actions/checkout@v4 - with: - repository: open-component-model/ocm - path: ocm - - name: Build OCM (temporarily) - run: | - make -C ocm bin/ocm - echo "${{ github.workspace }}/ocm/bin" >> "$GITHUB_PATH" - - - name: Run e2e test - env: - RESOURCE_TIMEOUT: 5m - HELM_CHART: ghcr.io/stefanprodan/charts/podinfo:6.7.1 - IMAGE_REFERENCE: ghcr.io/stefanprodan/podinfo:6.7.1 - CONTROLLER_LOG_PATH: ./ocm-k8s-toolkit-controller.log - IMAGE_REGISTRY_URL: http://localhost:31000 - INTERNAL_IMAGE_REGISTRY_URL: http://registry-internal.default.svc.cluster.local:5000 - PROTECTED_REGISTRY_URL: http://localhost:31001 - INTERNAL_PROTECTED_REGISTRY_URL: http://protected-registry1-internal.default.svc.cluster.local:5001 - PROTECTED_REGISTRY_URL2: http://localhost:31002 - INTERNAL_PROTECTED_REGISTRY_URL2: http://protected-registry2-internal.default.svc.cluster.local:5002 - run: make test-e2e - - - name: Publish logs on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: controller-logs - # Currently, it is planned that the integration tests runs on every commit on a PR. Therefore, we could - # produce a lot of logs. To note clutter the storage, the retention-days are reduced to 1. - retention-days: 1 - path: | - helm-controller.log - kustomize-controller.log - ocm-k8s-toolkit-controller.log \ No newline at end of file +# e2e-tests: +# runs-on: large_runner +# steps: +# - name: Self Hosted Runner Post Job Cleanup Action +# uses: TooMuch4U/actions-clean@v2.2 +# - name: Checkout +# uses: actions/checkout@v4 +# - name: Setup Go +# uses: actions/setup-go@v5 +# with: +# go-version-file: '${{ github.workspace }}/go.mod' +# cache: false +# +# - name: Get Go environment +# run: | +# echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV +# echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV +# - name: Set up cache +# # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs +# uses: actions/cache@v4 +# with: +# path: | +# ${{ env.go_cache }} +# ${{ env.go_modcache }} +# bin/k8s +# key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} +# restore-keys: | +# ${{ env.cache_name }}-${{ runner.os }}-go- +# env: +# cache_name: integration-test +# +# - name: Create k8s Kind Cluster +# uses: helm/kind-action@v1 +# with: +# config: test/e2e/config/kind-config.yaml +# +# - name: Install internal image registry +# run: | +# kubectl apply -f test/e2e/config/image-registry.yaml +# kubectl wait pod -l app=registry --for condition=Ready --timeout 5m +# kubectl wait pod -l app=protected-registry1 --for condition=Ready --timeout 5m +# kubectl wait pod -l app=protected-registry2 --for condition=Ready --timeout 5m +# +# - name: Install external CRDs +# run: kubectl apply --server-side -k https://github.com/openfluxcd/artifact//config/crd?ref=v0.1.1 +# +# - name: Checkout helm-controller +# uses: actions/checkout@v4 +# with: +# repository: openfluxcd/helm-controller +# path: helm-controller +# +# # TODO: Create helm-controller image in public repository to omit rebuilds +# - name: Install helm-controller +# env: +# IMG: localhost:31000/helm-controller:latest +# run: | +# make -C helm-controller docker-build +# make -C helm-controller docker-push +# make -C helm-controller install +# make -C helm-controller deploy +# kubectl wait deployment.apps/helm-controller --for condition=Available --namespace helm-system --timeout 5m +# kubectl logs --tail -1 -l app=helm-controller -n helm-system -f --ignore-errors &> helm-controller.log & +# +# - name: Checkout kustomize-controller +# uses: actions/checkout@v4 +# with: +# repository: openfluxcd/kustomize-controller +# path: kustomize-controller +# +# # TODO: Create kustomize-controller image in public repository to omit rebuilds +# - name: Install kustomize-controller +# env: +# IMG: localhost:31000/kustomize-controller:latest +# run: | +# make -C kustomize-controller docker-build +# make -C kustomize-controller docker-push +# make -C kustomize-controller install +# make -C kustomize-controller deploy +# kubectl wait deployment.apps/kustomize-controller --for condition=Available --namespace kustomize-system --timeout 5m +# kubectl logs --tail -1 -l app=kustomize-controller -n kustomize-system -f --ignore-errors &> kustomize-controller.log & +# +# # TODO: Replace once the release with the 'skipDigestGeneration' field in the component constructor is available +# # uses: open-component-model/ocm-setup-action@main +# # with: +# # version: v0.19.0-rc.1 +# - name: Set up cache for ocm (temporarily) +# uses: actions/cache@v4 +# with: +# path: | +# ocm/bin +# key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} +# restore-keys: | +# ${{ env.cache_name }}-${{ runner.os }}-go- +# env: +# cache_name: dummy-cache +# - name: Checkout OCM (temporarily) +# uses: actions/checkout@v4 +# with: +# repository: open-component-model/ocm +# path: ocm +# - name: Build OCM (temporarily) +# run: | +# make -C ocm bin/ocm +# echo "${{ github.workspace }}/ocm/bin" >> "$GITHUB_PATH" +# +# - name: Run e2e test +# env: +# RESOURCE_TIMEOUT: 5m +# HELM_CHART: ghcr.io/stefanprodan/charts/podinfo:6.7.1 +# IMAGE_REFERENCE: ghcr.io/stefanprodan/podinfo:6.7.1 +# CONTROLLER_LOG_PATH: ./ocm-k8s-toolkit-controller.log +# IMAGE_REGISTRY_URL: http://localhost:31000 +# INTERNAL_IMAGE_REGISTRY_URL: http://registry-internal.default.svc.cluster.local:5000 +# PROTECTED_REGISTRY_URL: http://localhost:31001 +# INTERNAL_PROTECTED_REGISTRY_URL: http://protected-registry1-internal.default.svc.cluster.local:5001 +# PROTECTED_REGISTRY_URL2: http://localhost:31002 +# INTERNAL_PROTECTED_REGISTRY_URL2: http://protected-registry2-internal.default.svc.cluster.local:5002 +# run: make test-e2e +# +# - name: Publish logs on failure +# if: failure() +# uses: actions/upload-artifact@v4 +# with: +# name: controller-logs +# # Currently, it is planned that the integration tests runs on every commit on a PR. Therefore, we could +# # produce a lot of logs. To note clutter the storage, the retention-days are reduced to 1. +# retention-days: 1 +# path: | +# helm-controller.log +# kustomize-controller.log +# ocm-k8s-toolkit-controller.log \ No newline at end of file diff --git a/Makefile b/Makefile index a6ed61f3..fa117538 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ else GOBIN=$(shell go env GOBIN) endif +OS ?= $(shell go env GOOS) +ARCH ?= $(shell go env GOARCH) + + # CONTAINER_TOOL defines the container tool to be used for building images. # Be aware that the target commands are only tested with Docker which is # scaffolded by default. However, you might want to replace it to use other @@ -64,7 +68,7 @@ vet: ## Run go vet against code. go vet ./... .PHONY: test -test: manifests generate envtest ## Run tests. +test: manifests generate envtest zot-registry ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. @@ -172,12 +176,14 @@ KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION) CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) +ZOT_BINARY ?= $(LOCALBIN)/zot-registry ## Tool Versions KUSTOMIZE_VERSION ?= v5.4.1 CONTROLLER_TOOLS_VERSION ?= v0.16.0 ENVTEST_VERSION ?= release-0.18 GOLANGCI_LINT_VERSION ?= v1.61.0 +ZOT_VERSION ?= v2.1.2 ## ZOT OCI Registry ZOT_VERSION ?= v2.1.2 @@ -213,6 +219,14 @@ deploy-cert-manager: ## Deploy cert-manager to the K8s cluster specified in ~/.k undeploy-cert-manager: ## Undeploy cert-manager from the K8s cluster specified in ~/.kube/config. $(KUBECTL) delete --ignore-not-found=$(IGNORE_NOT_FOUND) -f $(CERT-MANAGER_YAML) +.PHONY: zot-registry +zot-registry: $(LOCALBIN) ## Download zot registry binary locally if necessary. +ifeq (, $(shell which $(ZOT_BINARY))) + wget "https://github.com/project-zot/zot/releases/download/$(ZOT_VERSION)/zot-$(OS)-$(ARCH)-minimal" \ + -O $(ZOT_BINARY) \ + && chmod u+x $(ZOT_BINARY) +endif + .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) diff --git a/PROJECT b/PROJECT index c8ec47cf..08a89d0e 100644 --- a/PROJECT +++ b/PROJECT @@ -71,4 +71,4 @@ resources: kind: Replication path: github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1 version: v1alpha1 -version: "3" \ No newline at end of file +version: "3" diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index 7e67d0f8..575b93cf 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -113,3 +113,37 @@ type ResourceInfo struct { // +required Digest string `json:"digest,omitempty"` } + +type BlobInfo struct { + // Digest is the digest of the blob in the form of ':'. + Digest string `json:"digest"` + + // Tag/Version of the blob + Tag string `json:"tag"` + + // Size is the number of bytes of the blob. + // Can be used to determine how to file should be handled when downloaded (memory/disk) + Size int64 `json:"size"` +} + +// OCIArtifactInfo contains information on how to locate an OCI Artifact. +type OCIArtifactInfo struct { + // OCI repository name + // +required + Repository string `json:"repository"` + + // Manifest digest (required to delete the manifest and prepare OCI artifact for GC) + // +required + Digest string `json:"digest"` + + // Blob + // +required + Blob *BlobInfo `json:"blob"` +} + +// +k8s:deepcopy-gen=false +type OCIArtifactCreator interface { + GetOCIArtifact() *OCIArtifactInfo + GetOCIRepository() string + GetManifestDigest() string +} diff --git a/api/v1alpha1/component_types.go b/api/v1alpha1/component_types.go index c005248a..c96e194d 100644 --- a/api/v1alpha1/component_types.go +++ b/api/v1alpha1/component_types.go @@ -20,7 +20,6 @@ import ( "fmt" "time" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -100,12 +99,6 @@ type ComponentStatus struct { // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` - // ArtifactRef references the generated artifact containing a list of - // component descriptors. This list can be used by other controllers to - // avoid re-downloading (and potentially also re-verifying) the components. - // +optional - ArtifactRef corev1.LocalObjectReference `json:"artifactRef,omitempty"` - // Component specifies the concrete version of the component that was // fetched after based on the semver constraints during the last successful // reconciliation. @@ -117,6 +110,12 @@ type ComponentStatus struct { // in the order the configuration data was applied. // +optional EffectiveOCMConfig []OCMConfiguration `json:"effectiveOCMConfig,omitempty"` + + // OCIArtifact references the generated OCI artifact containing a list of + // component descriptors. This list can be used by other controllers to + // avoid re-downloading (and potentially also re-verifying) the components. + // +optional + OCIArtifact *OCIArtifactInfo `json:"ociArtifact,omitempty"` } // +kubebuilder:object:root=true @@ -180,6 +179,28 @@ func (in *Component) GetVerifications() []Verification { return in.Spec.Verify } +func (in *Component) GetOCIArtifact() *OCIArtifactInfo { + return in.Status.OCIArtifact +} + +// GetOCIRepository returns the name of the OCI repository of the OCI artifact in which the component +// descriptors are stored. +func (in *Component) GetOCIRepository() string { + return in.Status.OCIArtifact.Repository +} + +// GetManifestDigest returns the manifest digest of the OCI artifact, in which the component descriptors +// are stored. +func (in *Component) GetManifestDigest() string { + return in.Status.OCIArtifact.Digest +} + +// GetBlobDigest returns the blob digest of the OCI artifact, in which the component descriptors +// are stored. +func (in *Component) GetBlobDigest() string { + return in.Status.OCIArtifact.Blob.Digest +} + // +kubebuilder:object:root=true // ComponentList contains a list of Component. diff --git a/api/v1alpha1/condition_types.go b/api/v1alpha1/condition_types.go index d7f0f2d3..9d0504f9 100644 --- a/api/v1alpha1/condition_types.go +++ b/api/v1alpha1/condition_types.go @@ -17,22 +17,16 @@ limitations under the License. package v1alpha1 const ( - // SecretFetchFailedReason is used when the controller failed to fetch its secrets. - SecretFetchFailedReason = "SecretFetchFailed" - // ConfigFetchFailedReason is used when the controller failed to fetch its configs. ConfigFetchFailedReason = "ConfigFetchFailed" - // VerificationsInvalidReason is used when the controller failed to gather the verification information. - VerificationsInvalidReason = "VerificationsInvalid" - // ConfigureContextFailedReason is used when the controller failed to create an authenticated context. ConfigureContextFailedReason = "ConfigureContextFailed" // CheckVersionFailedReason is used when the controller failed to check for new versions. CheckVersionFailedReason = "CheckVersionFailed" - // RepositorySpecInvalidReason is used when the referenced repository spec cannot be unmarshaled and therefore is + // RepositorySpecInvalidReason is used when the referenced repository spec cannot be unmarshalled and therefore is // invalid. RepositorySpecInvalidReason = "RepositorySpecInvalid" @@ -42,7 +36,7 @@ const ( // ComponentIsNotReadyReason is used when the referenced component is not Ready yet. ComponentIsNotReadyReason = "ComponentIsNotReady" - // ComponentIsNotReadyReason is used when the referenced component is not Ready yet. + // ReplicationFailedReason is used when the referenced component is not Ready yet. ReplicationFailedReason = "ReplicationFailed" // VerificationFailedReason is used when the signature verification of a component failed. @@ -57,14 +51,29 @@ const ( // GetComponentVersionFailedReason is used when the component cannot be fetched. GetComponentVersionFailedReason = "GetComponentVersionFailed" - // StorageReconcileFailedReason is used when there was a problem reconciling the artifact storage. - StorageReconcileFailedReason = "StorageReconcileFailed" + // MarshalFailedReason is used when we fail to marshal a struct. + MarshalFailedReason = "MarshalFailed" + + // YamlToJSONDecodeFailedReason is used when we fail to decode yaml to json. + YamlToJSONDecodeFailedReason = "YamlToJsonDecodeFailed" + + // CreateOCIRepositoryNameFailedReason is used when we fail to create an OCI repository name. + CreateOCIRepositoryNameFailedReason = "CreateOCIRepositoryNameFailed" + + // CreateOCIRepositoryFailedReason is used when we fail to create an OCI repository. + CreateOCIRepositoryFailedReason = "CreateOCIRepositoryFailed" - // ReconcileArtifactFailedReason is used when we fail in creating an Artifact. - ReconcileArtifactFailedReason = "ReconcileArtifactFailed" + // PushOCIArtifactFailedReason is used when we fail to push an OCI artifact. + PushOCIArtifactFailedReason = "PushOCIArtifactFailed" - // GetArtifactFailedReason is used when we fail in getting an Artifact. - GetArtifactFailedReason = "GetArtifactFailed" + // FetchOCIArtifactFailedReason is used when we fail to fetch an OCI artifact. + FetchOCIArtifactFailedReason = "FetchOCIArtifactFailed" + + // CopyOCIArtifactFailedReason is used when we fail to copy an OCI artifact. + CopyOCIArtifactFailedReason = "CopyOCIArtifactFailed" + + // OCIRepositoryExistsFailedReason is used when we fail to check the existence of an OCI repository. + OCIRepositoryExistsFailedReason = "OCIRepositoryExistsFailed" // ResolveResourceFailedReason is used when we fail in resolving a resource. ResolveResourceFailedReason = "ResolveResourceFailed" @@ -72,8 +81,17 @@ const ( // GetResourceAccessFailedReason is used when we fail in getting a resource access(es). GetResourceAccessFailedReason = "GetResourceAccessFailed" - // GetComponentForArtifactFailedReason is used when we fail in getting a component for an artifact. - GetComponentForArtifactFailedReason = "GetComponentForArtifactFailed" + // GetBlobAccessFailedReason is used when we fail to get a blob access. + GetBlobAccessFailedReason = "GetBlobAccessFailed" + + // VerifyResourceFailedReason is used when we fail to verify a resource. + VerifyResourceFailedReason = "VerifyResourceFailed" + + // GetResourceFailedReason is used when we fail to get the resource. + GetResourceFailedReason = "GetResourceFailed" + + // CompressGzipFailedReason is used when we fail to compress to gzip. + CompressGzipFailedReason = "CompressGzipFailed" // StatusSetFailedReason is used when we fail to set the component status. StatusSetFailedReason = "StatusSetFailed" @@ -84,14 +102,17 @@ const ( // ConfigurationFailedReason is used when a resource was not able to be configured. ConfigurationFailedReason = "ConfigurationFailed" - // LocalizationRuleGenerationFailedReason is used when the controller failed to localize an artifact. + // CreateTGZFailedReason is used when a TGZ creation failed. + CreateTGZFailedReason = "CreateTGZFailed" + + // LocalizationRuleGenerationFailedReason is used when the controller failed to localize an OCI artifact. LocalizationRuleGenerationFailedReason = "LocalizationRuleGenerationFailed" // LocalizationIsNotReadyReason is used when a controller is waiting to get the localization result. LocalizationIsNotReadyReason = "LocalizationIsNotReady" - // UniqueIDGenerationFailedReason is used when the controller failed to generate a unique identifier for a pending artifact. - // This can happen if the artifact is based on multiple other sources but these sources could not be used + // UniqueIDGenerationFailedReason is used when the controller failed to generate a unique identifier for a pending OCI artifact. + // This can happen if the OCI artifact is based on multiple other sources but these sources could not be used // to determine a unique identifier. UniqueIDGenerationFailedReason = "UniqueIDGenerationFailed" diff --git a/api/v1alpha1/configuredresource_types.go b/api/v1alpha1/configuredresource_types.go index d44807b5..af8d2617 100644 --- a/api/v1alpha1/configuredresource_types.go +++ b/api/v1alpha1/configuredresource_types.go @@ -85,20 +85,35 @@ func (in *ConfiguredResource) SetTarget(v *ConfigurationReference) { v.DeepCopyInto(&in.Spec.Target) } +func (in *ConfiguredResource) GetOCIArtifact() *OCIArtifactInfo { + return in.Status.OCIArtifact +} + +// GetOCIRepository returns the name of the OCI repository of the OCI artifact in which the configured resource is +// stored. +func (in *ConfiguredResource) GetOCIRepository() string { + return in.Status.OCIArtifact.Repository +} + +// GetManifestDigest returns the manifest digest of the OCI artifact, in which the configured resource is stored. +func (in *ConfiguredResource) GetManifestDigest() string { + return in.Status.OCIArtifact.Digest +} + +// GetBlobDigest returns the blob digest of the OCI artifact, in which the configured resource is stored. +func (in *ConfiguredResource) GetBlobDigest() string { + return in.Status.OCIArtifact.Blob.Digest +} + // ConfiguredResourceStatus defines the observed state of ConfiguredResource. type ConfiguredResourceStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` - // The configuration reconcile loop generates an artifact, which contains the + // The configuration reconcile loop generates an OCI artifact, which contains the // ConfiguredResourceSpec.Target ConfigurationReference after configuration. - // It is filled once the Artifact is created and the configuration completed. - ArtifactRef *ObjectKey `json:"artifactRef,omitempty"` - - // Digest contains a technical identifier for the artifact. This technical identifier - // can be used to track changes on the ArtifactRef as it is a combination of the origin - // ConfiguredResourceSpec.Config applied to the ConfiguredResourceSpec.Target. - Digest string `json:"digest,omitempty"` + // It is filled once the OCI artifact is created and the configuration completed. + OCIArtifact *OCIArtifactInfo `json:"ociArtifact,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/constants.go b/api/v1alpha1/constants.go index 2f038541..f788eb92 100644 --- a/api/v1alpha1/constants.go +++ b/api/v1alpha1/constants.go @@ -24,9 +24,6 @@ const ( OCMConfigKey = ".ocmconfig" // OCMLabelDowngradable defines the secret. OCMLabelDowngradable = "ocm.software/ocm-k8s-toolkit/downgradable" - // OCMComponentDescriptorList defines the file name of the component descriptor list exposed as artifact by the - // component controller. - OCMComponentDescriptorList = "component-descriptor-list.yaml" ) // Log levels. @@ -37,11 +34,13 @@ const ( // Finalizers for controllers. const ( - // ArtifactFinalizer is the finalizer that is added to artifacts created by the ocm controllers. + // ArtifactFinalizer is the finalizer that is added to an object that handles the lifecycle of an artifact created by the ocm controllers. ArtifactFinalizer = "finalizers.ocm.software/artifact" ) -// External CRDs. +// OCI related constants. const ( - ArtifactCrd = "https://github.com/openfluxcd/artifact/releases/download/v0.1.1/openfluxcd.ocm.software_artifacts.yaml" + OCISchemaVersion = 2 + // Based on https://github.com/opencontainers/distribution-spec/blob/7872490e9d4943b20f11e21475bc13fd2e02b7d8/spec.md?plain=1#L157. + OCIRepositoryNameConstraints = "[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*" ) diff --git a/api/v1alpha1/localizedresource_types.go b/api/v1alpha1/localizedresource_types.go index 395d60d4..c95683fd 100644 --- a/api/v1alpha1/localizedresource_types.go +++ b/api/v1alpha1/localizedresource_types.go @@ -73,6 +73,26 @@ func (in *LocalizedResource) SetTarget(v *ConfigurationReference) { v.DeepCopyInto(&in.Spec.Target) } +func (in *LocalizedResource) GetOCIArtifact() *OCIArtifactInfo { + return in.Status.OCIArtifact +} + +// GetOCIRepository returns the name of the OCI repository of the OCI artifact, in which the localized resource is +// stored. +func (in *LocalizedResource) GetOCIRepository() string { + return in.Status.OCIArtifact.Repository +} + +// GetManifestDigest returns the manifest digest of the OCI artifact, in which the localized resource is stored. +func (in *LocalizedResource) GetManifestDigest() string { + return in.Status.OCIArtifact.Digest +} + +// GetBlobDigest returns the blob digest of the OCI artifact, in which the localized resource is stored. +func (in *LocalizedResource) GetBlobDigest() string { + return in.Status.OCIArtifact.Blob.Digest +} + type LocalizedResourceSpec struct { // Target that is to be localized Target ConfigurationReference `json:"target"` @@ -91,15 +111,16 @@ type LocalizedResourceStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` - // The LocalizedResource reports an ArtifactRef which contains the content of the Resource after Localization - ArtifactRef *ObjectKey `json:"artifactRef,omitempty"` + // The OCIArtifact contains the information where to find the OCI artifact which contains + // the content of the Resource after Localization + OCIArtifact *OCIArtifactInfo `json:"ociArtifact,omitempty"` // The LocalizedResource reports a ConfiguredResourceRef which contains a reference to the ConfiguredResource - // that is responsible for generating the ArtifactRef. + // that is responsible for generating the OCI artifact. ConfiguredResourceRef *ObjectKey `json:"configuredResourceRef,omitempty"` // ConfigRef is a reference to the Configuration that was generated by the Localization process - // and is used to setup the ConfiguredResource responsible for generating the ArtifactRef. + // and is used to setup the ConfiguredResource responsible for generating the OCI artifact. ConfigRef *ObjectKey `json:"configRef,omitempty"` // A unique digest of the combination of the config and target resources applied through a LocalizationStrategy diff --git a/api/v1alpha1/resource_types.go b/api/v1alpha1/resource_types.go index b8ec88a1..d95a845c 100644 --- a/api/v1alpha1/resource_types.go +++ b/api/v1alpha1/resource_types.go @@ -62,10 +62,10 @@ type ResourceStatus struct { // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` - // ArtifactRef points to the Artifact which represents the output of the + // OCIArtifact points to the OCI artifact which represents the output of the // last successful Resource sync. // +optional - ArtifactRef corev1.LocalObjectReference `json:"artifactRef,omitempty"` + OCIArtifact *OCIArtifactInfo `json:"ociArtifact,omitempty"` // +optional Resource *ResourceInfo `json:"resource,omitempty"` @@ -131,6 +131,25 @@ func (in *Resource) GetEffectiveOCMConfig() []OCMConfiguration { return in.Status.EffectiveOCMConfig } +func (in *Resource) GetOCIArtifact() *OCIArtifactInfo { + return in.Status.OCIArtifact +} + +// GetOCIRepository returns the name of the OCI repository of the OCI artifact in which the resource is stored. +func (in *Resource) GetOCIRepository() string { + return in.Status.OCIArtifact.Repository +} + +// GetManifestDigest returns the manifest digest of the OCI artifact, in which the resource is stored. +func (in *Resource) GetManifestDigest() string { + return in.Status.OCIArtifact.Digest +} + +// GetBlobDigest returns the blob digest of the OCI artifact, in which the resource is stored. +func (in *Resource) GetBlobDigest() string { + return in.Status.OCIArtifact.Blob.Digest +} + // +kubebuilder:object:root=true // ResourceList contains a list of Resource. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d7b6a68d..dad944ed 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -11,6 +11,21 @@ import ( "ocm.software/ocm/api/ocm/compdesc/meta/v1" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BlobInfo) DeepCopyInto(out *BlobInfo) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BlobInfo. +func (in *BlobInfo) DeepCopy() *BlobInfo { + if in == nil { + return nil + } + out := new(BlobInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Component) DeepCopyInto(out *Component) { *out = *in @@ -127,13 +142,17 @@ func (in *ComponentStatus) DeepCopyInto(out *ComponentStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - out.ArtifactRef = in.ArtifactRef in.Component.DeepCopyInto(&out.Component) if in.EffectiveOCMConfig != nil { in, out := &in.EffectiveOCMConfig, &out.EffectiveOCMConfig *out = make([]OCMConfiguration, len(*in)) copy(*out, *in) } + if in.OCIArtifact != nil { + in, out := &in.OCIArtifact, &out.OCIArtifact + *out = new(OCIArtifactInfo) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentStatus. @@ -379,10 +398,10 @@ func (in *ConfiguredResourceStatus) DeepCopyInto(out *ConfiguredResourceStatus) (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.ArtifactRef != nil { - in, out := &in.ArtifactRef, &out.ArtifactRef - *out = new(ObjectKey) - **out = **in + if in.OCIArtifact != nil { + in, out := &in.OCIArtifact, &out.OCIArtifact + *out = new(OCIArtifactInfo) + (*in).DeepCopyInto(*out) } } @@ -710,10 +729,10 @@ func (in *LocalizedResourceStatus) DeepCopyInto(out *LocalizedResourceStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.ArtifactRef != nil { - in, out := &in.ArtifactRef, &out.ArtifactRef - *out = new(ObjectKey) - **out = **in + if in.OCIArtifact != nil { + in, out := &in.OCIArtifact, &out.OCIArtifact + *out = new(OCIArtifactInfo) + (*in).DeepCopyInto(*out) } if in.ConfiguredResourceRef != nil { in, out := &in.ConfiguredResourceRef, &out.ConfiguredResourceRef @@ -737,6 +756,26 @@ func (in *LocalizedResourceStatus) DeepCopy() *LocalizedResourceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OCIArtifactInfo) DeepCopyInto(out *OCIArtifactInfo) { + *out = *in + if in.Blob != nil { + in, out := &in.Blob, &out.Blob + *out = new(BlobInfo) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIArtifactInfo. +func (in *OCIArtifactInfo) DeepCopy() *OCIArtifactInfo { + if in == nil { + return nil + } + out := new(OCIArtifactInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OCMConfiguration) DeepCopyInto(out *OCMConfiguration) { *out = *in @@ -1247,7 +1286,11 @@ func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - out.ArtifactRef = in.ArtifactRef + if in.OCIArtifact != nil { + in, out := &in.OCIArtifact, &out.OCIArtifact + *out = new(OCIArtifactInfo) + (*in).DeepCopyInto(*out) + } if in.Resource != nil { in, out := &in.Resource, &out.Resource *out = new(ResourceInfo) diff --git a/cmd/main.go b/cmd/main.go index cff8706f..20048892 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,24 +17,22 @@ limitations under the License. package main import ( + // +kubebuilder:scaffold:imports "context" "crypto/tls" "flag" "os" - "time" // to ensure that exec-entrypoint and run can make use of them. // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) _ "k8s.io/client-go/plugin/pkg/client/auth" "github.com/fluxcd/pkg/runtime/events" - "github.com/openfluxcd/controller-manager/server" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/webhook" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" @@ -49,11 +47,10 @@ import ( "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/ocmrepository" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/replication" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/resource" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" ) -// +kubebuilder:scaffold:imports - var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -63,24 +60,19 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(v1alpha1.AddToScheme(scheme)) - utilruntime.Must(artifactv1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } //nolint:funlen // this is the main function func main() { var ( - metricsAddr string - enableLeaderElection bool - probeAddr string - secureMetrics bool - enableHTTP2 bool - artifactRetentionTTL = 60 * time.Second - artifactRetentionRecords = 2 - storagePath string - storageAddr string - storageAdvAddr string - eventsAddr string + metricsAddr string + enableLeaderElection bool + probeAddr string + secureMetrics bool + enableHTTP2 bool + eventsAddr string + registryAddr string ) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metric endpoint binds to. "+ "Use the port :8080. If not set, it will be 0 in order to disable the metrics server") @@ -92,10 +84,8 @@ func main() { "If set the metrics endpoint is served securely") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") - flag.StringVar(&storageAddr, "storage-addr", ":9090", "The address the static file server binds to.") - flag.StringVar(&storageAdvAddr, "storage-adv-addr", "", "The advertised address of the static file server.") - flag.StringVar(&storagePath, "storage-path", "/data", "The local storage path.") flag.StringVar(&eventsAddr, "events-addr", "", "The address of the events receiver.") + flag.StringVar(®istryAddr, "registry-addr", "ocm-k8s-toolkit-zot-registry.ocm-k8s-toolkit-system.svc.cluster.local:5000", "The address of the registry.") opts := zap.Options{ Development: true, @@ -171,10 +161,15 @@ func main() { os.Exit(1) } - storage, artifactServer, err := server.NewArtifactStore(mgr.GetClient(), mgr.GetScheme(), - storagePath, storageAddr, storageAdvAddr, artifactRetentionTTL, artifactRetentionRecords) + registry, err := ociartifact.NewRegistry(registryAddr) + registry.PlainHTTP = true if err != nil { - setupLog.Error(err, "unable to initialize storage") + setupLog.Error(err, "unable to initialize registry object") + os.Exit(1) + } + + if err := registry.Ping(ctx); err != nil { + setupLog.Error(err, "unable to ping OCI registry") os.Exit(1) } @@ -184,7 +179,7 @@ func main() { Scheme: mgr.GetScheme(), EventRecorder: eventsRecorder, }, - Storage: storage, + Registry: registry, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Component") os.Exit(1) @@ -196,7 +191,7 @@ func main() { Scheme: mgr.GetScheme(), EventRecorder: eventsRecorder, }, - Storage: storage, + Registry: registry, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Resource") os.Exit(1) @@ -208,8 +203,8 @@ func main() { Scheme: mgr.GetScheme(), EventRecorder: eventsRecorder, }, - Storage: storage, - LocalizationClient: locclient.NewClientWithLocalStorage(mgr.GetClient(), storage, mgr.GetScheme()), + Registry: registry, + LocalizationClient: locclient.NewClientWithRegistry(mgr.GetClient(), registry, mgr.GetScheme()), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LocalizedResource") os.Exit(1) @@ -221,8 +216,8 @@ func main() { Scheme: mgr.GetScheme(), EventRecorder: eventsRecorder, }, - Storage: storage, - ConfigClient: cfgclient.NewClientWithLocalStorage(mgr.GetClient(), storage, mgr.GetScheme()), + Registry: registry, + ConfigClient: cfgclient.NewClientWithRegistry(mgr.GetClient(), registry, mgr.GetScheme()), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ConfiguredResource") os.Exit(1) @@ -238,6 +233,7 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Replication") os.Exit(1) } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -254,10 +250,6 @@ func main() { // entire process will terminate if we lose leadership, so we don't need // to handle that. <-mgr.Elected() - - if err := artifactServer.Start(ctx); err != nil { - setupLog.Error(err, "unable to start artifact server") - } }() setupLog.Info("starting manager") diff --git a/config/crd/bases/delivery.ocm.software_components.yaml b/config/crd/bases/delivery.ocm.software_components.yaml index 5325f24f..a8f02601 100644 --- a/config/crd/bases/delivery.ocm.software_components.yaml +++ b/config/crd/bases/delivery.ocm.software_components.yaml @@ -180,23 +180,6 @@ spec: status: description: ComponentStatus defines the observed state of Component. properties: - artifactRef: - description: |- - ArtifactRef references the generated artifact containing a list of - component descriptors. This list can be used by other controllers to - avoid re-downloading (and potentially also re-verifying) the components. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic component: description: |- Component specifies the concrete version of the component that was @@ -327,6 +310,45 @@ spec: object. format: int64 type: integer + ociArtifact: + description: |- + OCIArtifact references the generated OCI artifact containing a list of + component descriptors. This list can be used by other controllers to + avoid re-downloading (and potentially also re-verifying) the components. + properties: + blob: + description: Blob + properties: + digest: + description: Digest is the digest of the blob in the form + of ':'. + type: string + size: + description: |- + Size is the number of bytes of the blob. + Can be used to determine how to file should be handled when downloaded (memory/disk) + format: int64 + type: integer + tag: + description: Tag/Version of the blob + type: string + required: + - digest + - size + - tag + type: object + digest: + description: Manifest digest (required to delete the manifest + and prepare OCI artifact for GC) + type: string + repository: + description: OCI repository name + type: string + required: + - blob + - digest + - repository + type: object type: object required: - spec diff --git a/config/crd/bases/delivery.ocm.software_configuredresources.yaml b/config/crd/bases/delivery.ocm.software_configuredresources.yaml index 43ea0560..4ea1e58b 100644 --- a/config/crd/bases/delivery.ocm.software_configuredresources.yaml +++ b/config/crd/bases/delivery.ocm.software_configuredresources.yaml @@ -99,19 +99,6 @@ spec: status: description: ConfiguredResourceStatus defines the observed state of ConfiguredResource. properties: - artifactRef: - description: |- - The configuration reconcile loop generates an artifact, which contains the - ConfiguredResourceSpec.Target ConfigurationReference after configuration. - It is filled once the Artifact is created and the configuration completed. - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object conditions: items: description: Condition contains details for one aspect of the current @@ -168,15 +155,48 @@ spec: - type type: object type: array - digest: - description: |- - Digest contains a technical identifier for the artifact. This technical identifier - can be used to track changes on the ArtifactRef as it is a combination of the origin - ConfiguredResourceSpec.Config applied to the ConfiguredResourceSpec.Target. - type: string observedGeneration: format: int64 type: integer + ociArtifact: + description: |- + The configuration reconcile loop generates an OCI artifact, which contains the + ConfiguredResourceSpec.Target ConfigurationReference after configuration. + It is filled once the OCI artifact is created and the configuration completed. + properties: + blob: + description: Blob + properties: + digest: + description: Digest is the digest of the blob in the form + of ':'. + type: string + size: + description: |- + Size is the number of bytes of the blob. + Can be used to determine how to file should be handled when downloaded (memory/disk) + format: int64 + type: integer + tag: + description: Tag/Version of the blob + type: string + required: + - digest + - size + - tag + type: object + digest: + description: Manifest digest (required to delete the manifest + and prepare OCI artifact for GC) + type: string + repository: + description: OCI repository name + type: string + required: + - blob + - digest + - repository + type: object type: object type: object served: true diff --git a/config/crd/bases/delivery.ocm.software_localizedresources.yaml b/config/crd/bases/delivery.ocm.software_localizedresources.yaml index 873e405e..bf78941b 100644 --- a/config/crd/bases/delivery.ocm.software_localizedresources.yaml +++ b/config/crd/bases/delivery.ocm.software_localizedresources.yaml @@ -98,17 +98,6 @@ spec: type: object status: properties: - artifactRef: - description: The LocalizedResource reports an ArtifactRef which contains - the content of the Resource after Localization - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object conditions: items: description: Condition contains details for one aspect of the current @@ -168,7 +157,7 @@ spec: configRef: description: |- ConfigRef is a reference to the Configuration that was generated by the Localization process - and is used to setup the ConfiguredResource responsible for generating the ArtifactRef. + and is used to setup the ConfiguredResource responsible for generating the OCI artifact. properties: name: type: string @@ -180,7 +169,7 @@ spec: configuredResourceRef: description: |- The LocalizedResource reports a ConfiguredResourceRef which contains a reference to the ConfiguredResource - that is responsible for generating the ArtifactRef. + that is responsible for generating the OCI artifact. properties: name: type: string @@ -196,6 +185,44 @@ spec: observedGeneration: format: int64 type: integer + ociArtifact: + description: |- + The OCIArtifact contains the information where to find the OCI artifact which contains + the content of the Resource after Localization + properties: + blob: + description: Blob + properties: + digest: + description: Digest is the digest of the blob in the form + of ':'. + type: string + size: + description: |- + Size is the number of bytes of the blob. + Can be used to determine how to file should be handled when downloaded (memory/disk) + format: int64 + type: integer + tag: + description: Tag/Version of the blob + type: string + required: + - digest + - size + - tag + type: object + digest: + description: Manifest digest (required to delete the manifest + and prepare OCI artifact for GC) + type: string + repository: + description: OCI repository name + type: string + required: + - blob + - digest + - repository + type: object type: object type: object served: true diff --git a/config/crd/bases/delivery.ocm.software_resources.yaml b/config/crd/bases/delivery.ocm.software_resources.yaml index 9bb536e1..a180b58b 100644 --- a/config/crd/bases/delivery.ocm.software_resources.yaml +++ b/config/crd/bases/delivery.ocm.software_resources.yaml @@ -149,22 +149,6 @@ spec: status: description: ResourceStatus defines the observed state of Resource. properties: - artifactRef: - description: |- - ArtifactRef points to the Artifact which represents the output of the - last successful Resource sync. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic conditions: description: Conditions holds the conditions for the Resource. items: @@ -278,6 +262,44 @@ spec: object. format: int64 type: integer + ociArtifact: + description: |- + OCIArtifact points to the OCI artifact which represents the output of the + last successful Resource sync. + properties: + blob: + description: Blob + properties: + digest: + description: Digest is the digest of the blob in the form + of ':'. + type: string + size: + description: |- + Size is the number of bytes of the blob. + Can be used to determine how to file should be handled when downloaded (memory/disk) + format: int64 + type: integer + tag: + description: Tag/Version of the blob + type: string + required: + - digest + - size + - tag + type: object + digest: + description: Manifest digest (required to delete the manifest + and prepare OCI artifact for GC) + type: string + repository: + description: OCI repository name + type: string + required: + - blob + - digest + - repository + type: object resource: properties: access: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 73f37caa..574287a7 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -12,7 +12,7 @@ resources: - bases/delivery.ocm.software_resourceconfigs.yaml # +kubebuilder:scaffold:crdkustomizeresource -patches: +# patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD # +kubebuilder:scaffold:crdkustomizewebhookpatch diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 2530fc60..ef2db61c 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,6 +1,5 @@ resources: - manager.yaml -- service.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index c78c5884..c151df93 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -36,7 +36,6 @@ spec: args: # - --leader-elect We dont need it for testing - --health-probe-bind-address=:8081 - - --storage-adv-addr=ocm-k8s-toolkit-artifact-service.ocm-k8s-toolkit-system.svc.cluster.local. - --zap-log-level=4 image: controller:latest name: manager diff --git a/config/manager/service.yaml b/config/manager/service.yaml deleted file mode 100644 index 76d3cf2e..00000000 --- a/config/manager/service.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: artifact-service -spec: - type: ClusterIP - selector: - control-plane: controller-manager - ports: - - name: http - port: 80 - protocol: TCP - targetPort: http diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 087bea97..50eff65d 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -22,4 +22,5 @@ resources: - component_editor_role.yaml - component_viewer_role.yaml - ocmrepository_editor_role.yaml -- ocmrepository_viewer_role.yaml \ No newline at end of file +- ocmrepository_viewer_role.yaml + diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ad84bd88..6f77a41e 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -52,7 +52,6 @@ rules: - localizedresources/finalizers - ocmrepositories/finalizers - replications/finalizers - - resources/finalizers verbs: - update - apiGroups: @@ -87,29 +86,3 @@ rules: - patch - update - watch -- apiGroups: - - openfluxcd.ocm.software - resources: - - artifacts - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - openfluxcd.ocm.software - resources: - - artifacts/finalizers - verbs: - - update -- apiGroups: - - openfluxcd.ocm.software - resources: - - artifacts/status - verbs: - - get - - patch - - update diff --git a/config/zot/configmap.yaml b/config/zot/configmap.yaml index cc477a45..8b2f5e07 100644 --- a/config/zot/configmap.yaml +++ b/config/zot/configmap.yaml @@ -21,4 +21,4 @@ data: "log": { "level": "debug" } - } \ No newline at end of file + } diff --git a/config/zot/kustomization.yaml b/config/zot/kustomization.yaml index fd24fea3..4649066b 100644 --- a/config/zot/kustomization.yaml +++ b/config/zot/kustomization.yaml @@ -1,10 +1,10 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- configmap.yaml -- deployment.yaml -- service.yaml + - configmap.yaml + - deployment.yaml + - service.yaml images: -- name: zot-minimal - newName: ghcr.io/project-zot/zot-minimal - newTag: latest + - name: zot-minimal + newName: ghcr.io/project-zot/zot-minimal + newTag: latest \ No newline at end of file diff --git a/config/zot/service.yaml b/config/zot/service.yaml index e5773366..ecad7f86 100644 --- a/config/zot/service.yaml +++ b/config/zot/service.yaml @@ -23,4 +23,4 @@ spec: # targetPort: 5000 # nodePort: 31000 # selector: -# app: zot +# app: zot \ No newline at end of file diff --git a/docs/adr/configuration.md b/docs/adr/configuration.md index 4daeb831..a140b529 100644 --- a/docs/adr/configuration.md +++ b/docs/adr/configuration.md @@ -89,7 +89,7 @@ Again, an optional `subPath` can be used to further refine value substitution. This one can be used for values provided through another resource. This option is convenient if values are bundled with another component and are shipped together with the target component. Or are the end result of a Localization step. Anything -that can provide a Snapshot can provide values. +that can provide an Artifact (formerly known as Snapshot) can provide values. ```yaml sourceRef: diff --git a/go.mod b/go.mod index 4f7f10ae..336b75b1 100644 --- a/go.mod +++ b/go.mod @@ -15,14 +15,14 @@ require ( github.com/fluxcd/pkg/runtime v0.53.1 github.com/fluxcd/pkg/tar v0.11.0 github.com/google/go-containerregistry v0.20.3 - github.com/mandelsoft/filepath v0.0.0-20240223090642-3e2777258aa3 github.com/mandelsoft/goutils v0.0.0-20241005173814-114fa825bbdc github.com/mandelsoft/vfs v0.4.4 + github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/onsi/ginkgo/v2 v2.22.2 github.com/onsi/gomega v1.36.2 github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.0 github.com/openfluxcd/artifact v0.1.1 - github.com/openfluxcd/controller-manager v0.1.2 github.com/stretchr/testify v1.10.0 github.com/ulikunitz/xz v0.5.12 k8s.io/api v0.32.2 @@ -30,6 +30,7 @@ require ( k8s.io/apimachinery v0.32.2 k8s.io/client-go v0.32.2 ocm.software/ocm v0.20.1 + oras.land/oras-go/v2 v2.5.0 sigs.k8s.io/controller-runtime v0.20.2 sigs.k8s.io/yaml v1.4.0 ) @@ -147,10 +148,6 @@ require ( github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fluxcd/pkg/apis/acl v0.6.0 // indirect - github.com/fluxcd/pkg/lockedfile v0.3.0 // indirect - github.com/fluxcd/pkg/sourceignore v0.7.0 // indirect - github.com/fluxcd/source-controller/api v1.3.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -203,7 +200,7 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/gowebpki/jcs v1.0.1 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -224,12 +221,12 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/letsencrypt/boulder v0.0.0-20241010192615-6692160cedfa // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/magiconair/properties v1.8.9 // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/mandelsoft/filepath v0.0.0-20240223090642-3e2777258aa3 // indirect github.com/mandelsoft/logging v0.0.0-20240618075559-fdca28a87b0a // indirect github.com/mandelsoft/spiff v1.7.0-beta-6 // indirect github.com/marstr/guid v1.1.0 // indirect @@ -259,8 +256,6 @@ require ( github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/oleiade/reflections v1.1.0 // indirect - github.com/opencontainers/go-digest/blake3 v0.0.0-20240426182413-22b78e47854a // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pborman/uuid v1.2.1 // indirect @@ -321,21 +316,19 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - github.com/zeebo/assert v1.3.0 // indirect - github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/errs v1.4.0 // indirect go.mongodb.org/mongo-driver v1.17.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect go.opentelemetry.io/otel/metric v1.33.0 // indirect go.opentelemetry.io/otel/sdk v1.33.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/proto/otlp v1.4.0 // indirect go.step.sm/crypto v0.56.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect @@ -371,7 +364,6 @@ require ( k8s.io/kubectl v0.32.1 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect oras.land/oras-go v1.2.6 // indirect - oras.land/oras-go/v2 v2.5.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/kustomize/api v0.19.0 // indirect sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect diff --git a/go.sum b/go.sum index 5e63b585..889bbee0 100644 --- a/go.sum +++ b/go.sum @@ -388,26 +388,16 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluxcd/cli-utils v0.36.0-flux.12 h1:8cD6SmaKa/lGo0KCu0XWiGrXJMLMBQwSsnoP0cG+Gjw= github.com/fluxcd/cli-utils v0.36.0-flux.12/go.mod h1:Nb/zMqsJAzjz4/HIsEc2LTqxC6eC0rV26t4hkJT/F9o= -github.com/fluxcd/pkg/apis/acl v0.6.0 h1:rllf5uQLzTow81ZCslkQ6LPpDNqVQr6/fWaNksdUEtc= -github.com/fluxcd/pkg/apis/acl v0.6.0/go.mod h1:IVDZx3MAoDWjlLrJHMF9Z27huFuXAEQlnbWw0M6EcTs= github.com/fluxcd/pkg/apis/event v0.16.0 h1:ffKc/3erowPnh72lFszz7sPQhLZ7bhqNrq+pu1Pb+JE= github.com/fluxcd/pkg/apis/event v0.16.0/go.mod h1:D/QQi5lHT9/Ur3OMFLJO71D4KDQHbJ5s8dQV3h1ZAT0= github.com/fluxcd/pkg/apis/meta v1.10.0 h1:rqbAuyl5ug7A5jjRf/rNwBXmNl6tJ9wG2iIsriwnQUk= github.com/fluxcd/pkg/apis/meta v1.10.0/go.mod h1:n7NstXHDaleAUMajcXTVkhz0MYkvEXy1C/eLI/t1xoI= -github.com/fluxcd/pkg/lockedfile v0.3.0 h1:tZkBAffcxyt4zMigHIKc54cKgN5I/kFF005gyWZdyds= -github.com/fluxcd/pkg/lockedfile v0.3.0/go.mod h1:5iCYXAs953LlXZq7nTId9ZSGnHVvTfZ0mDmrDE49upk= github.com/fluxcd/pkg/runtime v0.53.1 h1:S+QRSoiU+LH1sTvJLNvT1x3E5hBq/sjOsRHazA7OqTo= github.com/fluxcd/pkg/runtime v0.53.1/go.mod h1:8vkIhS1AhkmjC98LRm5xM+CRG5KySFTXpJWk+ZdtT4I= -github.com/fluxcd/pkg/sourceignore v0.7.0 h1:qQrB2o543wA1o4vgR62ufwkAaDp8+f8Wdj1HKDlmDrU= -github.com/fluxcd/pkg/sourceignore v0.7.0/go.mod h1:A4GuZt2seJJkBm3kMiIx9nheoYZs98KTMr/A6/2fIro= github.com/fluxcd/pkg/ssa v0.43.0 h1:XmADD3C0erYZayKfGI0WTsMlW9TtS4bp5gy4Axo1dcA= github.com/fluxcd/pkg/ssa v0.43.0/go.mod h1:MjkaOr4/5C8wkwsdVLMmfS64lDZOgJP4VNxmmJL0Iuc= github.com/fluxcd/pkg/tar v0.11.0 h1:pjf/rzr6HNAPiuxT59mtba9tfBtdNiSQ/UqduG8vZ2I= github.com/fluxcd/pkg/tar v0.11.0/go.mod h1:+kiP25NqibWMpFWgizyPEMqnMJIux7bCgEy+4pfxyI4= -github.com/fluxcd/pkg/testserver v0.7.0 h1:kNVAn+3bAF2rfR9cT6SxzgEz2o84i+o7zKY3XRKTXmk= -github.com/fluxcd/pkg/testserver v0.7.0/go.mod h1:Ih5IK3Y5G3+a6c77BTqFkdPDCY1Yj1A1W5cXQqkCs9s= -github.com/fluxcd/source-controller/api v1.3.0 h1:Z5Lq0aJY87yg0cQDEuwGLKS60GhdErCHtsi546HUt10= -github.com/fluxcd/source-controller/api v1.3.0/go.mod h1:+tfd0vltjcVs/bbnq9AlYR9AAHSVfM/Z4v4TpQmdJf4= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -595,8 +585,8 @@ github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -681,9 +671,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -743,6 +730,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -815,8 +804,6 @@ github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2e github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w= github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be h1:f2PlhC9pm5sqpBZFvnAoKj+KzXRzbjFMA+TqXfJdgho= github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/go-digest/blake3 v0.0.0-20240426182413-22b78e47854a h1:xwooQrLddjfeKhucuLS4ElD3TtuuRwF8QWC9eHrnbxY= -github.com/opencontainers/go-digest/blake3 v0.0.0-20240426182413-22b78e47854a/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= @@ -824,8 +811,6 @@ github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/openfluxcd/artifact v0.1.1 h1:sSpaUYAbvXty+NRldYhVqIGK+7pyfow/IM+IrwrLRHI= github.com/openfluxcd/artifact v0.1.1/go.mod h1:A+2bRh4vjyFK5A/mtfefqXA0weNSnazkkMJPJ4SMzm8= -github.com/openfluxcd/controller-manager v0.1.2 h1:gYurNX4Ya2cu2WV6QwLwoBZsnCtJFIGfec7flyG4zVI= -github.com/openfluxcd/controller-manager v0.1.2/go.mod h1:13nw6eXYMuk6UYUqdJ+/oS1MGgYXa0zIitU1cw+f2Fc= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -1055,15 +1040,8 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= -github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= -github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= -github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= -github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c h1:U1b4THKcgOpJ+kILupuznNwPiURtwVW3e9alJvji9+s= github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c/go.mod h1:GSDpFDD4TASObxvfZfvpZZ3OWHIUHMlhVWlkOe4ewVk= github.com/zmap/zlint/v3 v3.6.0 h1:vTEaDRtYN0d/1Ax60T+ypvbLQUHwHxbvYRnUMVr35ug= @@ -1086,8 +1064,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= @@ -1106,8 +1084,8 @@ go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4Jjx go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.step.sm/crypto v0.56.0 h1:KcFfV76cI9Xaw8bdSc9x55skyuSdcHcTdL37vvVZnvY= go.step.sm/crypto v0.56.0/go.mod h1:snWNloxY9s1W+HsFqcviq55nvzbqqX6LxVt0Vktv5mw= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/internal/controller/component/component_controller.go b/internal/controller/component/component_controller.go index 70c179a4..476f1ce7 100644 --- a/internal/controller/component/component_controller.go +++ b/internal/controller/component/component_controller.go @@ -20,31 +20,29 @@ import ( "context" "errors" "fmt" - "os" - "path/filepath" - "strings" "github.com/Masterminds/semver/v3" "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/patch" "github.com/mandelsoft/goutils/sliceutils" - "github.com/openfluxcd/controller-manager/storage" + "github.com/opencontainers/go-digest" "k8s.io/apimachinery/pkg/types" "ocm.software/ocm/api/datacontext" "ocm.software/ocm/api/ocm/resolvers" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/yaml" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - corev1 "k8s.io/api/core/v1" ocmctx "ocm.software/ocm/api/ocm" ctrl "sigs.k8s.io/controller-runtime" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" ) @@ -52,15 +50,56 @@ import ( // Reconciler reconciles a Component object. type Reconciler struct { *ocm.BaseReconciler - Storage *storage.Storage + Registry ociartifact.RegistryType } var _ ocm.Reconciler = (*Reconciler)(nil) // SetupWithManager sets up the controller with the Manager. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + // Create index for ocmrepository reference name from components + const fieldName = "spec.repositoryRef.name" + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.Resource{}, fieldName, func(obj client.Object) []string { + component, ok := obj.(*v1alpha1.Component) + if !ok { + return nil + } + + return []string{component.Spec.RepositoryRef.Name} + }); err != nil { + return err + } + return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Component{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Watches( + &v1alpha1.Component{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + ocmRepository, ok := obj.(*v1alpha1.OCMRepository) + if !ok { + return []reconcile.Request{} + } + + // Get list of components that reference the ocmrepository + list := &v1alpha1.ComponentList{} + if err := r.List(ctx, list, client.MatchingFields{fieldName: ocmRepository.GetName()}); err != nil { + return []reconcile.Request{} + } + + // For every component that references the ocmrepository create a reconciliation request for that + // component + requests := make([]reconcile.Request, 0, len(list.Items)) + for _, component := range list.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: component.GetNamespace(), + Name: component.GetName(), + }, + }) + } + + return requests + })). Complete(r) } @@ -68,16 +107,13 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { // +kubebuilder:rbac:groups=delivery.ocm.software,resources=components/status,verbs=get;update;patch // +kubebuilder:rbac:groups=delivery.ocm.software,resources=components/finalizers,verbs=update -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts/finalizers,verbs=update - // +kubebuilder:rbac:groups="",resources=secrets;configmaps;serviceaccounts,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // Reconcile the component object. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, retErr error) { + log.FromContext(ctx).Info("reconciling component", "name", req.Name) component := &v1alpha1.Component{} if err := r.Get(ctx, req.NamespacedName, component); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -102,10 +138,29 @@ func (r *Reconciler) reconcileWithStatusUpdate(ctx context.Context, component *v func (r *Reconciler) reconcileExists(ctx context.Context, component *v1alpha1.Component) (_ ctrl.Result, retErr error) { logger := log.FromContext(ctx) - if component.GetDeletionTimestamp() != nil { + + if !component.GetDeletionTimestamp().IsZero() { + if err := ociartifact.DeleteForObject(ctx, r.Registry, component); err != nil { + return ctrl.Result{}, err + } + + if updated := controllerutil.RemoveFinalizer(component, v1alpha1.ArtifactFinalizer); updated { + if err := r.Update(ctx, component); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err) + } + } + logger.Info("component is being deleted and cannot be used", "name", component.Name) - return ctrl.Result{}, nil + return ctrl.Result{Requeue: true}, nil + } + + if updated := controllerutil.AddFinalizer(component, v1alpha1.ArtifactFinalizer); updated { + if err := r.Update(ctx, component); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) + } + + return ctrl.Result{Requeue: true}, nil } if component.Spec.Suspend { @@ -130,18 +185,22 @@ func (r *Reconciler) reconcile(ctx context.Context, component *v1alpha1.Componen return ctrl.Result{}, fmt.Errorf("failed to get repository: %w", err) } - if repo.DeletionTimestamp != nil { + if !repo.DeletionTimestamp.IsZero() { err := errors.New("repository is being deleted, please do not use it") - logger.Error(err, "repository is being deleted, please do not use it", "name", component.Spec.RepositoryRef.Name) + logger.Error(err, "waiting for deletion", "name", component.Spec.RepositoryRef.Name) return ctrl.Result{}, nil } + // Note: Marking the component as not ready, when the ocmrepository is not ready is not completely valid. As the + // was potentially ready, then the ocmrepository changed, but that does not necessarily mean that the component is + // not ready as well. + // However, as the component is hard-dependant on the ocmrepository, we decided to mark it not ready as well. if !conditions.IsReady(repo) { logger.Info("repository is not ready", "name", component.Spec.RepositoryRef.Name) status.MarkNotReady(r.EventRecorder, component, v1alpha1.RepositoryIsNotReadyReason, "repository is not ready yet") - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{}, errors.New("ocmRepository is not ready") } return r.reconcileOCM(ctx, component, repo) @@ -167,6 +226,7 @@ func (r *Reconciler) reconcileOCM(ctx context.Context, component *v1alpha1.Compo return result, nil } +//nolint:funlen // we do not want to cut function at an arbitrary point func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context, component *v1alpha1.Component, repository *v1alpha1.OCMRepository) (ctrl.Result, error) { logger := log.FromContext(ctx) @@ -178,14 +238,16 @@ func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context if err != nil { status.MarkNotReady(r.GetEventRecorder(), component, v1alpha1.ConfigureContextFailedReason, err.Error()) - return ctrl.Result{}, nil + return ctrl.Result{}, err } + verifications, err := ocm.GetVerifications(ctx, r.GetClient(), component) if err != nil { status.MarkNotReady(r.GetEventRecorder(), component, v1alpha1.ConfigureContextFailedReason, err.Error()) - return ctrl.Result{}, nil + return ctrl.Result{}, err } + err = ocm.ConfigureContext(ctx, octx, r.GetClient(), configs, verifications) if err != nil { status.MarkNotReady(r.GetEventRecorder(), component, v1alpha1.ConfigureContextFailedReason, err.Error()) @@ -198,7 +260,7 @@ func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context logger.Error(err, "failed to parse repository spec") status.MarkNotReady(r.EventRecorder, component, v1alpha1.RepositorySpecInvalidReason, "RepositorySpec is invalid") - return ctrl.Result{}, nil + return ctrl.Result{}, err } repo, err := session.LookupRepository(octx, spec) @@ -238,26 +300,57 @@ func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context return ctrl.Result{}, err } - err = r.Storage.ReconcileStorage(ctx, component) + // Store descriptors and create OCI artifact + logger.Info("pushing descriptors to storage") + ociRepositoryName, err := ociartifact.CreateRepositoryName(component.Spec.RepositoryRef.Name, component.GetName()) if err != nil { - status.MarkNotReady(r.EventRecorder, component, v1alpha1.StorageReconcileFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, component, v1alpha1.CreateOCIRepositoryNameFailedReason, err.Error()) - return ctrl.Result{}, fmt.Errorf("failed to reconcileComponent storage: %w", err) + return ctrl.Result{}, err } - err = r.createArtifactForDescriptors(ctx, octx, component, cv, descriptors) + ociRepository, err := r.Registry.NewRepository(ctx, ociRepositoryName) if err != nil { - status.MarkNotReady(r.EventRecorder, component, v1alpha1.ReconcileArtifactFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, component, v1alpha1.CreateOCIRepositoryFailedReason, err.Error()) return ctrl.Result{}, err } - // Update status - r.setComponentStatus(component, configs, v1alpha1.ComponentInfo{ + descriptorsBytes, err := yaml.Marshal(descriptors) + if err != nil { + status.MarkNotReady(r.EventRecorder, component, v1alpha1.MarshalFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + tag := ocm.NormalizeVersion(version) + manifestDigest, err := ociRepository.PushArtifact(ctx, tag, descriptorsBytes) + if err != nil { + status.MarkNotReady(r.EventRecorder, component, v1alpha1.PushOCIArtifactFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + ociArtifact := v1alpha1.OCIArtifactInfo{ + Repository: ociRepositoryName, + Digest: manifestDigest.String(), + Blob: &v1alpha1.BlobInfo{ + Digest: digest.FromBytes(descriptorsBytes).String(), + Tag: tag, + Size: int64(len(descriptorsBytes)), + }, + } + + logger.Info("updating status") + component.Status.OCIArtifact = &ociArtifact + + component.Status.Component = v1alpha1.ComponentInfo{ RepositorySpec: repository.Spec.RepositorySpec, Component: component.Spec.Component, Version: version, - }) + } + + component.Status.EffectiveOCMConfig = configs status.MarkReady(r.EventRecorder, component, "Applied version %s", version) @@ -356,73 +449,3 @@ func (r *Reconciler) verifyComponentVersionAndListDescriptors(ctx context.Contex return descriptors, nil } - -func (r *Reconciler) createArtifactForDescriptors(ctx context.Context, octx ocmctx.Context, - component *v1alpha1.Component, cv ocmctx.ComponentVersionAccess, descriptors *ocm.Descriptors, -) error { - logger := log.FromContext(ctx) - - // Create temp working dir - tmpDir, err := os.MkdirTemp("", fmt.Sprintf("%s-%s-%s-", component.Kind, component.Namespace, component.Name)) - if err != nil { - return reconcile.TerminalError(fmt.Errorf("failed to create temporary working directory: %w", err)) - } - octx.Finalizer().With(func() error { - if err = os.RemoveAll(tmpDir); err != nil { - ctrl.LoggerFrom(ctx).Error(err, "failed to remove temporary working directory") - } - - return nil - }) - - content, err := yaml.Marshal(descriptors) - if err != nil { - return reconcile.TerminalError(fmt.Errorf("failed to marshal content: %w", err)) - } - - const perm = 0o655 - if err := os.WriteFile(filepath.Join(tmpDir, v1alpha1.OCMComponentDescriptorList), content, perm); err != nil { - return reconcile.TerminalError(fmt.Errorf("failed to write file: %w", err)) - } - - revision := r.normalizeComponentVersionName(cv.GetName()) + "-" + cv.GetVersion() - if err := r.Storage.ReconcileArtifact( - ctx, - component, - revision, - tmpDir, - revision+".tar.gz", - func(art *artifactv1.Artifact, _ string) error { - // Archive directory to storage - if err := r.Storage.Archive(art, tmpDir, nil); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } - - component.Status.ArtifactRef = corev1.LocalObjectReference{ - Name: art.Name, - } - - return nil - }, - ); err != nil { - return fmt.Errorf("failed to reconcileComponent artifact: %w", err) - } - - logger.Info("successfully reconciled component", "name", component.Name) - - return nil -} - -func (r *Reconciler) normalizeComponentVersionName(name string) string { - return strings.ReplaceAll(name, "/", "-") -} - -func (r *Reconciler) setComponentStatus( - component *v1alpha1.Component, - configs []v1alpha1.OCMConfiguration, - info v1alpha1.ComponentInfo, -) { - component.Status.Component = info - - component.Status.EffectiveOCMConfig = configs -} diff --git a/internal/controller/component/component_controller_test.go b/internal/controller/component/component_controller_test.go index 8ac65307..523b052f 100644 --- a/internal/controller/component/component_controller_test.go +++ b/internal/controller/component/component_controller_test.go @@ -18,74 +18,63 @@ package component import ( "context" - "fmt" - "net/http" "os" "time" + "github.com/fluxcd/pkg/apis/meta" . "github.com/mandelsoft/goutils/testutils" + "github.com/mandelsoft/vfs/pkg/vfs" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "ocm.software/ocm/api/helper/builder" - - "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/pkg/runtime/conditions" - "github.com/fluxcd/pkg/tar" - "github.com/mandelsoft/filepath/pkg/filepath" - "github.com/mandelsoft/vfs/pkg/osfs" - "github.com/mandelsoft/vfs/pkg/vfs" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "ocm.software/ocm/api/ocm/extensions/repositories/ctf" - "ocm.software/ocm/api/utils/accessio" + . "ocm.software/ocm/api/helper/builder" "ocm.software/ocm/api/utils/accessobj" - "sigs.k8s.io/controller-runtime/pkg/envtest/komega" "sigs.k8s.io/yaml" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" + "github.com/fluxcd/pkg/runtime/conditions" + "github.com/mandelsoft/vfs/pkg/osfs" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" environment "ocm.software/ocm/api/helper/env" + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/utils/accessio" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest/komega" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) const ( - CTFPath = "ocm-k8s-ctfstore--*" - Namespace = "test-namespace" - RepositoryObj = "test-repository" - Component = "ocm.software/test-component" - ComponentObj = "test-component" - Version1 = "1.0.0" - Version2 = "1.0.1" + CTFPath = "ocm-k8s-ctfstore--*" + Component = "ocm.software/test-component" + ComponentObj = "test-component" + Version1 = "1.0.0" + Version2 = "1.0.1" ) var _ = Describe("Component Controller", func() { var ( - ctx context.Context - cancel context.CancelFunc env *Builder ctfpath string - - repositoryName string - testNumber int - repositoryObj *v1alpha1.OCMRepository ) BeforeEach(func() { ctfpath = Must(os.MkdirTemp("", CTFPath)) env = NewBuilder(environment.FileSystem(osfs.OsFs)) - ctx = context.Background() - ctx, cancel = context.WithCancel(context.Background()) }) AfterEach(func() { Expect(os.RemoveAll(ctfpath)).To(Succeed()) Expect(env.Cleanup()).To(Succeed()) - cancel() }) Context("component controller", func() { - BeforeEach(func() { + var repositoryObj *v1alpha1.OCMRepository + var namespace *corev1.Namespace + + BeforeEach(func(ctx SpecContext) { By("creating a repository with name") env.OCMCommonTransport(ctfpath, accessio.FormatDirectory, func() { env.Component(Component, func() { @@ -96,10 +85,18 @@ var _ = Describe("Component Controller", func() { spec := Must(ctf.NewRepositorySpec(ctf.ACC_READONLY, ctfpath)) specdata := Must(spec.MarshalJSON()) - repositoryName = fmt.Sprintf("%s-%d", RepositoryObj, testNumber) + namespaceName := test.GenerateNamespace(ctx.SpecReport().LeafNodeText) + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + }, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + + repositoryName := "repository" repositoryObj = &v1alpha1.OCMRepository{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, + Namespace: namespaceName, Name: repositoryName, }, Spec: v1alpha1.OCMRepositorySpec{ @@ -113,27 +110,35 @@ var _ = Describe("Component Controller", func() { conditions.MarkTrue(repositoryObj, "Ready", "ready", "message") Expect(k8sClient.Status().Update(ctx, repositoryObj)).To(Succeed()) - - testNumber++ }) - AfterEach(func() { - // make sure the repo is still ready - conditions.MarkTrue(repositoryObj, "Ready", "ready", "message") - Expect(k8sClient.Status().Update(ctx, repositoryObj)).To(Succeed()) + AfterEach(func(ctx SpecContext) { + By("deleting the repository") + Expect(k8sClient.Delete(ctx, repositoryObj)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(repositoryObj), repositoryObj) + return errors.IsNotFound(err) + }).WithContext(ctx).Should(BeTrue()) + + components := &v1alpha1.ComponentList{} + + Expect(k8sClient.List(ctx, components, client.InNamespace(namespace.GetName()))).To(Succeed()) + Expect(components.Items).To(HaveLen(0)) + + // TODO: test if OCI artifact was deleted }) - It("reconcileComponent a component", func() { + It("reconcileComponent a component", func(ctx SpecContext) { By("creating a component") component := &v1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: fmt.Sprintf("%s-%d", ComponentObj, testNumber), + Namespace: namespace.GetName(), + Name: ComponentObj, }, Spec: v1alpha1.ComponentSpec{ RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: repositoryName, + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), }, Component: Component, Semver: "1.0.0", @@ -143,39 +148,15 @@ var _ = Describe("Component Controller", func() { } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - By("check that artifact has been created successfully") - - Eventually(komega.Object(component), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) - - artifact := &artifactv1.Artifact{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: component.Namespace, - Name: component.Status.ArtifactRef.Name, - }, - } - Eventually(komega.Get(artifact)).Should(Succeed()) - - By("check if the component descriptor list can be retrieved from the artifact server") - r := Must(http.Get(artifact.Spec.URL)) - - tmpdir := Must(os.MkdirTemp("/tmp", "descriptors-")) - DeferCleanup(func() error { - return os.RemoveAll(tmpdir) - }) - MustBeSuccessful(tar.Untar(r.Body, tmpdir)) - - repo := Must(ctf.Open(env, accessobj.ACC_WRITABLE, ctfpath, vfs.FileMode(vfs.O_RDWR), env)) - cv := Must(repo.LookupComponentVersion(Component, Version1)) - expecteddescs := Must(ocm.ListComponentDescriptors(ctx, cv, repo)) + By("checking that the component has been reconciled successfully") + waitUntilComponentIsReady(ctx, component, "1.0.0") + validateArtifact(ctx, component, env, ctfpath) - data := Must(os.ReadFile(filepath.Join(tmpdir, v1alpha1.OCMComponentDescriptorList))) - descs := &ocm.Descriptors{} - MustBeSuccessful(yaml.Unmarshal(data, descs)) - Expect(descs).To(YAMLEqual(expecteddescs)) + By("delete resources manually") + deleteComponent(ctx, component) }) - It("does not reconcile when the repository is not ready", func() { + It("does not reconcile when the repository is not ready", func(ctx SpecContext) { By("marking the repository as not ready") conditions.MarkFalse(repositoryObj, "Ready", "notReady", "reason") Expect(k8sClient.Status().Update(ctx, repositoryObj)).To(Succeed()) @@ -183,13 +164,13 @@ var _ = Describe("Component Controller", func() { By("creating a component object") component := &v1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: ComponentObj + "-not-ready", + Namespace: namespace.GetName(), + Name: ComponentObj, }, Spec: v1alpha1.ComponentSpec{ RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: repositoryName, + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), }, Component: Component, Semver: "1.0.0", @@ -199,22 +180,40 @@ var _ = Describe("Component Controller", func() { } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - By("check that no artifact has been created") - Eventually(komega.Object(component), "15s").Should( - HaveField("Status.ArtifactRef.Name", BeEmpty())) + By("checking that the component has not been reconciled successfully") + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + + // Conditions are not nil, if reconciliation has run at least once. + return component.Status.Conditions != nil && !conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) + + By("checking that reference to OCI artifact has not been created") + Expect(component).To(HaveField("Status.OCIArtifact", BeNil())) + + By("deleting the resources manually") + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) - It("grabs the new version when it becomes available", func() { + It("grabs the new version when it becomes available", func(ctx SpecContext) { By("creating a component") component := &v1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: fmt.Sprintf("%s-%d", ComponentObj, testNumber), + Namespace: namespace.GetName(), + Name: ComponentObj, }, Spec: v1alpha1.ComponentSpec{ RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: repositoryName, + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), }, Component: Component, Semver: ">=1.0.0", @@ -224,14 +223,11 @@ var _ = Describe("Component Controller", func() { } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - By("check that artifact has been created successfully") - - Eventually(komega.Object(component), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) - - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) - Expect(component.Status.Component.Version).To(Equal(Version1)) + By("checking that the component has been reconciled successfully") + waitUntilComponentIsReady(ctx, component, Version1) + validateArtifact(ctx, component, env, ctfpath) + By("increasing the component version") env.OCMCommonTransport(ctfpath, accessio.FormatDirectory, func() { env.Component(Component, func() { env.Version(Version1) @@ -241,14 +237,17 @@ var _ = Describe("Component Controller", func() { }) }) - Eventually(func() bool { - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) + By("checking that the increased version has been discovered successfully") + waitUntilComponentIsReady(ctx, component, Version2) - return component.Status.Component.Version == Version2 - }).WithTimeout(15 * time.Second).Should(BeTrue()) + By("checking that increased version is reflected in the OCI artifact") + validateArtifact(ctx, component, env, ctfpath) + + By("delete resources manually") + deleteComponent(ctx, component) }) - It("grabs lower version if downgrade is allowed", func() { + It("grabs lower version if downgrade is allowed", func(ctx SpecContext) { componentName := Component + "-downgrade" env.OCMCommonTransport(ctfpath, accessio.FormatDirectory, func() { env.Component(componentName, func() { @@ -264,13 +263,13 @@ var _ = Describe("Component Controller", func() { By("creating a component") component := &v1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: fmt.Sprintf("%s-%d", ComponentObj, testNumber), + Namespace: namespace.GetName(), + Name: ComponentObj, }, Spec: v1alpha1.ComponentSpec{ RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: repositoryName, + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), }, Component: componentName, DowngradePolicy: v1alpha1.DowngradePolicyAllow, @@ -281,24 +280,23 @@ var _ = Describe("Component Controller", func() { } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - By("check that artifact has been created successfully") - - Eventually(komega.Object(component), "15s").Should(HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) - - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) - Expect(component.Status.Component.Version).To(Equal("0.0.3")) + By("checking that the component has been reconciled successfully") + waitUntilComponentIsReady(ctx, component, "0.0.3") + validateArtifact(ctx, component, env, ctfpath) + By("decreasing the component version") component.Spec.Semver = "0.0.2" Expect(k8sClient.Update(ctx, component)).To(Succeed()) - Eventually(func() bool { - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) + By("checking that the decreased version has been discovered successfully") + waitUntilComponentIsReady(ctx, component, "0.0.2") + validateArtifact(ctx, component, env, ctfpath) - return component.Status.Component.Version == "0.0.2" - }).WithTimeout(15 * time.Second).Should(BeTrue()) + By("delete resources manually") + deleteComponent(ctx, component) }) - It("does not grab lower version if downgrade is denied", func() { + It("does not grab lower version if downgrade is denied", func(ctx SpecContext) { componentName := Component + "-downgrade-2" env.OCMCommonTransport(ctfpath, accessio.FormatDirectory, func() { env.Component(componentName, func() { @@ -314,13 +312,13 @@ var _ = Describe("Component Controller", func() { By("creating a component") component := &v1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: fmt.Sprintf("%s-%d", ComponentObj, testNumber), + Namespace: namespace.GetName(), + Name: ComponentObj, }, Spec: v1alpha1.ComponentSpec{ RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: repositoryName, + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), }, Component: componentName, DowngradePolicy: v1alpha1.DowngradePolicyDeny, @@ -330,24 +328,29 @@ var _ = Describe("Component Controller", func() { } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - By("check that artifact has been created successfully") - Eventually(komega.Object(component), "15s").Should(HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) - - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) - Expect(component.Status.Component.Version).To(Equal("0.0.3")) + By("checking that the component has been reconciled successfully") + waitUntilComponentIsReady(ctx, component, "0.0.3") + validateArtifact(ctx, component, env, ctfpath) + By("trying to decrease component version") component.Spec.Semver = "0.0.2" Expect(k8sClient.Update(ctx, component)).To(Succeed()) + By("checking that downgrade was not allowed") Eventually(func() bool { Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) cond := conditions.Get(component, meta.ReadyCondition) return cond.Message == "terminal error: component version cannot be downgraded from version 0.0.3 to version 0.0.2" }).WithTimeout(15 * time.Second).Should(BeTrue()) + Expect(component.Status.Component.Version).To(Equal("0.0.3")) + Expect(component.Status.OCIArtifact.Blob.Tag).To(Equal("0.0.3")) + + By("delete resources manually") + deleteComponent(ctx, component) }) - It("can force downgrade even if not allowed by the component", func() { + It("can force downgrade even if not allowed by the component", func(ctx SpecContext) { componentName := Component + "-downgrade-3" env.OCMCommonTransport(ctfpath, accessio.FormatDirectory, func() { env.Component(componentName, func() { @@ -359,13 +362,13 @@ var _ = Describe("Component Controller", func() { By("creating a component") component := &v1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: fmt.Sprintf("%s-%d", ComponentObj, testNumber), + Namespace: namespace.GetName(), + Name: ComponentObj, }, Spec: v1alpha1.ComponentSpec{ RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: repositoryName, + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), }, Component: componentName, DowngradePolicy: v1alpha1.DowngradePolicyEnforce, @@ -376,36 +379,75 @@ var _ = Describe("Component Controller", func() { } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - By("check that artifact has been created successfully") - - Eventually(komega.Object(component), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) - - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) - Expect(component.Status.Component.Version).To(Equal("0.0.3")) + By("checking that the component has been reconciled successfully") + waitUntilComponentIsReady(ctx, component, "0.0.3") + validateArtifact(ctx, component, env, ctfpath) + By("decreasing the component version") component.Spec.Semver = "0.0.2" Expect(k8sClient.Update(ctx, component)).To(Succeed()) - Eventually(func() bool { - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) + By("checking that the decreased version has been discovered successfully") + waitUntilComponentIsReady(ctx, component, "0.0.2") + validateArtifact(ctx, component, env, ctfpath) - return component.Status.Component.Version == "0.0.2" - }).WithTimeout(15 * time.Second).Should(BeTrue()) + By("delete resources manually") + deleteComponent(ctx, component) + }) + + It("reconcile a component with a plus in the version", func(ctx SpecContext) { + componentName := Component + "-with-plus" + componentObjName := ComponentObj + "-with-plus" + componentVersionPlus := Version1 + "+componentVersionSuffix" + expectedBlobTag := Version1 + ".build-componentVersionSuffix" + + By("creating a component in CTF repository") + env.OCMCommonTransport(ctfpath, accessio.FormatDirectory, func() { + env.Component(componentName, func() { + env.Version(componentVersionPlus) + }) + }) + + By("creating a component resource") + component := &v1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace.GetName(), + Name: componentObjName, + }, + Spec: v1alpha1.ComponentSpec{ + RepositoryRef: v1alpha1.ObjectKey{ + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), + }, + Component: componentName, + Semver: componentVersionPlus, + Interval: metav1.Duration{Duration: time.Minute * 10}, + }, + Status: v1alpha1.ComponentStatus{}, + } + Expect(k8sClient.Create(ctx, component)).To(Succeed()) + + By("checking that the component has been reconciled successfully") + waitUntilComponentIsReady(ctx, component, componentVersionPlus) + validateArtifact(ctx, component, env, ctfpath) + + By("checking that artifact's blob tag is properly set") + Expect(component.Status.OCIArtifact.Blob.Tag).To(Equal(expectedBlobTag)) + + By("delete resources manually") + deleteComponent(ctx, component) }) }) Context("ocm config handling", func() { - const ( - Namespace = "test-namespace" - ) - var ( - configs []*corev1.ConfigMap - secrets []*corev1.Secret + configs []*corev1.ConfigMap + secrets []*corev1.Secret + namespace *corev1.Namespace + repositoryObj *v1alpha1.OCMRepository ) - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { By("creating a repository with name") env.OCMCommonTransport(ctfpath, accessio.FormatDirectory, func() { env.Component(Component, func() { @@ -416,12 +458,20 @@ var _ = Describe("Component Controller", func() { spec := Must(ctf.NewRepositorySpec(ctf.ACC_READONLY, ctfpath)) specdata := Must(spec.MarshalJSON()) - configs, secrets = createTestConfigsAndSecrets(ctx) + namespaceName := test.GenerateNamespace(ctx.SpecReport().LeafNodeText) + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + }, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) - repositoryName = fmt.Sprintf("%s-%d", RepositoryObj, testNumber) + configs, secrets = createTestConfigsAndSecrets(ctx, namespace.GetName()) + + repositoryName := "repository" repositoryObj = &v1alpha1.OCMRepository{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, + Namespace: namespace.GetName(), Name: repositoryName, }, Spec: v1alpha1.OCMRepositorySpec{ @@ -543,28 +593,42 @@ var _ = Describe("Component Controller", func() { conditions.MarkTrue(repositoryObj, "Ready", "ready", "message") Expect(k8sClient.Status().Update(ctx, repositoryObj)).To(Succeed()) - - testNumber++ }) - AfterEach(func() { - // make sure the repo is still ready + AfterEach(func(ctx SpecContext) { + By("make sure the repo is still ready") conditions.MarkTrue(repositoryObj, "Ready", "ready", "message") Expect(k8sClient.Status().Update(ctx, repositoryObj)).To(Succeed()) cleanupTestConfigsAndSecrets(ctx, configs, secrets) + + By("delete repository") + Expect(k8sClient.Delete(ctx, repositoryObj)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(repositoryObj), repositoryObj) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) + + By("ensuring no components are left") + Eventually(func(g Gomega, ctx SpecContext) { + components := &v1alpha1.ComponentList{} + g.Expect(k8sClient.List(ctx, components, client.InNamespace(namespace.GetName()))).To(Succeed()) + g.Expect(components.Items).To(HaveLen(0)) + }, "15s").WithContext(ctx).Should(Succeed()) + + // TODO: check that the OCI artifact is not in the registry anymore }) - It("component resolves and propagates config from repository", func() { + It("component resolves and propagates config from repository", func(ctx SpecContext) { By("creating a component") component := &v1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: fmt.Sprintf("%s-%d", ComponentObj, testNumber), + Namespace: namespace.GetName(), + Name: ComponentObj, }, Spec: v1alpha1.ComponentSpec{ RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: repositoryName, + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), }, Component: Component, Semver: "1.0.0", @@ -573,8 +637,8 @@ var _ = Describe("Component Controller", func() { NamespacedObjectKindReference: meta.NamespacedObjectKindReference{ APIVersion: v1alpha1.GroupVersion.String(), Kind: v1alpha1.KindOCMRepository, - Name: repositoryName, - Namespace: Namespace, + Namespace: namespace.GetName(), + Name: repositoryObj.GetName(), }, Policy: v1alpha1.ConfigurationPolicyDoNotPropagate, }, @@ -585,6 +649,11 @@ var _ = Describe("Component Controller", func() { } Expect(k8sClient.Create(ctx, component)).To(Succeed()) + By("checking that the component has been reconciled successfully") + waitUntilComponentIsReady(ctx, component, "1.0.0") + validateArtifact(ctx, component, env, ctfpath) + + By("checking component's effective OCM config") Eventually(komega.Object(component), "15s").Should( HaveField("Status.EffectiveOCMConfig", ConsistOf( v1alpha1.OCMConfiguration{ @@ -605,12 +674,59 @@ var _ = Describe("Component Controller", func() { }, Policy: v1alpha1.ConfigurationPolicyDoNotPropagate, }, - ))) + )), + ) + + By("delete resources manually") + deleteComponent(ctx, component) }) }) }) -func createTestConfigsAndSecrets(ctx context.Context) (configs []*corev1.ConfigMap, secrets []*corev1.Secret) { +func waitUntilComponentIsReady(ctx context.Context, component *v1alpha1.Component, expectedVersion string) { + GinkgoHelper() + Eventually(func(g Gomega, ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + g.Expect(component).Should(HaveField("Status.Component.Version", expectedVersion)) + + return conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) +} + +func validateArtifact(ctx context.Context, component *v1alpha1.Component, env *Builder, ctfPath string) { + GinkgoHelper() + + By("checking that component has a reference to OCI artifact") + Eventually(komega.Object(component), "15s").Should( + HaveField("Status.OCIArtifact", Not(BeNil()))) + + By("checking that the OCI artifact contains the correct content") + ociRepo := Must(registry.NewRepository(ctx, component.GetOCIRepository())) + componentContent := Must(ociRepo.FetchArtifact(ctx, component.GetManifestDigest())) + + descriptors := &ocm.Descriptors{} + MustBeSuccessful(yaml.Unmarshal(componentContent, descriptors)) + ctfRepo := Must(ctf.Open(env, accessobj.ACC_WRITABLE, ctfPath, vfs.FileMode(vfs.O_RDWR), env)) + cv := Must(ctfRepo.LookupComponentVersion(component.Status.Component.Component, component.Status.Component.Version)) + expectedDescriptors := Must(ocm.ListComponentDescriptors(ctx, cv, ctfRepo)) + Expect(descriptors).To(YAMLEqual(expectedDescriptors)) +} + +func deleteComponent(ctx context.Context, component *v1alpha1.Component) { + GinkgoHelper() + + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) +} + +func createTestConfigsAndSecrets(ctx context.Context, namespace string) (configs []*corev1.ConfigMap, secrets []*corev1.Secret) { const ( Config1 = "config1" Config2 = "config2" @@ -624,7 +740,7 @@ func createTestConfigsAndSecrets(ctx context.Context) (configs []*corev1.ConfigM By("setup configs") config1 := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, + Namespace: namespace, Name: Config1, }, Data: map[string]string{ @@ -653,7 +769,7 @@ sets: config2 := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, + Namespace: namespace, Name: Config2, }, Data: map[string]string{ @@ -682,7 +798,7 @@ sets: config3 := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, + Namespace: namespace, Name: Config3, }, Data: map[string]string{ @@ -712,7 +828,7 @@ sets: By("setup secrets") secret1 := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, + Namespace: namespace, Name: Secret1, }, Data: map[string][]byte{ @@ -736,7 +852,7 @@ consumers: secret2 := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, + Namespace: namespace, Name: Secret2, }, Data: map[string][]byte{ @@ -760,7 +876,7 @@ consumers: secret3 := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, + Namespace: namespace, Name: Secret3, }, Data: map[string][]byte{ diff --git a/internal/controller/component/suite_test.go b/internal/controller/component/suite_test.go index c7c66166..4380bda8 100644 --- a/internal/controller/component/suite_test.go +++ b/internal/controller/component/suite_test.go @@ -16,38 +16,30 @@ package component import ( "context" "fmt" - "io" - "net/http" "os" + "os/exec" "path/filepath" "runtime" "testing" - "time" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - "github.com/openfluxcd/controller-manager/server" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/yaml" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" metricserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // +kubebuilder:scaffold:imports @@ -55,15 +47,14 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -const ( - ARTIFACT_PATH = "ocm-k8s-artifactstore--*" - ARTIFACT_SERVER = "localhost:8080" -) - var cfg *rest.Config var k8sClient client.Client var k8sManager ctrl.Manager var testEnv *envtest.Environment +var zotCmd *exec.Cmd +var registry *ociartifact.Registry +var zotRootDir string +var recorder record.EventRecorder func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -76,26 +67,10 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") - // Get external artifact CRD - resp, err := http.Get(v1alpha1.ArtifactCrd) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() error { - return resp.Body.Close() - }) - - crdByte, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - - artifactCRD := &apiextensionsv1.CustomResourceDefinition{} - err = yaml.Unmarshal(crdByte, artifactCRD) - Expect(err).NotTo(HaveOccurred()) - testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, - CRDs: []*apiextensionsv1.CustomResourceDefinition{artifactCRD}, - // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the // default path defined in controller-runtime which is /usr/local/kubebuilder/. @@ -105,6 +80,8 @@ var _ = BeforeSuite(func() { fmt.Sprintf("1.30.0-%s-%s", runtime.GOOS, runtime.GOARCH)), } + var err error + // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) @@ -112,7 +89,6 @@ var _ = BeforeSuite(func() { DeferCleanup(testEnv.Stop) Expect(v1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) - Expect(artifactv1.AddToScheme(scheme.Scheme)).Should(Succeed()) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme @@ -130,39 +106,49 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - tmpdir := Must(os.MkdirTemp("", ARTIFACT_PATH)) - address := ARTIFACT_SERVER - storage := Must(server.NewStorage(k8sClient, testEnv.Scheme, tmpdir, address, 0, 0)) - artifactServer := Must(server.NewArtifactServer(tmpdir, address, time.Millisecond)) + // Setup zot registry and start it up + zotRootDir = Must(os.MkdirTemp("", "")) + DeferCleanup(func() { + Expect(os.RemoveAll(zotRootDir)).To(Succeed()) + }) - Expect((&Reconciler{ - BaseReconciler: &ocm.BaseReconciler{ - Client: k8sClient, - Scheme: testEnv.Scheme, - EventRecorder: &record.FakeRecorder{ - Events: make(chan string, 32), - IncludeObject: true, - }, - }, - Storage: storage, - }).SetupWithManager(k8sManager)).To(Succeed()) + zotCmd, registry = test.SetupRegistry(filepath.Join("..", "..", "..", "bin", "zot-registry"), zotRootDir, "0.0.0.0", "8080") + events := make(chan string) + recorder = &record.FakeRecorder{ + Events: events, + IncludeObject: true, + } ctx, cancel := context.WithCancel(context.Background()) DeferCleanup(cancel) - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: Namespace, - }, - } - Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) - go func() { - defer GinkgoRecover() - Expect(artifactServer.Start(ctx)).To(Succeed()) + for { + select { + case event := <-events: + GinkgoLogr.Info("Event received", "event", event) + case <-ctx.Done(): + return + } + } }() + + Expect((&Reconciler{ + BaseReconciler: &ocm.BaseReconciler{ + Client: k8sClient, + Scheme: testEnv.Scheme, + EventRecorder: recorder, + }, + Registry: registry, + }).SetupWithManager(k8sManager)).To(Succeed()) + go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) }() }) + +var _ = AfterSuite(func(ctx context.Context) { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") +}) diff --git a/internal/controller/configuration/client/client.go b/internal/controller/configuration/client/client.go index 68755254..8e2ac252 100644 --- a/internal/controller/configuration/client/client.go +++ b/internal/controller/configuration/client/client.go @@ -4,14 +4,13 @@ import ( "context" "fmt" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/configuration/types" - artifactutil "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" ) type Client interface { @@ -24,24 +23,24 @@ type Client interface { GetTarget(ctx context.Context, ref v1alpha1.ConfigurationReference) (target types.ConfigurationTarget, err error) } -func NewClientWithLocalStorage(r client.Reader, s *storage.Storage, scheme *runtime.Scheme) Client { +func NewClientWithRegistry(r client.Reader, registry ociartifact.RegistryType, scheme *runtime.Scheme) Client { factory := serializer.NewCodecFactory(scheme) info, _ := runtime.SerializerInfoForMediaType(factory.SupportedMediaTypes(), runtime.ContentTypeYAML) encoder := factory.EncoderForVersion(info.Serializer, v1alpha1.GroupVersion) return &localStorageBackedClient{ - Reader: r, - Storage: s, - scheme: scheme, - encoder: encoder, + Reader: r, + Registry: registry, + scheme: scheme, + encoder: encoder, } } type localStorageBackedClient struct { client.Reader - *storage.Storage - scheme *runtime.Scheme - encoder runtime.Encoder + Registry ociartifact.RegistryType + scheme *runtime.Scheme + encoder runtime.Encoder } var _ Client = &localStorageBackedClient{} @@ -57,7 +56,7 @@ func (clnt *localStorageBackedClient) GetTarget(ctx context.Context, ref v1alpha case v1alpha1.KindLocalizedResource: fallthrough case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return ociartifact.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Registry, &ref) default: return nil, fmt.Errorf("unsupported configuration target kind: %s", ref.Kind) } @@ -66,7 +65,7 @@ func (clnt *localStorageBackedClient) GetTarget(ctx context.Context, ref v1alpha func (clnt *localStorageBackedClient) GetConfiguration(ctx context.Context, ref v1alpha1.ConfigurationReference) (source types.ConfigurationSource, err error) { switch ref.Kind { case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return ociartifact.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Registry, &ref) case v1alpha1.KindResourceConfig: return GetResourceConfigFromKubernetes(ctx, clnt.Reader, clnt.encoder, ref) default: @@ -90,5 +89,5 @@ func GetResourceConfigFromKubernetes(ctx context.Context, clnt client.Reader, en return nil, fmt.Errorf("failed to fetch localization config %s: %w", reference.Name, err) } - return &artifactutil.ObjectConfig{Object: &cfg, Encoder: encoder}, nil + return &ociartifact.ObjectConfig{Object: &cfg, Encoder: encoder}, nil } diff --git a/internal/controller/configuration/configuration_controller.go b/internal/controller/configuration/configuration_controller.go index 5fdd2a22..b3a89f51 100644 --- a/internal/controller/configuration/configuration_controller.go +++ b/internal/controller/configuration/configuration_controller.go @@ -23,20 +23,18 @@ import ( "os" "github.com/fluxcd/pkg/runtime/patch" - "github.com/openfluxcd/controller-manager/storage" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" configurationclient "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/configuration/client" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" "github.com/open-component-model/ocm-k8s-toolkit/pkg/index" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" ) @@ -50,8 +48,6 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.ConfiguredResource{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - // Update when the owned artifact containing the configured data changes - Owns(&artifactv1.Artifact{}). // Update when a resource specified as target changes Watches(&v1alpha1.Resource{}, onTargetChange). Watches(&v1alpha1.LocalizedResource{}, onTargetChange). @@ -69,8 +65,8 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { // Reconciler reconciles a ConfiguredResource object. type Reconciler struct { *ocm.BaseReconciler - *storage.Storage ConfigClient configurationclient.Client + Registry ociartifact.RegistryType } // +kubebuilder:rbac:groups=delivery.ocm.software,resources=configuredresources,verbs=get;list;watch;create;update;patch;delete @@ -85,6 +81,8 @@ type Reconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, err error) { + logger := log.FromContext(ctx) + configuration := &v1alpha1.ConfiguredResource{} if err := r.Get(ctx, req.NamespacedName, configuration); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -94,23 +92,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re return ctrl.Result{}, nil } - if !configuration.GetDeletionTimestamp().IsZero() { - // TODO: This is a temporary solution until a artifact-reconciler is written to handle the deletion of artifacts - if err := ocm.RemoveArtifactForCollectable(ctx, r.Client, r.Storage, configuration); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to remove artifact: %w", err) - } - - if removed := controllerutil.RemoveFinalizer(configuration, v1alpha1.ArtifactFinalizer); removed { - if err := r.Update(ctx, configuration); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err) - } - } - - return ctrl.Result{}, nil - } + if configuration.GetDeletionTimestamp() != nil { + logger.Info("configuration is being deleted and cannot be used", "name", configuration.Name) - if added := controllerutil.AddFinalizer(configuration, v1alpha1.ArtifactFinalizer); added { - return ctrl.Result{Requeue: true}, r.Update(ctx, configuration) + return ctrl.Result{Requeue: true}, nil } return r.reconcileWithStatusUpdate(ctx, configuration) @@ -131,13 +116,10 @@ func (r *Reconciler) reconcileWithStatusUpdate(ctx context.Context, localization return result, nil } +//nolint:funlen // function length is acceptable func (r *Reconciler) reconcileExists(ctx context.Context, configuration *v1alpha1.ConfiguredResource) (ctrl.Result, error) { logger := log.FromContext(ctx) - if err := r.Storage.ReconcileStorage(ctx, configuration); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to reconcile storage: %w", err) - } - if configuration.Spec.Target.Namespace == "" { configuration.Spec.Target.Namespace = configuration.Namespace } @@ -160,28 +142,46 @@ func (r *Reconciler) reconcileExists(ctx context.Context, configuration *v1alpha return ctrl.Result{}, fmt.Errorf("failed to fetch cfg: %w", err) } - digest, revision, filename, err := artifact.UniqueIDsForArtifactContentCombination(cfg, target) + combinedDigest, revision, _, err := ociartifact.UniqueIDsForArtifactContentCombination(cfg, target) if err != nil { status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.UniqueIDGenerationFailedReason, err.Error()) - return ctrl.Result{}, fmt.Errorf("failed to map digest from config to target: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to map combinedDigest from config to target: %w", err) } - logger.V(1).Info("verifying configuration", "digest", digest, "revision", revision) - hasValidArtifact, err := ocm.ValidateArtifactForCollectable( - ctx, - r.Client, - r.Storage, - configuration, - digest, - ) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to check if artifact is valid: %w", err) + // TODO: we cannot use `combinedDigest` to determine a change as the combinedDigest calculation is incorrect + // (it takes a k8s object with managed fields that change on every update). + + // Check if an OCI artifact of the configuration resource already exists and if it holds the same calculated combinedDigest + // from above + logger.V(1).Info("verifying configuration", "combinedDigest", combinedDigest, "revision", revision) + + hasValidArtifact := false + + if configuration.GetOCIArtifact() != nil { + ociRepository, err := r.Registry.NewRepository(ctx, configuration.GetOCIRepository()) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.CreateOCIRepositoryFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + exists, err := ociRepository.ExistsArtifact(ctx, configuration.GetManifestDigest()) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.OCIRepositoryExistsFailedReason, err.Error()) + + return ctrl.Result{}, err + } + if exists { + hasValidArtifact = combinedDigest == configuration.GetBlobDigest() + } } - var configured string + // If no valid OCI artifact is present (because it never existed or is just not valid), we will configure the target, + // create an OCI artifact and return. + //nolint:nestif // Ignore as it is not that complex. if !hasValidArtifact { - logger.V(1).Info("configuring", "digest", digest, "revision", revision) + logger.V(1).Info("configuring", "combinedDigest", combinedDigest, "revision", revision) basePath, err := os.MkdirTemp("", "configured-") if err != nil { return ctrl.Result{}, fmt.Errorf("failed to create temporary directory to perform configuration: %w", err) @@ -192,43 +192,60 @@ func (r *Reconciler) reconcileExists(ctx context.Context, configuration *v1alpha } }() - if configured, err = Configure(ctx, r.ConfigClient, cfg, target, basePath); err != nil { + configured, err := Configure(ctx, r.ConfigClient, cfg, target, basePath) + if err != nil { status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) return ctrl.Result{}, fmt.Errorf("failed to configure: %w", err) } - } - configuration.Status.Digest = digest - - if err := r.Storage.ReconcileArtifact( - ctx, - configuration, - revision, - configured, - filename, - func(artifact *artifactv1.Artifact, dir string) error { - if !hasValidArtifact { - // Archive directory to storage - if err := r.Storage.Archive(artifact, dir, nil); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } - } + // Create archive from configured directory and gzip it. + dataTGZ, err := compression.CreateTGZFromPath(configured) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.CreateTGZFailedReason, err.Error()) - configuration.Status.ArtifactRef = &v1alpha1.ObjectKey{ - Name: artifact.Name, - Namespace: artifact.Namespace, - } + return ctrl.Result{}, fmt.Errorf("failed to create TGZ from path: %w", err) + } - return nil - }, - ); err != nil { - status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ReconcileArtifactFailedReason, err.Error()) + repositoryName, err := ociartifact.CreateRepositoryName(configuration.GetName()) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.CreateOCIRepositoryNameFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to create repository name: %w", err) + } - return ctrl.Result{}, fmt.Errorf("failed to reconcile artifact: %w", err) + repository, err := r.Registry.NewRepository(ctx, repositoryName) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to configure: %w", err) + } + + // TODO: Find out which version should be used to tag the OCI artifact. + // Things to consider: + // - HelmRelease (FluxCD) requires the OCI artifact to have the same tag as the helm chart itself + // - But how to get the helm chart version? (User input, parse from content) + tag := "dummy" + manifestDigest, err := repository.PushArtifact(ctx, tag, dataTGZ) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to configure: %w", err) + } + + // We use the combinedDigest calculated above for the blob-info combinedDigest, so we can compare for any changes + configuration.Status.OCIArtifact = &v1alpha1.OCIArtifactInfo{ + Repository: repositoryName, + Digest: manifestDigest.String(), + Blob: &v1alpha1.BlobInfo{ + Digest: combinedDigest, + Tag: tag, + Size: int64(len(dataTGZ)), + }, + } } - logger.Info("configuration successful", "artifact", configuration.Status.ArtifactRef) + logger.Info("configuration successful", "OCIArtifact", configuration.GetOCIArtifact()) status.MarkReady(r.EventRecorder, configuration, "configured successfully") return ctrl.Result{RequeueAfter: configuration.Spec.Interval.Duration}, nil diff --git a/internal/controller/configuration/configuration_controller_test.go b/internal/controller/configuration/configuration_controller_test.go index 84686e8a..20fb08e2 100644 --- a/internal/controller/configuration/configuration_controller_test.go +++ b/internal/controller/configuration/configuration_controller_test.go @@ -2,20 +2,22 @@ package configuration import ( "context" + "fmt" + "os" "path/filepath" "time" _ "embed" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - + "github.com/fluxcd/pkg/runtime/conditions" + . "github.com/mandelsoft/goutils/testutils" "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/projectionfs" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -23,6 +25,7 @@ import ( environment "ocm.software/ocm/api/helper/env" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) @@ -58,36 +61,50 @@ var _ = Describe("ConfiguredResource Controller", func() { }) It("should configure an artifact from a resource based on a ResourceConfig", func(ctx SpecContext) { - component := NoOpComponent(ctx, tmp) + By("creating a mock component") + component := NoOpComponent(ctx) + By("creating a mock target resource") fileToConfigure := "test.yaml" fileContentBeforeConfiguration := []byte(`mykey: "value"`) fileContentAfterConfiguration := []byte(`mykey: "substituted"`) dir := filepath.Join(tmp, "test") - test.CreateTGZ(dir, map[string][]byte{ - fileToConfigure: fileContentBeforeConfiguration, - }) + Expect(os.Mkdir(dir, os.ModePerm|os.ModeDir)).To(Succeed()) + + path := filepath.Join(dir, fileToConfigure) + + writer := Must(os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.ModePerm)) + defer func() { + Expect(writer.Close()).To(Succeed()) + }() + + Must(writer.Write(fileContentBeforeConfiguration)) targetResource := test.SetupMockResourceWithData(ctx, TargetResourceObj, Namespace, &test.MockResourceOptions{ - BasePath: tmp, DataPath: dir, ComponentRef: v1alpha1.ObjectKey{ Namespace: Namespace, Name: component.GetName(), }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, ) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, targetResource, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(targetResource), targetResource) + if err != nil { + return false + } + return conditions.IsReady(targetResource) + }, "15s").WithContext(ctx).Should(BeTrue()) + + By("creating a resource config") cfg := v1alpha1.ResourceConfig{ ObjectMeta: metav1.ObjectMeta{ Name: ResourceConfig, @@ -114,10 +131,8 @@ var _ = Describe("ConfiguredResource Controller", func() { }, } Expect(k8sClient.Create(ctx, &cfg)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, &cfg, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) + By("creating a configured resource") configuredResource := &v1alpha1.ConfiguredResource{ ObjectMeta: metav1.ObjectMeta{ Name: ConfiguredResource, @@ -130,34 +145,55 @@ var _ = Describe("ConfiguredResource Controller", func() { }, } Expect(k8sClient.Create(ctx, configuredResource)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, configuredResource, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) - - Eventually(Object(configuredResource), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) - art := &artifactv1.Artifact{} - art.Name = configuredResource.Status.ArtifactRef.Name - art.Namespace = configuredResource.Namespace - - test.VerifyArtifact(strg, art, map[string]func(data []byte){ - fileToConfigure: func(data []byte) { - Expect(data).To(MatchYAML(fileContentAfterConfiguration)) - }, - }) + Eventually(func(ctx context.Context) error { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(configuredResource), configuredResource) + if err != nil { + return err + } + if !conditions.IsReady(configuredResource) { + return fmt.Errorf("resource not ready") + } + if configuredResource.GetOCIArtifact() == nil { + return fmt.Errorf("OCI artifact not present") + } + return nil + }, "15s").WithContext(ctx).Should(Succeed()) + + ociRepository, err := registry.NewRepository(ctx, configuredResource.GetOCIRepository()) + Expect(err).NotTo(HaveOccurred()) + resourceContent, err := ociRepository.FetchArtifact(ctx, configuredResource.GetManifestDigest()) + Expect(err).NotTo(HaveOccurred()) + dataExtracted, err := compression.ExtractDataFromTGZ(resourceContent) + Expect(err).NotTo(HaveOccurred()) + Expect(dataExtracted).To(MatchYAML(fileContentAfterConfiguration)) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, configuredResource)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(configuredResource), configuredResource) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, &cfg)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(&cfg), &cfg) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, targetResource)).To(Succeed()) + + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) }) - }) -func NoOpComponent(ctx context.Context, basePath string) *v1alpha1.Component { +func NoOpComponent(ctx context.Context) *v1alpha1.Component { component := test.SetupComponentWithDescriptorList(ctx, "any-component-that-should-not-be-introspected", Namespace, - nil, + []byte("noop"), &test.MockComponentOptions{ - BasePath: basePath, - Strg: strg, + Registry: registry, Client: k8sClient, Recorder: recorder, Info: v1alpha1.ComponentInfo{ @@ -168,8 +204,6 @@ func NoOpComponent(ctx context.Context, basePath string) *v1alpha1.Component { Repository: "repo-that-should-not-be-introspected", }, ) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) + return component } diff --git a/internal/controller/configuration/configure.go b/internal/controller/configuration/configure.go index 8d910121..e4febe66 100644 --- a/internal/controller/configuration/configure.go +++ b/internal/controller/configuration/configure.go @@ -18,7 +18,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/substitute" "github.com/open-component-model/ocm-k8s-toolkit/pkg/substitute/steps" "github.com/open-component-model/ocm-k8s-toolkit/pkg/util" @@ -55,16 +55,16 @@ func Configure(ctx context.Context, ) (string, error) { logger := log.FromContext(ctx) targetDir := filepath.Join(basePath, "target") - if err := target.UnpackIntoDirectory(targetDir); errors.Is(err, artifact.ErrAlreadyUnpacked) { + if err := target.UnpackIntoDirectory(targetDir); errors.Is(err, ociartifact.ErrAlreadyUnpacked) { logger.Info("target was already present, reusing existing directory", "path", targetDir) } else if err != nil { return "", fmt.Errorf("failed to get target directory: %w", err) } - // TODO Workaround because the tarball from artifact storer uses a folder - // named after the util name instead of storing at artifact root level as this is the expected format - // for helm tgz archives. - // See issue: https://github.com/helm/helm/issues/5552 + // TODO: Workaround because the tarball from artifact storer uses a folder + // named after the util name instead of storing at artifact root level as this is the expected format + // for helm tgz archives. + // See issue: https://github.com/helm/helm/issues/5552 useSubDir, subDir, err := util.IsHelmChart(targetDir) if err != nil { return "", fmt.Errorf("failed to determine if target is a helm chart to traverse into subdirectory: %w", err) diff --git a/internal/controller/configuration/suite_test.go b/internal/controller/configuration/suite_test.go index 04f11f25..9b133cb9 100644 --- a/internal/controller/configuration/suite_test.go +++ b/internal/controller/configuration/suite_test.go @@ -16,36 +16,32 @@ package configuration import ( "context" "fmt" - "io" - "net/http" + "os" + "os/exec" "path/filepath" "runtime" "testing" - "time" + . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/openfluxcd/controller-manager/server" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/yaml" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" metricserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" cfgclient "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/configuration/client" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // +kubebuilder:scaffold:imports @@ -53,16 +49,14 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -const ( - ARTIFACT_SERVER = "localhost:0" -) - var cfg *rest.Config var k8sClient client.Client var k8sManager ctrl.Manager var testEnv *envtest.Environment -var strg *storage.Storage var recorder record.EventRecorder +var zotCmd *exec.Cmd +var registry *ociartifact.Registry +var zotRootDir string func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -75,26 +69,10 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") - // Get external artifact CRD - resp, err := http.Get(v1alpha1.ArtifactCrd) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() error { - return resp.Body.Close() - }) - - crdByte, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - - artifactCRD := &apiextensionsv1.CustomResourceDefinition{} - err = yaml.Unmarshal(crdByte, artifactCRD) - Expect(err).NotTo(HaveOccurred()) - testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, - CRDs: []*apiextensionsv1.CustomResourceDefinition{artifactCRD}, - // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the // default path defined in controller-runtime which is /usr/local/kubebuilder/. @@ -104,6 +82,8 @@ var _ = BeforeSuite(func() { fmt.Sprintf("1.30.0-%s-%s", runtime.GOOS, runtime.GOARCH)), } + var err error + // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) @@ -111,7 +91,6 @@ var _ = BeforeSuite(func() { DeferCleanup(testEnv.Stop) Expect(v1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) - Expect(artifactv1.AddToScheme(scheme.Scheme)).Should(Succeed()) Expect(err).NotTo(HaveOccurred()) @@ -130,37 +109,38 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - tmpdir := GinkgoT().TempDir() - Expect(err).ToNot(HaveOccurred()) - address := ARTIFACT_SERVER - strg, err = server.NewStorage(k8sClient, testEnv.Scheme, tmpdir, address, 0, 0) - Expect(err).ToNot(HaveOccurred()) - artifactServer, err := server.NewArtifactServer(tmpdir, address, time.Millisecond) - Expect(err).ToNot(HaveOccurred()) - recorder = &record.FakeRecorder{ Events: make(chan string, 32), IncludeObject: true, } + // Setup zot registry and start it up + zotRootDir = Must(os.MkdirTemp("", "")) + DeferCleanup(func() { + Expect(os.RemoveAll(zotRootDir)).To(Succeed()) + }) + + zotCmd, registry = test.SetupRegistry(filepath.Join("..", "..", "..", "bin", "zot-registry"), zotRootDir, "0.0.0.0", "8083") + Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ Client: k8sClient, Scheme: testEnv.Scheme, EventRecorder: recorder, }, - ConfigClient: cfgclient.NewClientWithLocalStorage(k8sClient, strg, scheme.Scheme), - Storage: strg, + ConfigClient: cfgclient.NewClientWithRegistry(k8sClient, registry, scheme.Scheme), + Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) ctx, cancel := context.WithCancel(context.Background()) DeferCleanup(cancel) - go func() { - defer GinkgoRecover() - Expect(artifactServer.Start(ctx)).To(Succeed()) - }() go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) }() }) + +var _ = AfterSuite(func(ctx context.Context) { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") +}) diff --git a/internal/controller/configuration/types/configuration_reference.go b/internal/controller/configuration/types/configuration_reference.go index d6d46aab..f0222e94 100644 --- a/internal/controller/configuration/types/configuration_reference.go +++ b/internal/controller/configuration/types/configuration_reference.go @@ -1,6 +1,8 @@ package types -import "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" +import ( + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" +) // ConfigurationReference can be used both as a source (ConfigurationSource), // and as a target (ConfigurationTarget) for configuration. @@ -10,12 +12,12 @@ type ConfigurationReference interface { } // ConfigurationSource is a source of localization. -// It contains instructions on how to localize an artifact.Content. +// It contains instructions on how to localize an ociartifact.Content. type ConfigurationSource interface { - artifact.Content + ociartifact.Content } // ConfigurationTarget is a target for configuration. type ConfigurationTarget interface { - artifact.Content + ociartifact.Content } diff --git a/internal/controller/localization/client/client.go b/internal/controller/localization/client/client.go index 40f53fdf..3a9461a2 100644 --- a/internal/controller/localization/client/client.go +++ b/internal/controller/localization/client/client.go @@ -4,14 +4,13 @@ import ( "context" "fmt" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/localization/types" - artifactutil "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" ) type Client interface { @@ -27,24 +26,24 @@ type Client interface { GetLocalizationConfig(ctx context.Context, ref v1alpha1.ConfigurationReference) (source types.LocalizationConfig, err error) } -func NewClientWithLocalStorage(r client.Reader, s *storage.Storage, scheme *runtime.Scheme) Client { +func NewClientWithRegistry(c client.Client, registry *ociartifact.Registry, scheme *runtime.Scheme) Client { factory := serializer.NewCodecFactory(scheme) info, _ := runtime.SerializerInfoForMediaType(factory.SupportedMediaTypes(), runtime.ContentTypeYAML) encoder := factory.EncoderForVersion(info.Serializer, v1alpha1.GroupVersion) return &localStorageBackedClient{ - Reader: r, - Storage: s, - scheme: scheme, - encoder: encoder, + Client: c, + Registry: registry, + scheme: scheme, + encoder: encoder, } } type localStorageBackedClient struct { - client.Reader - *storage.Storage - scheme *runtime.Scheme - encoder runtime.Encoder + client.Client + Registry *ociartifact.Registry + scheme *runtime.Scheme + encoder runtime.Encoder } func (clnt *localStorageBackedClient) Scheme() *runtime.Scheme { @@ -63,7 +62,7 @@ func (clnt *localStorageBackedClient) GetLocalizationTarget( case v1alpha1.KindLocalizedResource: fallthrough case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return ociartifact.GetContentBackedByArtifactFromComponent(ctx, clnt.Client, clnt.Registry, &ref) default: return nil, fmt.Errorf("unsupported localization target kind: %s", ref.Kind) } @@ -75,9 +74,9 @@ func (clnt *localStorageBackedClient) GetLocalizationConfig( ) (types.LocalizationConfig, error) { switch ref.Kind { case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return ociartifact.GetContentBackedByArtifactFromComponent(ctx, clnt.Client, clnt.Registry, &ref) case v1alpha1.KindLocalizationConfig: - return GetLocalizationConfigFromKubernetes(ctx, clnt.Reader, clnt.encoder, ref) + return GetLocalizationConfigFromKubernetes(ctx, clnt.Client, clnt.encoder, ref) default: return nil, fmt.Errorf("unsupported localization config kind: %s", ref.Kind) } @@ -99,5 +98,5 @@ func GetLocalizationConfigFromKubernetes(ctx context.Context, clnt client.Reader return nil, fmt.Errorf("failed to fetch localization config %s: %w", reference.Name, err) } - return &artifactutil.ObjectConfig{Object: &cfg, Encoder: encoder}, nil + return &ociartifact.ObjectConfig{Object: &cfg, Encoder: encoder}, nil } diff --git a/internal/controller/localization/localization_controller.go b/internal/controller/localization/localization_controller.go index 1cd8c4f0..9c2aa5f8 100644 --- a/internal/controller/localization/localization_controller.go +++ b/internal/controller/localization/localization_controller.go @@ -1,6 +1,7 @@ package localization import ( + "bytes" "context" "errors" "fmt" @@ -9,12 +10,11 @@ import ( "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/patch" "github.com/google/go-containerregistry/pkg/name" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/yaml" "ocm.software/ocm/api/ocm/compdesc" "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob" - "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" "ocm.software/ocm/api/ocm/extensions/accessmethods/ociblob" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -23,17 +23,17 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ocmctx "ocm.software/ocm/api/ocm" ocmmetav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + ocmociartifact "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" ctrl "sigs.k8s.io/controller-runtime" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" localizationclient "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/localization/client" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/localization/types" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/index" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" "github.com/open-component-model/ocm-k8s-toolkit/pkg/util" @@ -42,8 +42,8 @@ import ( // Reconciler reconciles a LocalizationRules object. type Reconciler struct { *ocm.BaseReconciler - *storage.Storage LocalizationClient localizationclient.Client + Registry ociartifact.RegistryType } var _ ocm.Reconciler = (*Reconciler)(nil) @@ -84,6 +84,8 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, err error) { + logger := log.FromContext(ctx) + localization := &v1alpha1.LocalizedResource{} if err := r.Get(ctx, req.NamespacedName, localization); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -93,25 +95,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re return ctrl.Result{}, nil } - if !localization.GetDeletionTimestamp().IsZero() { - // TODO: This is a temporary solution until a artifact-reconciler is written to handle the deletion of artifacts - if err := ocm.RemoveArtifactForCollectable(ctx, r.Client, r.Storage, localization); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to remove artifact: %w", err) - } - - if removed := controllerutil.RemoveFinalizer(localization, v1alpha1.ArtifactFinalizer); removed { - if err := r.Update(ctx, localization); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err) - } - } - - return ctrl.Result{}, nil - } - - if added := controllerutil.AddFinalizer(localization, v1alpha1.ArtifactFinalizer); added { - if err := r.Update(ctx, localization); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) - } + if localization.GetDeletionTimestamp() != nil { + logger.Info("localization is being deleted and cannot be used", "name", localization.Name) return ctrl.Result{Requeue: true}, nil } @@ -137,10 +122,6 @@ func (r *Reconciler) reconcileWithStatusUpdate(ctx context.Context, localization func (r *Reconciler) reconcileExists(ctx context.Context, localization *v1alpha1.LocalizedResource) (ctrl.Result, error) { logger := log.FromContext(ctx) - if err := r.Storage.ReconcileStorage(ctx, localization); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to reconcile storage: %w", err) - } - if localization.Spec.Target.Namespace == "" { localization.Spec.Target.Namespace = localization.Namespace } @@ -152,7 +133,7 @@ func (r *Reconciler) reconcileExists(ctx context.Context, localization *v1alpha1 return ctrl.Result{}, fmt.Errorf("failed to fetch target: %w", err) } - targetBackedByComponent, ok := target.(LocalizableArtifactContent) + targetBackedByComponent, ok := target.(LocalizableContent) if !ok { err = fmt.Errorf("target is not backed by a component and cannot be localized") status.MarkNotReady(r.EventRecorder, localization, v1alpha1.TargetFetchFailedReason, err.Error()) @@ -171,7 +152,7 @@ func (r *Reconciler) reconcileExists(ctx context.Context, localization *v1alpha1 return ctrl.Result{}, fmt.Errorf("failed to fetch config: %w", err) } - rules, err := localizeRules(ctx, r.Client, r.Storage, targetBackedByComponent, cfg) + rules, err := localizeRules(ctx, r.Client, r.Registry, targetBackedByComponent, cfg) if err != nil { status.MarkNotReady(r.EventRecorder, localization, v1alpha1.LocalizationRuleGenerationFailedReason, err.Error()) @@ -251,38 +232,7 @@ func (r *Reconciler) reconcileExists(ctx context.Context, localization *v1alpha1 return ctrl.Result{}, fmt.Errorf("configured resource containing localization is not yet ready") } - art := &artifactv1.Artifact{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: configuredResource.GetNamespace(), - Name: configuredResource.Status.ArtifactRef.Name, - }, art); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to fetch artifact: %w", err) - } - - artOp, err := controllerutil.CreateOrUpdate(ctx, r.Client, art, func() error { - if err := controllerutil.SetOwnerReference(localization, art, r.Scheme); err != nil { - return fmt.Errorf("failed to set indirect owner reference on artifact: %w", err) - } - - if art.GetAnnotations() == nil { - art.SetAnnotations(map[string]string{}) - } - a := art.GetAnnotations() - a["ocm.software/artifact-purpose"] = "localization" - a["ocm.software/localization"] = fmt.Sprintf("%s/%s", localization.GetNamespace(), localization.GetName()) - art.SetAnnotations(a) - - return nil - }) - if err != nil { - status.MarkNotReady(r.EventRecorder, localization, v1alpha1.ReconcileArtifactFailedReason, err.Error()) - - return ctrl.Result{}, fmt.Errorf("failed to create or update artifact: %w", err) - } - logger.V(1).Info(fmt.Sprintf("artifact %s", artOp)) - - localization.Status.ArtifactRef = configuredResource.Status.ArtifactRef - localization.Status.Digest = configuredResource.Status.Digest + localization.Status.OCIArtifact = configuredResource.GetOCIArtifact() localization.Status.ConfiguredResourceRef = &v1alpha1.ObjectKey{ Name: configuredResource.GetName(), Namespace: configuredResource.GetNamespace(), @@ -296,8 +246,8 @@ func (r *Reconciler) reconcileExists(ctx context.Context, localization *v1alpha1 func localizeRules( ctx context.Context, c client.Client, - s *storage.Storage, - content LocalizableArtifactContent, + r ociartifact.RegistryType, + content LocalizableContent, cfg types.LocalizationConfig, ) ( []v1alpha1.ConfigurationRule, @@ -308,7 +258,7 @@ func localizeRules( return nil, fmt.Errorf("failed to parse localization config: %w", err) } - componentSet, componentDescriptor, err := ComponentDescriptorAndSetFromResource(ctx, c, s, content.GetComponent()) + componentSet, componentDescriptor, err := ComponentDescriptorAndSetFromResource(ctx, r, content.GetComponent()) if err != nil { return nil, fmt.Errorf("failed to get content descriptor and set: %w", err) } @@ -317,7 +267,7 @@ func localizeRules( localizedRules := make([]v1alpha1.ConfigurationRule, len(rules)) for i, rule := range rules { - // TODO Decide what the hell to do with GoTemplates + // TODO: Decide what the hell to do with GoTemplates if rule.GoTemplate != nil { localizedRules[i] = v1alpha1.ConfigurationRule{ GoTemplate: (*v1alpha1.ConfigurationRuleGoTemplate)(rule.GoTemplate), @@ -355,28 +305,40 @@ func localizeRules( return localizedRules, nil } -// LocalizableArtifactContent is an artifact content that is backed by a component and resource, allowing it +// LocalizableContent is an artifact content that is backed by a component and resource, allowing it // to be localized (by resolving relative references from the resource & component into absolute values). -type LocalizableArtifactContent interface { - artifact.Content +type LocalizableContent interface { + ociartifact.Content GetComponent() *v1alpha1.Component GetResource() *v1alpha1.Resource } func ComponentDescriptorAndSetFromResource( ctx context.Context, - clnt client.Reader, - strg *storage.Storage, + registry ociartifact.RegistryType, baseComponent *v1alpha1.Component, ) (compdesc.ComponentVersionResolver, *compdesc.ComponentDescriptor, error) { - art, err := util.GetNamespaced[artifactv1.Artifact](ctx, clnt, baseComponent.Status.ArtifactRef, baseComponent.Namespace) + ociArtifact := baseComponent.GetOCIArtifact() + if ociArtifact == nil { + return nil, nil, fmt.Errorf("component %s misses OCI artifact", baseComponent.GetName()) + } + + repository, err := registry.NewRepository(ctx, ociArtifact.Repository) if err != nil { - return nil, nil, fmt.Errorf("failed to Get artifact: %w", err) + return nil, nil, fmt.Errorf("failed to create repository: %w", err) } - componentSet, err := ocm.GetComponentSetForArtifact(strg, art) + + // Get component descriptor set from artifact + data, err := repository.FetchArtifact(ctx, baseComponent.GetManifestDigest()) if err != nil { - return nil, nil, fmt.Errorf("failed to Get component version set: %w", err) + return nil, nil, fmt.Errorf("failed to fetch artifact: %w", err) + } + cds := &ocm.Descriptors{} + if err := yaml.NewYAMLToJSONDecoder(bytes.NewReader(data)).Decode(cds); err != nil { + return nil, nil, fmt.Errorf("failed to decode yaml to json: %w", err) } + componentSet := compdesc.NewComponentVersionSet(cds.List...) + componentDescriptor, err := componentSet.LookupComponentVersion(baseComponent.Spec.Component, baseComponent.Status.Component.Version) if err != nil { return nil, nil, fmt.Errorf("failed to lookup component version: %w", err) @@ -414,7 +376,7 @@ func resolve( // TODO this seems hacky but I copy & pasted, we need to find a better way for ref == "" && refErr == nil { switch x := specInCtx.(type) { - case *ociartifact.AccessSpec: + case *ocmociartifact.AccessSpec: ref = x.ImageReference case *ociblob.AccessSpec: ref = fmt.Sprintf("%s@%s", x.Reference, x.Digest) diff --git a/internal/controller/localization/localization_controller_test.go b/internal/controller/localization/localization_controller_test.go index baa69bd8..59453ebc 100644 --- a/internal/controller/localization/localization_controller_test.go +++ b/internal/controller/localization/localization_controller_test.go @@ -3,30 +3,25 @@ package localization import ( "bytes" "context" - "os" "path/filepath" "text/template" _ "embed" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - + "github.com/fluxcd/pkg/runtime/conditions" "github.com/mandelsoft/vfs/pkg/memoryfs" "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/projectionfs" "github.com/mandelsoft/vfs/pkg/vfs" - "k8s.io/apimachinery/pkg/runtime/serializer" - "ocm.software/ocm/api/utils/tarutils" - "sigs.k8s.io/controller-runtime/pkg/client" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - corev1 "k8s.io/api/core/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/serializer" ocmbuilder "ocm.software/ocm/api/helper/builder" environment "ocm.software/ocm/api/helper/env" + "ocm.software/ocm/api/utils/tarutils" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" @@ -65,30 +60,19 @@ var _ = Describe("Localization Controller", func() { BeforeEach(func() { tmp = GinkgoT().TempDir() - testfs, err := projectionfs.New(osfs.New(), tmp) + testFs, err := projectionfs.New(osfs.New(), tmp) Expect(err).ToNot(HaveOccurred()) - env = ocmbuilder.NewBuilder(environment.FileSystem(testfs)) + env = ocmbuilder.NewBuilder(environment.FileSystem(testFs)) DeferCleanup(env.Cleanup) }) - BeforeEach(func(ctx SpecContext) { - By("creating namespace object") - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: Namespace, - }, - } - Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) - }) - - It("should localize an artifact from a resource based on a config supplied in a sibling resource", func(ctx SpecContext) { + It("should localize an OCI artifact from a resource based on a config supplied in a sibling resource", func(ctx SpecContext) { component := test.SetupComponentWithDescriptorList(ctx, ComponentObj, Namespace, descriptorListYAML, &test.MockComponentOptions{ - BasePath: tmp, - Strg: strg, + Registry: registry, Client: k8sClient, Recorder: recorder, Info: v1alpha1.ComponentInfo{ @@ -99,78 +83,62 @@ var _ = Describe("Localization Controller", func() { Repository: RepositoryObj, }, ) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) - var err error targetResource = test.SetupMockResourceWithData(ctx, TargetResourceObj, Namespace, &test.MockResourceOptions{ - BasePath: tmp, DataPath: filepath.Join("testdata", "deployment-instruction-helm"), ComponentRef: v1alpha1.ObjectKey{ Namespace: Namespace, Name: ComponentObj, }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, ) - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, targetResource, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) + cfgResource = test.SetupMockResourceWithData(ctx, CfgResourceObj, Namespace, &test.MockResourceOptions{ - BasePath: tmp, - Data: bytes.NewReader(configYAML), + Data: bytes.NewReader(configYAML), ComponentRef: v1alpha1.ObjectKey{ Namespace: Namespace, Name: ComponentObj, }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, ) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, cfgResource, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) - localization := SetupLocalizedResource(ctx, map[string]string{ + localization := setupLocalizedResource(ctx, map[string]string{ "Namespace": Namespace, "Name": Localization, "TargetResourceName": targetResource.Name, "ConfigResourceName": cfgResource.Name, }) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, localization, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) - Eventually(Object(localization), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + By("checking that the resource has been reconciled successfully") + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(localization), localization) + if err != nil { + return false + } + return conditions.IsReady(localization) + }, "15s").WithContext(ctx).Should(BeTrue()) - art := &artifactv1.Artifact{} - art.Name = localization.Status.ArtifactRef.Name - art.Namespace = localization.Namespace + Expect(localization.GetOCIArtifact()).ToNot(BeNil()) - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - - localized := strg.LocalPath(art) - Expect(localized).To(BeAnExistingFile()) + repository, err := registry.NewRepository(ctx, localization.GetOCIRepository()) + Expect(err).ToNot(HaveOccurred()) + data, err := repository.FetchArtifact(ctx, localization.GetManifestDigest()) + Expect(err).ToNot(HaveOccurred()) memFs := vfs.New(memoryfs.New()) - localizedArchiveData, err := os.OpenFile(localized, os.O_RDONLY, 0o600) - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(func() { - Expect(localizedArchiveData.Close()).To(Succeed()) - }) - Expect(tarutils.UnzipTarToFs(memFs, localizedArchiveData)).To(Succeed()) + Expect(tarutils.UnzipTarToFs(memFs, bytes.NewReader(data))).To(Succeed()) valuesData, err := memFs.ReadFile("values.yaml") Expect(err).ToNot(HaveOccurred()) @@ -180,17 +148,27 @@ var _ = Describe("Localization Controller", func() { Expect(err).ToNot(HaveOccurred()) Expect(deploymentData).To(BeEquivalentTo(replacedDeploymentYAML)) + By("delete resources manually") + Expect(k8sClient.Delete(ctx, localization)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(localization), localization) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, cfgResource)).To(Succeed()) + Expect(k8sClient.Delete(ctx, targetResource)).To(Succeed()) + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) }) }) -func SetupLocalizedResource(ctx context.Context, data map[string]string) *v1alpha1.LocalizedResource { +func setupLocalizedResource(ctx context.Context, data map[string]string) *v1alpha1.LocalizedResource { localizationTemplate, err := template.New("localization").Parse(localizationTemplateKustomizePatch) Expect(err).ToNot(HaveOccurred()) var ltpl bytes.Buffer Expect(localizationTemplate.ExecuteTemplate(<pl, "localization", data)).To(Succeed()) localization := &v1alpha1.LocalizedResource{} - serializer := serializer.NewCodecFactory(k8sClient.Scheme()).UniversalDeserializer() - _, _, err = serializer.Decode(ltpl.Bytes(), nil, localization) + serializerFactory := serializer.NewCodecFactory(k8sClient.Scheme()).UniversalDeserializer() + _, _, err = serializerFactory.Decode(ltpl.Bytes(), nil, localization) Expect(err).To(Not(HaveOccurred())) Expect(k8sClient.Create(ctx, localization)).To(Succeed()) return localization diff --git a/internal/controller/localization/suite_test.go b/internal/controller/localization/suite_test.go index 3e564fe9..1e71fdeb 100644 --- a/internal/controller/localization/suite_test.go +++ b/internal/controller/localization/suite_test.go @@ -16,38 +16,36 @@ package localization import ( "context" "fmt" - "io" - "net/http" + "os" + "os/exec" "path/filepath" "runtime" "testing" - "time" + . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/openfluxcd/controller-manager/server" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/yaml" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" metricserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/configuration" cfgclient "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/configuration/client" locclient "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/localization/client" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // +kubebuilder:scaffold:imports @@ -55,16 +53,16 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -const ( - ARTIFACT_SERVER = "localhost:0" -) - var cfg *rest.Config var k8sClient client.Client var k8sManager ctrl.Manager var testEnv *envtest.Environment -var strg *storage.Storage var recorder record.EventRecorder +var zotCmd *exec.Cmd +var registry *ociartifact.Registry +var zotRootDir string +var ctx context.Context +var cancel context.CancelFunc func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -77,26 +75,10 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") - // Get external artifact CRD - resp, err := http.Get(v1alpha1.ArtifactCrd) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() error { - return resp.Body.Close() - }) - - crdByte, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - - artifactCRD := &apiextensionsv1.CustomResourceDefinition{} - err = yaml.Unmarshal(crdByte, artifactCRD) - Expect(err).NotTo(HaveOccurred()) - testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, - CRDs: []*apiextensionsv1.CustomResourceDefinition{artifactCRD}, - // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the // default path defined in controller-runtime which is /usr/local/kubebuilder/. @@ -107,13 +89,12 @@ var _ = BeforeSuite(func() { } // cfg is defined in this file globally. - cfg, err = testEnv.Start() + cfg, err := testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) DeferCleanup(testEnv.Stop) Expect(v1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) - Expect(artifactv1.AddToScheme(scheme.Scheme)).Should(Succeed()) Expect(err).NotTo(HaveOccurred()) @@ -132,27 +113,40 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - tmpdir := GinkgoT().TempDir() - Expect(err).ToNot(HaveOccurred()) - address := ARTIFACT_SERVER - strg, err = server.NewStorage(k8sClient, testEnv.Scheme, tmpdir, address, 0, 0) - Expect(err).ToNot(HaveOccurred()) - artifactServer, err := server.NewArtifactServer(tmpdir, address, time.Millisecond) - Expect(err).ToNot(HaveOccurred()) - recorder = &record.FakeRecorder{ Events: make(chan string, 32), IncludeObject: true, } + // Setup zot registry and start it up + zotRootDir = Must(os.MkdirTemp("", "")) + DeferCleanup(func() { + Expect(os.RemoveAll(zotRootDir)).To(Succeed()) + }) + + zotCmd, registry = test.SetupRegistry(filepath.Join("..", "..", "..", "bin", "zot-registry"), zotRootDir, "0.0.0.0", "8082") + + ctx, cancel = context.WithCancel(context.Background()) + DeferCleanup(cancel) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: Namespace, + }, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, namespace, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + }) + Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ Client: k8sClient, Scheme: testEnv.Scheme, EventRecorder: recorder, }, - LocalizationClient: locclient.NewClientWithLocalStorage(k8sClient, strg, scheme.Scheme), - Storage: strg, + LocalizationClient: locclient.NewClientWithRegistry(k8sClient, registry, scheme.Scheme), + Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) Expect((&configuration.Reconciler{ @@ -161,18 +155,19 @@ var _ = BeforeSuite(func() { Scheme: testEnv.Scheme, EventRecorder: recorder, }, - ConfigClient: cfgclient.NewClientWithLocalStorage(k8sClient, strg, scheme.Scheme), - Storage: strg, + ConfigClient: cfgclient.NewClientWithRegistry(k8sClient, registry, scheme.Scheme), + Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) ctx, cancel := context.WithCancel(context.Background()) DeferCleanup(cancel) - go func() { - defer GinkgoRecover() - Expect(artifactServer.Start(ctx)).To(Succeed()) - }() go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) }() }) + +var _ = AfterSuite(func(ctx context.Context) { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") +}) diff --git a/internal/controller/localization/types/localization_reference.go b/internal/controller/localization/types/localization_reference.go index 40253ad3..997951d0 100644 --- a/internal/controller/localization/types/localization_reference.go +++ b/internal/controller/localization/types/localization_reference.go @@ -1,6 +1,8 @@ package types -import "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" +import ( + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" +) // LocalizationReference can be used both as a source (LocalizationConfig), // and as a target (LocalizationTarget) for localization. @@ -9,12 +11,12 @@ type LocalizationReference interface { LocalizationTarget } -// LocalizationConfig is a configuration on how to localize an artifact.Content. +// LocalizationConfig is a configuration on how to localize an ociartifact.Content. type LocalizationConfig interface { - artifact.Content + ociartifact.Content } -// LocalizationTarget is a target artifact.Content for localization. +// LocalizationTarget is a target ociartifact.Content for localization. type LocalizationTarget interface { - artifact.Content + ociartifact.Content } diff --git a/internal/controller/ocmrepository/controller.go b/internal/controller/ocmrepository/controller.go index 68aca8d3..85dba704 100644 --- a/internal/controller/ocmrepository/controller.go +++ b/internal/controller/ocmrepository/controller.go @@ -96,7 +96,7 @@ func (r *Reconciler) reconcileExists(ctx context.Context, ocmRepo *v1alpha1.OCMR } if ocmRepo.Spec.Suspend { - logger.Info("component is suspended, skipping reconciliation") + logger.Info("OCMRepository is suspended, skipping reconciliation") return ctrl.Result{}, nil } diff --git a/internal/controller/replication/controller.go b/internal/controller/replication/controller.go index 244a01da..98b38522 100644 --- a/internal/controller/replication/controller.go +++ b/internal/controller/replication/controller.go @@ -97,7 +97,7 @@ func (r *Reconciler) reconcileExists(ctx context.Context, replication *v1alpha1. if replication.GetDeletionTimestamp() != nil { logger.Info("replication is being deleted and cannot be used", "name", replication.Name) - return ctrl.Result{}, nil + return ctrl.Result{Requeue: true}, nil } if replication.Spec.Suspend { diff --git a/internal/controller/resource/resource_controller.go b/internal/controller/resource/resource_controller.go index 81b28216..923c6ecc 100644 --- a/internal/controller/resource/resource_controller.go +++ b/internal/controller/resource/resource_controller.go @@ -17,21 +17,19 @@ limitations under the License. package resource import ( + "bytes" "context" "encoding/json" "errors" "fmt" - "os" - "path/filepath" - "strings" "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/patch" - "github.com/openfluxcd/controller-manager/storage" + "github.com/opencontainers/go-digest" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/yaml" "ocm.software/ocm/api/datacontext" "ocm.software/ocm/api/ocm/compdesc" - "ocm.software/ocm/api/ocm/extensions/download" "ocm.software/ocm/api/ocm/resolvers" "ocm.software/ocm/api/ocm/selectors" "ocm.software/ocm/api/ocm/tools/signing" @@ -44,23 +42,21 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" ocmctx "ocm.software/ocm/api/ocm" v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" ) type Reconciler struct { *ocm.BaseReconciler - Storage *storage.Storage + Registry ociartifact.RegistryType } var _ ocm.Reconciler = (*Reconciler)(nil) @@ -81,8 +77,6 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Resource{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - // Watch for artifacts-events that are owned by the resource controller - Owns(&artifactv1.Artifact{}). // Watch for component-events that are referenced by resources Watches( &v1alpha1.Component{}, @@ -116,11 +110,6 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { // +kubebuilder:rbac:groups=delivery.ocm.software,resources=resources,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=delivery.ocm.software,resources=resources/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=delivery.ocm.software,resources=resources/finalizers,verbs=update - -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts/finalizers,verbs=update func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { resource := &v1alpha1.Resource{} @@ -152,24 +141,24 @@ func (r *Reconciler) reconcileExists(ctx context.Context, resource *v1alpha1.Res return ctrl.Result{}, nil } - if !resource.GetDeletionTimestamp().IsZero() { - // TODO: This is a temporary solution until a artifact-reconciler is written to handle the deletion of artifacts - if err := ocm.RemoveArtifactForCollectable(ctx, r.Client, r.Storage, resource); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to remove artifact: %w", err) + if resource.GetDeletionTimestamp() != nil { + if err := ociartifact.DeleteForObject(ctx, r.Registry, resource); err != nil { + return ctrl.Result{}, err } - if removed := controllerutil.RemoveFinalizer(resource, v1alpha1.ArtifactFinalizer); removed { + if updated := controllerutil.RemoveFinalizer(resource, v1alpha1.ArtifactFinalizer); updated { if err := r.Update(ctx, resource); err != nil { return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err) } } - return ctrl.Result{}, nil + logger.Info("resource is being deleted and cannot be used", "name", resource.Name) + + return ctrl.Result{Requeue: true}, nil } - if added := controllerutil.AddFinalizer(resource, v1alpha1.ArtifactFinalizer); added { - err := r.Update(ctx, resource) - if err != nil { + if updated := controllerutil.AddFinalizer(resource, v1alpha1.ArtifactFinalizer); updated { + if err := r.Update(ctx, resource); err != nil { return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) } @@ -223,6 +212,7 @@ func (r *Reconciler) reconcileOCM(ctx context.Context, resource *v1alpha1.Resour return result, nil } +//nolint:funlen,cyclop // we do not want to cut function at an arbitrary point func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, resource *v1alpha1.Resource, component *v1alpha1.Component) (ctrl.Result, error) { logger := log.FromContext(ctx) logger.V(1).Info("reconciling resource") @@ -237,6 +227,7 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, return ctrl.Result{}, err } + err = ocm.ConfigureContext(ctx, octx, r.GetClient(), configs) if err != nil { status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.ConfigureContextFailedReason, err.Error()) @@ -244,26 +235,31 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, return ctrl.Result{}, err } - // Get artifact from component that contains component descriptor - artifactComponent := &artifactv1.Artifact{} - if err := r.Get(ctx, types.NamespacedName{ - // TODO: see https://github.com/open-component-model/ocm-project/issues/295 - Namespace: resource.GetNamespace(), - Name: component.Status.ArtifactRef.Name, - }, artifactComponent); err != nil { - status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetArtifactFailedReason, "Cannot get component artifact") + // Create repository to download the component descriptors + repositoryCD, err := r.Registry.NewRepository(ctx, component.GetOCIRepository()) + if err != nil { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.CreateOCIRepositoryFailedReason, err.Error()) - return ctrl.Result{}, fmt.Errorf("failed to get component artifact: %w", err) + return ctrl.Result{}, err } // Get component descriptor set from artifact - cdSet, err := ocm.GetComponentSetForArtifact(r.Storage, artifactComponent) + data, err := repositoryCD.FetchArtifact(ctx, component.GetManifestDigest()) if err != nil { - status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetComponentForArtifactFailedReason, err.Error()) + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.FetchOCIArtifactFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + cds := &ocm.Descriptors{} + if err := yaml.NewYAMLToJSONDecoder(bytes.NewReader(data)).Decode(cds); err != nil { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.YamlToJSONDecodeFailedReason, err.Error()) return ctrl.Result{}, err } + cdSet := compdesc.NewComponentVersionSet(cds.List...) + // Get referenced component descriptor from component descriptor set cd, err := cdSet.LookupComponentVersion(component.Status.Component.Component, component.Status.Component.Version) if err != nil { @@ -300,30 +296,116 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, return ctrl.Result{}, err } - // revision is the digest of the resource. It is used to identify the resource in the storage (as filename) and to - // check if the resource is already present in the storage. - revision := resourceAccess.Meta().Digest.Value + if err := verifyResource(ctx, resourceAccess, cv, cd); err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.VerifyResourceFailedReason, err.Error()) - // Get the artifact to check if it is already present while reconciling it - artifactStorage := r.Storage.NewArtifactFor(resource.GetKind(), resource.GetObjectMeta(), "", "") - if err := r.Client.Get(ctx, types.NamespacedName{Name: artifactStorage.Name, Namespace: artifactStorage.Namespace}, artifactStorage); err != nil { - if !apierrors.IsNotFound(err) { - status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetArtifactFailedReason, err.Error()) + return ctrl.Result{}, err + } - return ctrl.Result{}, fmt.Errorf("failed to get artifactStorage: %w", err) - } + // TODO: + // Problem: The current implementation would download the resource every reconcile-loop. This could be + // expensive depending on the resource. + // Additionally, there could be two different components that contain the same resource. Also in this case + // we would re-download the resource, even though it is already in the OCI registry. + // To circumvent this problem, we could use the digest provided by the resource-access (prior to the resource + // download) as the repository-name. Then, we could check if such a repository for the resource already + // exists. + // If so, we cannot not return immediately because we need to create a manifest-file to point to the already + // present resource-layer. Otherwise the GC would delete the resource-layer if the previously present manifest + // would be deleted. + + // The digest from the resource access is used, so it can be used to compare resource with the same name/identity + // on a digest-level. + repositoryResourceName := resourceAccess.Meta().Digest.Value + repositoryResource, err := r.Registry.NewRepository(ctx, repositoryResourceName) + if err != nil { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.CreateOCIRepositoryFailedReason, err.Error()) + + return ctrl.Result{}, err } - err = reconcileArtifact(ctx, octx, r.Storage, resource, resourceAccess, revision, artifactStorage, func() error { return verifyResource(ctx, resourceAccess, cv, cd) }) + var ( + manifestDigest digest.Digest + blobSize int64 + ) + + // If the resource is of type 'ociArtifact' or its access type is 'ociArtifact', the resource will be copied to the + // internal OCI registry + logger.Info("create OCI artifact for resource", "name", resource.GetName(), "type", resourceAccess.Meta().GetType()) + resourceAccessSpec, err := resourceAccess.Access() if err != nil { - status.MarkNotReady(r.EventRecorder, resource, v1alpha1.ReconcileArtifactFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetResourceAccessFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + if resourceAccessSpec == nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetResourceAccessFailedReason, "access spec is nil") return ctrl.Result{}, err } + tag := resourceAccess.Meta().GetVersion() + + if resourceAccessSpec.GetType() == "ociArtifact" { + manifestDigest, err = repositoryResource.CopyOCIArtifactForResourceAccess(ctx, resourceAccess) + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.CopyOCIArtifactFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + // TODO: How to get the blob size, without downloading the resource? + // Do we need the blob-size, when we copy the resource either way? + // We could use the size stored in the manifest. + blobSize = 0 + } else { + // Get resource content + // No need to close the blob access as it will be closed automatically + blobAccess, err := getBlobAccess(ctx, resourceAccess) + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetBlobAccessFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to get blob access: %w", err) + } + + resourceContent, err := blobAccess.Get() + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetResourceFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to get blob: %w", err) + } + + // Compress content to gzip if it is not already compressed to avoid to large blobs + resourceContentCompressed, err := compression.AutoCompressAsGzip(ctx, resourceContent) + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.CompressGzipFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to auto compress data: %w", err) + } + + tag = ocm.NormalizeVersion(tag) + manifestDigest, err = repositoryResource.PushArtifact(ctx, tag, resourceContentCompressed) + if err != nil { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.PushOCIArtifactFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to push OCI artifact: %w", err) + } + + blobSize = int64(len(resourceContentCompressed)) + } + // Update status - if err = setResourceStatus(ctx, configs, resource, resourceAccess); err != nil { - status.MarkNotReady(r.EventRecorder, component, v1alpha1.StatusSetFailedReason, err.Error()) + if err = setResourceStatus(ctx, configs, resource, resourceAccess, &v1alpha1.OCIArtifactInfo{ + Repository: repositoryResourceName, + Digest: manifestDigest.String(), + Blob: &v1alpha1.BlobInfo{ + Digest: resourceAccess.Meta().Digest.Value, + Tag: tag, + Size: blobSize, + }, + }); err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.StatusSetFailedReason, err.Error()) return ctrl.Result{}, fmt.Errorf("failed to set resource status: %w", err) } @@ -437,125 +519,8 @@ func verifyResource(ctx context.Context, access ocmctx.ResourceAccess, cv ocmctx return nil } -// downloadResource downloads the resource from the resource access. -func downloadResource(ctx context.Context, octx ocmctx.Context, targetDir string, resource *v1alpha1.Resource, acc ocmctx.ResourceAccess, bAcc blobaccess.BlobAccess, -) (string, error) { - log.FromContext(ctx).V(1).Info("download resource") - - // Using a redirected resource acc to prevent redundant download - accessMock, err := ocm.NewRedirectedResourceAccess(acc, bAcc) - if err != nil { - return "", fmt.Errorf("failed to create redirected resource acc: %w", err) - } - - path, err := download.DownloadResource(octx, accessMock, filepath.Join(targetDir, resource.Name)) - if err != nil { - return "", fmt.Errorf("failed to download resource: %w", err) - } - - return path, nil -} - -// reconcileArtifact will download, verify, and reconcile the artifact in the storage if it is not already present in the storage. -// TODO: https://github.com/open-component-model/ocm-project/issues/297 -func reconcileArtifact( - ctx context.Context, - octx ocmctx.Context, - storage *storage.Storage, - resource *v1alpha1.Resource, - acc ocmctx.ResourceAccess, - revision string, - artifact *artifactv1.Artifact, - verifyFunc func() error, -) (retErr error) { - log.FromContext(ctx).V(1).Info("reconcile artifact") - - // Check if the artifact is already present and located in the storage - localPath := storage.LocalPath(artifact) - - // use the filename which is the revision as the artifact name - artifactPresent := storage.ArtifactExist(artifact) && strings.Split(filepath.Base(localPath), ".")[0] == revision - - // Init variables with default values in case the artifact is present - // If the artifact is present, the dirPath will be the directory of the local path to the directory - dirPath := filepath.Dir(localPath) - // If the artifact is already present, we do not want to archive it again - archiveFunc := func(_ *artifactv1.Artifact, _ string) error { - return nil - } - - // If the artifact is not present, we will verify and download the resource and provide it as artifact - //nolint:nestif // this is our main logic and we rather keep it in here - if !artifactPresent { - // No need to close the blob access as it will be closed automatically - bAcc, err := getBlobAccess(ctx, acc) - if err != nil { - return err - } - - // Check if resource can be verified - if err := verifyFunc(); err != nil { - return err - } - - // Target directory in which the resource is downloaded - tmp, err := os.MkdirTemp("", "resource-*") - if err != nil { - return fmt.Errorf("failed to create temporary directory: %w", err) - } - defer func() { - retErr = errors.Join(retErr, os.RemoveAll(tmp)) - }() - - path, err := downloadResource(ctx, octx, tmp, resource, acc, bAcc) - if err != nil { - return err - } - - // Since the artifact is not already present, an archive function is added to archive the downloaded resource in the storage - archiveFunc = func(art *artifactv1.Artifact, _ string) error { - logger := log.FromContext(ctx).WithValues("artifact", art.Name, "revision", revision, "path", path) - fi, err := os.Stat(path) - if err != nil { - return fmt.Errorf("failed to get file info: %w", err) - } - if fi.IsDir() { - logger.V(1).Info("archiving directory") - // Archive directory to storage - if err := storage.Archive(art, path, nil); err != nil { - return fmt.Errorf("failed to archive: %w", err) - } - } else { - if err := compression.AutoCompressAsGzipAndArchiveFile(ctx, art, storage, path); err != nil { - return fmt.Errorf("failed to auto compress and archive file: %w", err) - } - } - - resource.Status.ArtifactRef = corev1.LocalObjectReference{ - Name: art.Name, - } - - return nil - } - - // Overwrite the default dirPath with the temporary directory path that points to the downloaded resource - dirPath = tmp - } - - if err := storage.ReconcileStorage(ctx, resource); err != nil { - return fmt.Errorf("failed to reconcile resource storage: %w", err) - } - - // Provide artifact in storage - if err := storage.ReconcileArtifact(ctx, resource, revision, dirPath, revision, archiveFunc); err != nil { - return fmt.Errorf("failed to reconcile resource artifact: %w", err) - } - - return nil -} - // setResourceStatus updates the resource status with the all required information. -func setResourceStatus(ctx context.Context, configs []v1alpha1.OCMConfiguration, resource *v1alpha1.Resource, resourceAccess ocmctx.ResourceAccess) error { +func setResourceStatus(ctx context.Context, configs []v1alpha1.OCMConfiguration, resource *v1alpha1.Resource, resourceAccess ocmctx.ResourceAccess, ociArtifact *v1alpha1.OCIArtifactInfo) error { log.FromContext(ctx).V(1).Info("updating resource status") // Get the access spec from the resource access @@ -580,5 +545,7 @@ func setResourceStatus(ctx context.Context, configs []v1alpha1.OCMConfiguration, resource.Status.EffectiveOCMConfig = configs + resource.Status.OCIArtifact = ociArtifact + return nil } diff --git a/internal/controller/resource/resource_controller_test.go b/internal/controller/resource/resource_controller_test.go index c93e7ba0..9bc3ea4d 100644 --- a/internal/controller/resource/resource_controller_test.go +++ b/internal/controller/resource/resource_controller_test.go @@ -17,226 +17,469 @@ limitations under the License. package resource import ( + "bytes" + "compress/gzip" "context" + _ "embed" "fmt" "io" - "net/http" "os" - "time" + "github.com/fluxcd/pkg/runtime/conditions" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" . "ocm.software/ocm/api/helper/builder" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + "ocm.software/ocm/api/utils/mime" + "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "github.com/containers/image/v5/pkg/compression" - "github.com/fluxcd/pkg/runtime/conditions" - "github.com/mandelsoft/filepath/pkg/filepath" "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" "ocm.software/ocm/api/ocm/extensions/artifacttypes" "ocm.software/ocm/api/ocm/extensions/repositories/ctf" "ocm.software/ocm/api/utils/accessio" - "ocm.software/ocm/api/utils/accessobj" - "ocm.software/ocm/api/utils/mime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + + "ocm.software/ocm/api/utils/accessobj" "sigs.k8s.io/yaml" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" environment "ocm.software/ocm/api/helper/env" v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" + ocmPkg "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) +var () + const ( CTFPath = "ocm-k8s-ctfstore--*" - Namespace = "test-namespace" RepositoryObj = "test-repository" Component = "ocm.software/test-component" - ComponentObj = "test-component" ComponentVersion = "1.0.0" ResourceObj = "test-resource" ResourceVersion = "1.0.0" - ResourceContent = "resource content" + ResourceContent = "some important content" ) var _ = Describe("Resource Controller", func() { var ( - ctx context.Context - cancel context.CancelFunc - env *Builder - ctfPath string + env *Builder + resourceLocalPath string ) - BeforeEach(func() { - ctfPath = Must(os.MkdirTemp("", CTFPath)) - DeferCleanup(func() error { - return os.RemoveAll(ctfPath) - }) - - env = NewBuilder(environment.FileSystem(osfs.OsFs)) - DeferCleanup(env.Cleanup) - - ctx, cancel = context.WithCancel(context.Background()) - DeferCleanup(cancel) - }) Context("resource controller", func() { - It("can reconcile a resource", func() { - By("creating namespace object") - namespace := &corev1.Namespace{ + var componentName string + var componentObj *v1alpha1.Component + var namespace *corev1.Namespace + + BeforeEach(func(ctx SpecContext) { + namespaceName := test.GenerateNamespace(ctx.SpecReport().LeafNodeText) + namespace = &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: Namespace, + Name: namespaceName, }, } Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) - By("preparing a mock component") - prepareComponent(ctx, env, ctfPath) + componentName = test.GenerateComponentName(ctx.SpecReport().LeafNodeText) - By("creating a resource object") - resource := &v1alpha1.Resource{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: ResourceObj, - }, - Spec: v1alpha1.ResourceSpec{ - ComponentRef: corev1.LocalObjectReference{ - Name: ComponentObj, + resourceLocalPath = Must(os.MkdirTemp("", CTFPath)) + DeferCleanup(func() error { + return os.RemoveAll(resourceLocalPath) + }) + + env = NewBuilder(environment.FileSystem(osfs.OsFs)) + DeferCleanup(env.Cleanup) + }) + + AfterEach(func(ctx SpecContext) { + By("deleting the component") + Expect(k8sClient.Delete(ctx, componentObj)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(componentObj), componentObj) + return errors.IsNotFound(err) + }).WithContext(ctx).Should(BeTrue()) + + resources := &v1alpha1.ResourceList{} + Expect(k8sClient.List(ctx, resources, client.InNamespace(namespace.GetName()))).To(Succeed()) + Expect(resources.Items).To(HaveLen(0)) + + // TODO: test if OCI artifact was deleted + }) + + Context("resource controller", func() { + It("can reconcile a plaintext resource", func() { + resourceType := artifacttypes.PLAIN_TEXT + + By("creating an ocm resource from a plain text") + env.OCMCommonTransport(resourceLocalPath, accessio.FormatDirectory, func() { + env.Component(Component, func() { + env.Version(ComponentVersion, func() { + env.Resource(ResourceObj, ResourceVersion, resourceType, v1.LocalRelation, func() { + env.BlobData(mime.MIME_TEXT, []byte(ResourceContent)) + }) + }) + }) + }) + + repo, err := ctf.Open(env, accessobj.ACC_WRITABLE, resourceLocalPath, vfs.FileMode(vfs.O_RDWR), env) + Expect(err).NotTo(HaveOccurred()) + cv, err := repo.LookupComponentVersion(Component, ComponentVersion) + Expect(err).NotTo(HaveOccurred()) + cd, err := ocmPkg.ListComponentDescriptors(ctx, cv, repo) + Expect(err).NotTo(HaveOccurred()) + dataCds, err := yaml.Marshal(cd) + Expect(err).NotTo(HaveOccurred()) + + spec, err := ctf.NewRepositorySpec(ctf.ACC_READONLY, resourceLocalPath) + specData, err := spec.MarshalJSON() + + By("creating a mocked component") + componentObj = test.SetupComponentWithDescriptorList(ctx, componentName, namespace.GetName(), dataCds, &test.MockComponentOptions{ + Registry: registry, + Client: k8sClient, + Recorder: recorder, + Info: v1alpha1.ComponentInfo{ + Component: Component, + Version: ComponentVersion, + RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, }, - Resource: v1alpha1.ResourceID{ - ByReference: v1alpha1.ResourceReference{ - Resource: v1.NewIdentity(ResourceObj), + Repository: RepositoryObj, + }) + + By("creating a resource object") + resource := &v1alpha1.Resource{ + ObjectMeta: k8smetav1.ObjectMeta{ + Namespace: namespace.GetName(), + Name: ResourceObj, + }, + Spec: v1alpha1.ResourceSpec{ + ComponentRef: corev1.LocalObjectReference{ + Name: componentName, + }, + Resource: v1alpha1.ResourceID{ + ByReference: v1alpha1.ResourceReference{ + Resource: v1.NewIdentity(ResourceObj), + }, }, }, - Interval: metav1.Duration{Duration: time.Minute * 5}, - }, - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, resource, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + By("checking that the resource has been reconciled successfully") + waitUntilResourceIsReady(ctx, resource) + Expect(resource).To(HaveField("Status.Resource.Name", Equal(ResourceObj))) + Expect(resource).To(HaveField("Status.Resource.Type", Equal(resourceType))) + Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) + + resourceAcc, err := cv.GetResource(v1.NewIdentity(ResourceObj)) + Expect(err).NotTo(HaveOccurred()) + validateArtifact(ctx, resource, resourceAcc, ResourceVersion) + + By("delete resource manually") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) - By("checking that the resource has been reconciled successfully") - Eventually(komega.Object(resource), "5m").Should( - HaveField("Status.ObservedGeneration", Equal(int64(1)))) - Expect(resource).To(HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) - Expect(resource).To(HaveField("Status.Resource.Name", Equal(ResourceObj))) - Expect(resource).To(HaveField("Status.Resource.Type", Equal(artifacttypes.PLAIN_TEXT))) - Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) + It("can reconcile a compressed plaintext resource", func() { + resourceType := artifacttypes.PLAIN_TEXT - By("checking that the artifact has been created successfully") - artifact := &artifactv1.Artifact{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: resource.Namespace, - Name: resource.Status.ArtifactRef.Name, - }, - } - Eventually(komega.Get(artifact)).Should(Succeed()) + // The resource controller will only gzip-compress the content, if it is not already compressed. Thus, we + // expect the content to be only compressed once. + resourceContentCompressed, err := compression.AutoCompressAsGzip(ctx, []byte(ResourceContent)) + Expect(err).NotTo(HaveOccurred()) + + By("creating an ocm resource from a plain text") + env.OCMCommonTransport(resourceLocalPath, accessio.FormatDirectory, func() { + env.Component(Component, func() { + env.Version(ComponentVersion, func() { + env.Resource(ResourceObj, ResourceVersion, resourceType, v1.LocalRelation, func() { + env.BlobData(mime.MIME_TEXT, resourceContentCompressed) + }) + }) + }) + }) - By("checking that the artifact server provides the resource") - r := Must(http.Get(artifact.Spec.URL)) - Expect(r).Should(HaveHTTPStatus(http.StatusOK)) + repo, err := ctf.Open(env, accessobj.ACC_WRITABLE, resourceLocalPath, vfs.FileMode(vfs.O_RDWR), env) + Expect(err).NotTo(HaveOccurred()) + cv, err := repo.LookupComponentVersion(Component, ComponentVersion) + Expect(err).NotTo(HaveOccurred()) + cd, err := ocmPkg.ListComponentDescriptors(ctx, cv, repo) + Expect(err).NotTo(HaveOccurred()) + dataCds, err := yaml.Marshal(cd) + Expect(err).NotTo(HaveOccurred()) - By("checking that the resource content is correct") - reader, decompressed, err := compression.AutoDecompress(r.Body) - Expect(decompressed).To(BeTrue()) - DeferCleanup(func() { - Expect(reader.Close()).To(Succeed()) + spec, err := ctf.NewRepositorySpec(ctf.ACC_READONLY, resourceLocalPath) + specData, err := spec.MarshalJSON() + + By("creating a mocked component") + componentObj = test.SetupComponentWithDescriptorList(ctx, componentName, namespace.GetName(), dataCds, &test.MockComponentOptions{ + Registry: registry, + Client: k8sClient, + Recorder: recorder, + Info: v1alpha1.ComponentInfo{ + Component: Component, + Version: ComponentVersion, + RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, + }, + Repository: RepositoryObj, + }) + + By("creating a resource object") + resource := &v1alpha1.Resource{ + ObjectMeta: k8smetav1.ObjectMeta{ + Namespace: namespace.GetName(), + Name: ResourceObj, + }, + Spec: v1alpha1.ResourceSpec{ + ComponentRef: corev1.LocalObjectReference{ + Name: componentName, + }, + Resource: v1alpha1.ResourceID{ + ByReference: v1alpha1.ResourceReference{ + Resource: v1.NewIdentity(ResourceObj), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + By("checking that the resource has been reconciled successfully") + waitUntilResourceIsReady(ctx, resource) + Expect(resource).To(HaveField("Status.Resource.Name", Equal(ResourceObj))) + Expect(resource).To(HaveField("Status.Resource.Type", Equal(resourceType))) + Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) + + resourceAcc, err := cv.GetResource(v1.NewIdentity(ResourceObj)) + Expect(err).NotTo(HaveOccurred()) + validateArtifact(ctx, resource, resourceAcc, ResourceVersion) + + By("delete resource manually") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) - Expect(err).To(BeNil()) - resourceContent := Must(io.ReadAll(reader)) - Expect(string(resourceContent)).To(Equal(ResourceContent)) - }) - }) -}) + It("can reconcile a OCI artifact resource", func() { + resourceType := artifacttypes.OCI_ARTIFACT + + By("creating an OCI artifact") + repository, err := registry.NewRepository(ctx, ResourceObj) + Expect(err).NotTo(HaveOccurred()) + contentCompressed, err := compression.AutoCompressAsGzip(ctx, []byte(ResourceContent)) + Expect(err).ToNot(HaveOccurred()) + manifestDigest, err := repository.PushArtifact(ctx, ResourceVersion, contentCompressed) + Expect(err).ToNot(HaveOccurred()) + DeferCleanup(func(ctx SpecContext) { + Expect(repository.DeleteArtifact(ctx, manifestDigest.String())).To(Succeed()) + }) -// prepareComponent essentially mocks the behavior of the component reconciler to provider the necessary component and -// artifact for the resource controller. -func prepareComponent(ctx context.Context, env *Builder, ctfPath string) { - By("creating ocm repositories with a component and resource") - env.OCMCommonTransport(ctfPath, accessio.FormatDirectory, func() { - env.Component(Component, func() { - env.Version(ComponentVersion, func() { - env.Resource(ResourceObj, ResourceVersion, artifacttypes.PLAIN_TEXT, v1.LocalRelation, func() { - env.BlobData(mime.MIME_TEXT, []byte(ResourceContent)) + By("creating an ocm resource from an OCI artifact") + env.OCMCommonTransport(resourceLocalPath, accessio.FormatDirectory, func() { + env.Component(Component, func() { + env.Version(ComponentVersion, func() { + env.Resource(ResourceObj, ResourceVersion, resourceType, v1.LocalRelation, func() { + env.Access(ociartifact.New(fmt.Sprintf("http://%s/%s:%s", repository.GetHost(), repository.GetName(), ResourceVersion))) + }) + }) + }) }) + + repo, err := ctf.Open(env, accessobj.ACC_WRITABLE, resourceLocalPath, vfs.FileMode(vfs.O_RDWR), env) + Expect(err).NotTo(HaveOccurred()) + cv, err := repo.LookupComponentVersion(Component, ComponentVersion) + Expect(err).NotTo(HaveOccurred()) + cd, err := ocmPkg.ListComponentDescriptors(ctx, cv, repo) + Expect(err).NotTo(HaveOccurred()) + dataCds, err := yaml.Marshal(cd) + Expect(err).NotTo(HaveOccurred()) + + spec, err := ctf.NewRepositorySpec(ctf.ACC_READONLY, resourceLocalPath) + specData, err := spec.MarshalJSON() + Expect(err).NotTo(HaveOccurred()) + + By("creating a mocked component") + componentObj = test.SetupComponentWithDescriptorList(ctx, componentName, namespace.GetName(), dataCds, &test.MockComponentOptions{ + Registry: registry, + Client: k8sClient, + Recorder: recorder, + Info: v1alpha1.ComponentInfo{ + Component: Component, + Version: ComponentVersion, + RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, + }, + Repository: RepositoryObj, + }) + + By("creating a resource object") + resource := &v1alpha1.Resource{ + ObjectMeta: k8smetav1.ObjectMeta{ + Namespace: namespace.GetName(), + Name: ResourceObj, + }, + Spec: v1alpha1.ResourceSpec{ + ComponentRef: corev1.LocalObjectReference{ + Name: componentName, + }, + Resource: v1alpha1.ResourceID{ + ByReference: v1alpha1.ResourceReference{ + Resource: v1.NewIdentity(ResourceObj), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + By("checking that the resource has been reconciled successfully") + waitUntilResourceIsReady(ctx, resource) + Expect(resource).To(HaveField("Status.Resource.Name", Equal(ResourceObj))) + Expect(resource).To(HaveField("Status.Resource.Type", Equal(resourceType))) + Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) + + resourceAcc, err := cv.GetResource(v1.NewIdentity(ResourceObj)) + Expect(err).NotTo(HaveOccurred()) + validateArtifact(ctx, resource, resourceAcc, ResourceVersion) + + By("delete resource manually") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) + + It("can reconcile a plaintext resource with a plus in the version", func() { + resourceType := artifacttypes.PLAIN_TEXT + resourceVersionPlus := ResourceVersion + "+resourceVersionSuffix" + expectedBlobTag := "1.0.0.build-resourceVersionSuffix" + + By("creating an ocm resource from a plain text") + env.OCMCommonTransport(resourceLocalPath, accessio.FormatDirectory, func() { + env.Component(Component, func() { + env.Version(ComponentVersion, func() { + env.Resource(ResourceObj, resourceVersionPlus, resourceType, v1.LocalRelation, func() { + env.BlobData(mime.MIME_TEXT, []byte(ResourceContent)) + }) + }) + }) + }) + + repo, err := ctf.Open(env, accessobj.ACC_WRITABLE, resourceLocalPath, vfs.FileMode(vfs.O_RDWR), env) + Expect(err).NotTo(HaveOccurred()) + cv, err := repo.LookupComponentVersion(Component, ComponentVersion) + Expect(err).NotTo(HaveOccurred()) + cd, err := ocmPkg.ListComponentDescriptors(ctx, cv, repo) + Expect(err).NotTo(HaveOccurred()) + dataCds, err := yaml.Marshal(cd) + Expect(err).NotTo(HaveOccurred()) + + spec, err := ctf.NewRepositorySpec(ctf.ACC_READONLY, resourceLocalPath) + specData, err := spec.MarshalJSON() + + By("creating a mocked component") + componentObj = test.SetupComponentWithDescriptorList(ctx, componentName, namespace.GetName(), dataCds, &test.MockComponentOptions{ + Registry: registry, + Client: k8sClient, + Recorder: recorder, + Info: v1alpha1.ComponentInfo{ + Component: Component, + Version: ComponentVersion, + RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, + }, + Repository: RepositoryObj, + }) + + By("creating a resource object") + resource := &v1alpha1.Resource{ + ObjectMeta: k8smetav1.ObjectMeta{ + Namespace: namespace.GetName(), + Name: ResourceObj, + }, + Spec: v1alpha1.ResourceSpec{ + ComponentRef: corev1.LocalObjectReference{ + Name: componentName, + }, + Resource: v1alpha1.ResourceID{ + ByReference: v1alpha1.ResourceReference{ + Resource: v1.NewIdentity(ResourceObj), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + By("checking that the resource has been reconciled successfully") + waitUntilResourceIsReady(ctx, resource) + Expect(resource).To(HaveField("Status.Resource.Name", Equal(ResourceObj))) + Expect(resource).To(HaveField("Status.Resource.Type", Equal(resourceType))) + Expect(resource).To(HaveField("Status.Resource.Version", Equal(resourceVersionPlus))) + + resourceAcc, err := cv.GetResource(v1.NewIdentity(ResourceObj)) + Expect(err).NotTo(HaveOccurred()) + validateArtifact(ctx, resource, resourceAcc, expectedBlobTag) + + By("delete resource manually") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) + }) + + // TODO: Add more testcases }) }) +}) - By("creating a component descriptor") - tmpDirCd := Must(os.MkdirTemp("/tmp", "descriptors-")) - DeferCleanup(func() error { - return os.RemoveAll(tmpDirCd) - }) - repo := Must(ctf.Open(env, accessobj.ACC_WRITABLE, ctfPath, vfs.FileMode(vfs.O_RDWR), env)) - cv := Must(repo.LookupComponentVersion(Component, ComponentVersion)) - cd := Must(ocm.ListComponentDescriptors(ctx, cv, repo)) - dataCds := Must(yaml.Marshal(cd)) - MustBeSuccessful(os.WriteFile(filepath.Join(tmpDirCd, v1alpha1.OCMComponentDescriptorList), dataCds, 0o655)) - - By("creating a component object") - component := &v1alpha1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: ComponentObj, - }, - Spec: v1alpha1.ComponentSpec{ - RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: RepositoryObj, - }, - Component: Component, - Semver: ComponentVersion, - Interval: metav1.Duration{Duration: time.Minute * 10}, - }, - } - Expect(k8sClient.Create(ctx, component)).To(Succeed()) - - By("creating an component artifact") - revision := ComponentObj + "-" + ComponentVersion - var artifactName string - Expect(globStorage.ReconcileArtifact(ctx, component, revision, tmpDirCd, revision+".tar.gz", - func(art *artifactv1.Artifact, _ string) error { - // Archive directory to storage - if err := globStorage.Archive(art, tmpDirCd, nil); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } +func validateArtifact(ctx context.Context, resource *v1alpha1.Resource, resourceAccess ocm.ResourceAccess, expectedTag string) { + GinkgoHelper() - artifactName = art.Name + By("checking that resource has a reference to OCI artifact") + Eventually(komega.Object(resource), "15s").Should( + HaveField("Status.OCIArtifact", Not(BeNil()))) - return nil - }, - )).To(Succeed()) + By("checking that the OCI artifact contains the correct content") + ociRepo := Must(registry.NewRepository(ctx, resource.GetOCIRepository())) + contentCompressed := Must(ociRepo.FetchArtifact(ctx, resource.GetManifestDigest())) + gzipReader, err := gzip.NewReader(bytes.NewReader(contentCompressed)) + Expect(err).NotTo(HaveOccurred()) + content, err := io.ReadAll(gzipReader) + Expect(string(content)).To(Equal(ResourceContent)) - By("checking that the artifact has been created successfully") - artifact := &artifactv1.Artifact{ - ObjectMeta: metav1.ObjectMeta{ - Name: artifactName, - Namespace: Namespace, - }, - } - Eventually(komega.Get(artifact)).Should(Succeed()) - - By("updating the component object with the respective status") - baseComponent := component.DeepCopy() - ready := *conditions.TrueCondition("Ready", "ready", "message") - ready.LastTransitionTime = metav1.Time{Time: time.Now()} - baseComponent.Status.Conditions = []metav1.Condition{ready} - baseComponent.Status.ArtifactRef = corev1.LocalObjectReference{Name: artifact.ObjectMeta.Name} - spec := Must(ctf.NewRepositorySpec(ctf.ACC_READONLY, ctfPath)) - specData := Must(spec.MarshalJSON()) - baseComponent.Status.Component = v1alpha1.ComponentInfo{ - RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, - Component: Component, - Version: ComponentVersion, + Expect(resource.GetBlobDigest()).To(Equal(resourceAccess.Meta().Digest.Value)) + Expect(resource.Status.OCIArtifact.Blob.Tag).To(Equal(expectedTag)) + if resourceAccess.Meta().GetType() == "ociArtifact" { + // OCI artifacts are only copied and no information about the blob length is available (open TODO). + Expect(resource.Status.OCIArtifact.Blob.Size).To(Equal(int64(0))) + } else { + Expect(resource.Status.OCIArtifact.Blob.Size).To(Equal(int64(len(contentCompressed)))) } - Expect(k8sClient.Status().Update(ctx, baseComponent)).To(Succeed()) +} + +func waitUntilResourceIsReady(ctx context.Context, resource *v1alpha1.Resource) { + GinkgoHelper() + Eventually(func(g Gomega, ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + if err != nil { + return false + } + g.Expect(resource).Should(HaveField("Status.Resource", Not(BeNil()))) + + return conditions.IsReady(resource) + }, "15s").WithContext(ctx).Should(BeTrue()) } diff --git a/internal/controller/resource/suite_test.go b/internal/controller/resource/suite_test.go index c5f9a2a4..50cbcb53 100644 --- a/internal/controller/resource/suite_test.go +++ b/internal/controller/resource/suite_test.go @@ -16,37 +16,29 @@ package resource import ( "context" "fmt" - "io" - "net/http" "os" + "os/exec" "path/filepath" "runtime" "testing" - "time" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - "github.com/openfluxcd/controller-manager/server" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/yaml" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" metricserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // +kubebuilder:scaffold:imports @@ -54,16 +46,15 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -const ( - ARTIFACT_PATH = "ocm-k8s-artifactstore--*" - ARTIFACT_SERVER = "localhost:8081" -) - -var cfg *rest.Config var k8sClient client.Client var k8sManager ctrl.Manager var testEnv *envtest.Environment -var globStorage *storage.Storage +var recorder record.EventRecorder +var zotCmd *exec.Cmd +var registry *ociartifact.Registry +var zotRootDir string +var ctx context.Context +var cancel context.CancelFunc func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -76,28 +67,12 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") - // Get external artifact CRD - resp, err := http.Get(v1alpha1.ArtifactCrd) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() error { - return resp.Body.Close() - }) - - crdByte, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - - artifactCRD := &apiextensionsv1.CustomResourceDefinition{} - err = yaml.Unmarshal(crdByte, artifactCRD) - Expect(err).NotTo(HaveOccurred()) - testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{ filepath.Join("..", "..", "..", "config", "crd", "bases"), }, ErrorIfCRDPathMissing: true, - CRDs: []*apiextensionsv1.CustomResourceDefinition{artifactCRD}, - // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the // default path defined in controller-runtime which is /usr/local/kubebuilder/. @@ -108,13 +83,12 @@ var _ = BeforeSuite(func() { } // cfg is defined in this file globally. - cfg, err = testEnv.Start() + cfg, err := testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) DeferCleanup(testEnv.Stop) Expect(v1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) - Expect(artifactv1.AddToScheme(scheme.Scheme)).Should(Succeed()) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme @@ -132,32 +106,41 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - tmpdir := Must(os.MkdirTemp("", ARTIFACT_PATH)) - address := ARTIFACT_SERVER - globStorage = Must(server.NewStorage(k8sClient, testEnv.Scheme, tmpdir, address, 0, 0)) - artifactServer := Must(server.NewArtifactServer(tmpdir, address, time.Millisecond)) + recorder = &record.FakeRecorder{ + Events: make(chan string, 32), + IncludeObject: true, + } + + // Setup zot registry and start it up + zotRootDir = Must(os.MkdirTemp("", "")) + DeferCleanup(func() { + Expect(os.RemoveAll(zotRootDir)).To(Succeed()) + }) + + zotCmd, registry = test.SetupRegistry(filepath.Join("..", "..", "..", "bin", "zot-registry"), zotRootDir, "0.0.0.0", "8081") + + ctx, cancel = context.WithCancel(context.Background()) + DeferCleanup(cancel) Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ - Client: k8sClient, - Scheme: testEnv.Scheme, - EventRecorder: &record.FakeRecorder{ - Events: make(chan string, 32), - IncludeObject: true, - }, + Client: k8sClient, + Scheme: testEnv.Scheme, + EventRecorder: recorder, }, - Storage: globStorage, + Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel = context.WithCancel(context.Background()) DeferCleanup(cancel) - go func() { - defer GinkgoRecover() - Expect(artifactServer.Start(ctx)).To(Succeed()) - }() go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) }() }) + +var _ = AfterSuite(func(ctx context.Context) { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") +}) diff --git a/pkg/compression/util.go b/pkg/compression/util.go index 82cbef16..06099bcc 100644 --- a/pkg/compression/util.go +++ b/pkg/compression/util.go @@ -1,40 +1,30 @@ package compression import ( + "archive/tar" "bytes" + "compress/gzip" "context" "errors" "fmt" "io" "os" + "path/filepath" "github.com/containers/image/v5/pkg/compression" "sigs.k8s.io/controller-runtime/pkg/log" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" ) -type WriterToStorageFromArtifact interface { - Copy(art *artifactv1.Artifact, reader io.Reader) error -} - -// AutoCompressAsGzipAndArchiveFile compresses the file if it is not already compressed and archives it in the storage. -// If the file is already compressed as gzip, it will be archived as is. -// If the file is not compressed or not in gzip format, it will be attempting to recompress to gzip and then archive. +// AutoCompressAsGzip compresses the content if it is not already compressed and returns it. +// If the file is already compressed as gzip, it will be returned as is. +// If the file is not compressed or not in gzip format, it will be attempting to recompress to gzip and then return. // This is because some source controllers such as kustomize expect this compression format in their artifacts. -func AutoCompressAsGzipAndArchiveFile(ctx context.Context, art *artifactv1.Artifact, storage WriterToStorageFromArtifact, path string) (retErr error) { - logger := log.FromContext(ctx).WithValues("artifact", art.Name, "path", path) - file, err := os.OpenFile(path, os.O_RDONLY, 0o400) - if err != nil { - return fmt.Errorf("failed to open file: %w", err) - } - defer func() { - retErr = errors.Join(retErr, file.Close()) - }() +func AutoCompressAsGzip(ctx context.Context, data []byte) (_ []byte, retErr error) { + logger := log.FromContext(ctx) - algo, decompressor, reader, err := compression.DetectCompressionFormat(file) + algo, decompressor, reader, err := compression.DetectCompressionFormat(bytes.NewReader(data)) if err != nil { - return fmt.Errorf("failed to detect compression format: %w", err) + return nil, fmt.Errorf("failed to detect compression format: %w", err) } // If the file is @@ -46,7 +36,7 @@ func AutoCompressAsGzipAndArchiveFile(ctx context.Context, art *artifactv1.Artif if decompressor != nil { decompressed, err := decompressor(reader) if err != nil { - return fmt.Errorf("failed to decompress: %w", err) + return nil, fmt.Errorf("failed to decompress: %w", err) } defer func() { retErr = errors.Join(retErr, decompressed.Close()) @@ -54,25 +44,17 @@ func AutoCompressAsGzipAndArchiveFile(ctx context.Context, art *artifactv1.Artif reader = decompressed } - logger.V(1).Info("archiving file, but detected file is not compressed or not in gzip format, recompressing and archiving") - // TODO: this loads the single file into memory which can be expensive, but orchestrating an io.Pipe here is not trivial + logger.V(1).Info("gzip-compress data") var buf bytes.Buffer if err := compressViaBuffer(&buf, reader); err != nil { - return err - } - if err := storage.Copy(art, &buf); err != nil { - return fmt.Errorf("failed to copy: %w", err) + return nil, err } - return nil - } - - logger.V(1).Info("archiving already compressed file from path") - if err := storage.Copy(art, reader); err != nil { - return fmt.Errorf("failed to copy file: %w", err) + return buf.Bytes(), nil } - return nil + // Return as is, if already compressed in gzip format + return data, nil } func compressViaBuffer(buf *bytes.Buffer, reader io.Reader) (retErr error) { @@ -83,9 +65,116 @@ func compressViaBuffer(buf *bytes.Buffer, reader io.Reader) (retErr error) { defer func() { retErr = errors.Join(retErr, compressToBuf.Close()) }() + if _, err := io.Copy(compressToBuf, reader); err != nil { return fmt.Errorf("failed to copy: %w", err) } return nil } + +func ExtractDataFromTGZ(data []byte) (_ []byte, retErr error) { + buf := bytes.NewReader(data) + + gzipReader, err := gzip.NewReader(buf) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer func() { + retErr = gzipReader.Close() + }() + + tarReader := tar.NewReader(gzipReader) + + _, err = tarReader.Next() + if err != nil { + return nil, fmt.Errorf("failed to read tar header: %w", err) + } + + var extractedData bytes.Buffer + //nolint:gosec // TODO: Decision needed + if _, err := io.Copy(&extractedData, tarReader); err != nil { + return nil, fmt.Errorf("failed to extract file content: %w", err) + } + + return extractedData.Bytes(), nil +} + +func CreateTGZFromPath(srcDir string) (_ []byte, retErr error) { + var buf bytes.Buffer + + // Create a gzip writer + gzipWriter := gzip.NewWriter(&buf) + defer func() { + retErr = gzipWriter.Close() + }() + + // Create a tar writer + tarWriter := tar.NewWriter(gzipWriter) + defer func() { + retErr = tarWriter.Close() + }() + + // Walk through the source directory + if err := filepath.Walk(srcDir, func(file string, fileInfo os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("could not read file %s: %w", file, err) + } + + if !fileInfo.Mode().IsRegular() { + return nil + } + + if fileInfo.IsDir() { + return nil + } + + // Create tar fileHeader + fileHeader, err := tar.FileInfoHeader(fileInfo, fileInfo.Name()) + if err != nil { + return fmt.Errorf("could not create tar fileHeader: %w", err) + } + + // Use relative path for fileHeader.Name to preserve folder structure + relPath, err := filepath.Rel(srcDir, file) + if err != nil { + return fmt.Errorf("could not create relative path: %w", err) + } + fileHeader.Name = relPath + + // Write fileHeader + err = tarWriter.WriteHeader(fileHeader) + if err != nil { + return fmt.Errorf("could not write tar fileHeader: %w", err) + } + + // Open the file + f, err := os.Open(file) + if err != nil { + return fmt.Errorf("could not open file %s: %w", file, err) + } + defer func() { + err = f.Close() + }() + + // Copy file data into the tar archive + _, err = io.Copy(tarWriter, f) + if err != nil { + return fmt.Errorf("could not copy file %s: %w", file, err) + } + + return nil + }); err != nil { + return nil, fmt.Errorf("could not walk directory %s: %w", srcDir, err) + } + + if err := tarWriter.Close(); err != nil { + return nil, fmt.Errorf("could not close tar writer: %w", err) + } + + if err := gzipWriter.Close(); err != nil { + return nil, fmt.Errorf("could not close gzip writer: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/pkg/compression/util_test.go b/pkg/compression/util_test.go index 384e09f8..d127414e 100644 --- a/pkg/compression/util_test.go +++ b/pkg/compression/util_test.go @@ -5,80 +5,53 @@ import ( "compress/gzip" "context" "io" - "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/ulikunitz/xz" contcompression "github.com/containers/image/v5/pkg/compression" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" ) -const testfile = "testfile" - var testData = []byte("test") -type MockStorage struct { - data *bytes.Buffer -} - -func (m *MockStorage) Copy(_ *artifactv1.Artifact, reader io.Reader) error { - m.data = new(bytes.Buffer) - _, err := io.Copy(m.data, reader) - return err -} - -func (m *MockStorage) GetData() *bytes.Buffer { - return m.data -} - -var _ compression.WriterToStorageFromArtifact = &MockStorage{} - func TestAutoCompressAndArchiveFile(t *testing.T) { tests := []struct { name string - setup func(t *testing.T) (string, *MockStorage) - validate func(t *testing.T, storage *MockStorage) + setup func(t *testing.T) []byte + validate func(t *testing.T, data []byte) }{ { name: "Uncompressed", - setup: func(t *testing.T) (string, *MockStorage) { - path := filepath.Join(t.TempDir(), testfile) - assert.NoError(t, os.WriteFile(path, testData, 0o644)) - return path, &MockStorage{} + setup: func(t *testing.T) []byte { + return testData }, validate: validateAutoCompressedAsGzip, }, { name: "Precompressed_Gzip", - setup: func(t *testing.T) (string, *MockStorage) { - path := filepath.Join(t.TempDir(), testfile) + setup: func(t *testing.T) []byte { var buf bytes.Buffer compress := gzip.NewWriter(&buf) _, err := io.Copy(compress, bytes.NewReader(testData)) assert.NoError(t, compress.Close()) assert.NoError(t, err) - assert.NoError(t, os.WriteFile(path, buf.Bytes(), 0o644)) - return path, &MockStorage{} + return buf.Bytes() }, validate: validateAutoCompressedAsGzip, }, { name: "Precompressed_Nongzip_Xz", - setup: func(t *testing.T) (string, *MockStorage) { - path := filepath.Join(t.TempDir(), testfile) + setup: func(t *testing.T) []byte { var buf bytes.Buffer compress, err := xz.NewWriter(&buf) assert.NoError(t, err) _, err = io.Copy(compress, bytes.NewReader(testData)) assert.NoError(t, compress.Close()) assert.NoError(t, err) - assert.NoError(t, os.WriteFile(path, buf.Bytes(), 0o644)) - return path, &MockStorage{} + return buf.Bytes() }, validate: validateAutoCompressedAsGzip, }, @@ -86,18 +59,17 @@ func TestAutoCompressAndArchiveFile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - path, storage := tt.setup(t) - art := &artifactv1.Artifact{} - err := compression.AutoCompressAsGzipAndArchiveFile(context.Background(), art, storage, path) + data := tt.setup(t) + dataCompressed, err := compression.AutoCompressAsGzip(context.Background(), data) assert.NoError(t, err) - tt.validate(t, storage) + tt.validate(t, dataCompressed) }) } } -func validateAutoCompressedAsGzip(t *testing.T, storage *MockStorage) { +func validateAutoCompressedAsGzip(t *testing.T, input []byte) { t.Helper() - algo, decompress, reader, err := contcompression.DetectCompressionFormat(storage.GetData()) + algo, decompress, reader, err := contcompression.DetectCompressionFormat(bytes.NewReader(input)) assert.NoError(t, err) assert.Equal(t, contcompression.Gzip.Name(), algo.Name()) decompressed, err := decompress(reader) diff --git a/pkg/ociartifact/artifact.go b/pkg/ociartifact/artifact.go new file mode 100644 index 00000000..33b1b2fd --- /dev/null +++ b/pkg/ociartifact/artifact.go @@ -0,0 +1,34 @@ +package ociartifact + +import ( + "context" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" +) + +// DeleteForObject checks if the object holds a name for an OCI repository, checks if the OCI repository exists, and if +// so, deletes the OCI artifact from the OCI repository. +func DeleteForObject(ctx context.Context, registry RegistryType, obj v1alpha1.OCIArtifactCreator) error { + info := obj.GetOCIArtifact() + if info == nil { + return nil + } + + if info.Repository != "" { + ociRepository, err := registry.NewRepository(ctx, info.Repository) + if err != nil { + return err + } + + exists, err := ociRepository.ExistsArtifact(ctx, info.Digest) + if err != nil { + return err + } + + if exists { + return ociRepository.DeleteArtifact(ctx, info.Digest) + } + } + + return nil +} diff --git a/pkg/artifact/object_config.go b/pkg/ociartifact/object_config.go similarity index 98% rename from pkg/artifact/object_config.go rename to pkg/ociartifact/object_config.go index 8ad01388..a52ce1b3 100644 --- a/pkg/artifact/object_config.go +++ b/pkg/ociartifact/object_config.go @@ -1,4 +1,4 @@ -package artifact +package ociartifact import ( "bytes" diff --git a/pkg/ociartifact/registry.go b/pkg/ociartifact/registry.go new file mode 100644 index 00000000..28a4d14b --- /dev/null +++ b/pkg/ociartifact/registry.go @@ -0,0 +1,39 @@ +package ociartifact + +import ( + "context" + "errors" + + "oras.land/oras-go/v2/registry/remote" +) + +type RegistryType interface { + NewRepository(ctx context.Context, name string) (RepositoryType, error) +} + +type Registry struct { + *remote.Registry +} + +func NewRegistry(url string) (*Registry, error) { + registry, err := remote.NewRegistry(url) + if err != nil { + return nil, err + } + + return &Registry{registry}, nil +} + +func (r *Registry) NewRepository(ctx context.Context, name string) (RepositoryType, error) { + repository, err := r.Repository(ctx, name) + if err != nil { + return nil, err + } + + remoteRepository, ok := repository.(*remote.Repository) + if !ok { + return nil, errors.New("invalid repository type") + } + + return &Repository{remoteRepository}, nil +} diff --git a/pkg/ociartifact/repository.go b/pkg/ociartifact/repository.go new file mode 100644 index 00000000..6985535d --- /dev/null +++ b/pkg/ociartifact/repository.go @@ -0,0 +1,288 @@ +package ociartifact + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/url" + "regexp" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/mitchellh/hashstructure/v2" + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/registry/remote" + "sigs.k8s.io/controller-runtime/pkg/log" + + ociV1 "github.com/opencontainers/image-spec/specs-go/v1" + ocmctx "ocm.software/ocm/api/ocm" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" +) + +// A RepositoryType is a type that can push and fetch blobs. +type RepositoryType interface { + // PushArtifact is a wrapper to push a single layer OCI artifact with an empty config and a single data layer + // containing the blob. As all OCI artifacts are produced and consumed by us, we do not have to care about the + // configuration. + PushArtifact(ctx context.Context, reference string, blob []byte) (digest.Digest, error) + + // FetchArtifact is a wrapper to fetch a single layer OCI artifact with a manifest digest. It expects and returns + // the single data layer. + FetchArtifact(ctx context.Context, manifestDigest string) ([]byte, error) + + // DeleteArtifact is a wrapper to delete a single layer OCI artifact with a manifest digest. + DeleteArtifact(ctx context.Context, manifestDigest string) error + + // ExistsArtifact is a wrapper to check if an OCI repository exists using the manifest digest. + ExistsArtifact(ctx context.Context, manifestDigest string) (bool, error) + + // CopyOCIArtifactForResourceAccess is a wrapper to copy an OCI artifact from an OCM resource access. + CopyOCIArtifactForResourceAccess(ctx context.Context, access ocmctx.ResourceAccess) (digest.Digest, error) + + GetHost() string + GetName() string +} + +// Repository is a wrapper for an OCI repository that provides methods to work with OCI artifacts. +type Repository struct { + *remote.Repository +} + +func (r *Repository) GetHost() string { + return r.Reference.Host() +} + +func (r *Repository) GetName() string { + return r.Reference.Repository +} + +func (r *Repository) PushArtifact(ctx context.Context, tag string, blob []byte) (digest.Digest, error) { + logger := log.FromContext(ctx) + + // Prepare and upload blob + blobDescriptor := ociV1.Descriptor{ + // The media type is meaningless as we do not use it. Thus, we just set a default one. + MediaType: ociV1.MediaTypeImageLayerGzip, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + } + + logger.Info("pushing blob", "descriptor", blobDescriptor) + if err := r.Push(ctx, blobDescriptor, content.NewVerifyReader( + bytes.NewReader(blob), + blobDescriptor, + )); err != nil { + return "", fmt.Errorf("error pushing blob: %w", err) + } + + // Prepare and upload an empty image config. As we do not plan to make use of the image config, the content does + // not matter. + emptyImageConfig := []byte("{}") + + imageConfigDescriptor := ociV1.Descriptor{ + MediaType: ociV1.MediaTypeImageConfig, + Digest: digest.FromBytes(emptyImageConfig), + Size: int64(len(emptyImageConfig)), + } + + logger.Info("pushing empty image config", "descriptor", imageConfigDescriptor) + if err := r.Push(ctx, imageConfigDescriptor, content.NewVerifyReader( + bytes.NewReader(emptyImageConfig), + imageConfigDescriptor, + )); err != nil { + return "", fmt.Errorf("error pushing empty config: %w", err) + } + + // Prepare and upload manifest + manifest := ociV1.Manifest{ + Versioned: specs.Versioned{SchemaVersion: v1alpha1.OCISchemaVersion}, + MediaType: ociV1.MediaTypeImageManifest, + Config: imageConfigDescriptor, + Layers: []ociV1.Descriptor{blobDescriptor}, + } + + manifestBytes, err := json.Marshal(manifest) + if err != nil { + return "", fmt.Errorf("error marshaling manifest: %w", err) + } + + manifestDigest := digest.FromBytes(manifestBytes) + + manifestDescriptor := ociV1.Descriptor{ + MediaType: manifest.MediaType, + Digest: manifestDigest, + Size: int64(len(manifestBytes)), + } + + logger.Info("pushing image manifest", "descriptor", manifestDescriptor) + if err := r.Push(ctx, manifestDescriptor, content.NewVerifyReader( + bytes.NewReader(manifestBytes), + manifestDescriptor, + )); err != nil { + return "", fmt.Errorf("error pushing manifest: %w", err) + } + + logger.Info("tagging image manifest", "tag", tag) + if err := r.Tag(ctx, manifestDescriptor, tag); err != nil { + return "", fmt.Errorf("error tagging manifest: %w", err) + } + + logger.Info("pushed single OCI artifact") + + return manifestDigest, nil +} + +func (r *Repository) FetchArtifact(ctx context.Context, manifestDigest string) ([]byte, error) { + // Fetch manifest descriptor to get manifest. + manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) + if err != nil { + return nil, fmt.Errorf("error fetching manifest: %w", err) + } + + manifestReader, err := r.Fetch(ctx, manifestDescriptor) + if err != nil { + return nil, fmt.Errorf("error fetching manifest: %w", err) + } + + var manifest ociV1.Manifest + if err := json.NewDecoder(manifestReader).Decode(&manifest); err != nil { + return nil, fmt.Errorf("error parsing manifest: %w", err) + } + + // We only expect single layer artifacts. + if len(manifest.Layers) != 1 { + return nil, fmt.Errorf("expected 1 layer, got %d", len(manifest.Layers)) + } + + reader, err := r.Fetch(ctx, manifest.Layers[0]) + if err != nil { + return nil, fmt.Errorf("error fetching layer: %w", err) + } + + return io.ReadAll(reader) +} + +func (r *Repository) DeleteArtifact(ctx context.Context, manifestDigest string) error { + logger := log.FromContext(ctx) + + manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) + if err != nil { + return fmt.Errorf("error fetching manifest: %w", err) + } + + logger.Info("deleting OCI artifact", "digest", manifestDigest) + + return r.Delete(ctx, manifestDescriptor) +} + +func (r *Repository) ExistsArtifact(ctx context.Context, manifestDigest string) (bool, error) { + logger := log.FromContext(ctx) + + manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) + if err != nil { + return false, fmt.Errorf("error fetching manifest: %w", err) + } + + logger.Info("checking if OCI artifact exists", "digest", manifestDigest) + + return r.Exists(ctx, manifestDescriptor) +} + +func (r *Repository) CopyOCIArtifactForResourceAccess(ctx context.Context, access ocmctx.ResourceAccess) (digest.Digest, error) { + logger := log.FromContext(ctx) + + gloAccess := access.GlobalAccess() + accessSpec, ok := gloAccess.(*ociartifact.AccessSpec) + if !ok { + return "", fmt.Errorf("expected type ociartifact.AccessSpec, but got %T", gloAccess) + } + + var http bool + var refSanitized string + refURL, err := url.Parse(accessSpec.ImageReference) + if err != nil { + return "", fmt.Errorf("error parsing image reference: %w", err) + } + + if refURL.Scheme != "" { + if refURL.Scheme == "http" { + http = true + } + refSanitized = strings.TrimPrefix(accessSpec.ImageReference, refURL.Scheme+"://") + } else { + refSanitized = accessSpec.ImageReference + } + + ref, err := name.ParseReference(refSanitized) + if err != nil { + return "", fmt.Errorf("error parsing image reference: %w", err) + } + + sourceRegistry, err := remote.NewRegistry(ref.Context().RegistryStr()) + if err != nil { + return "", fmt.Errorf("error creating source registry: %w", err) + } + + if http { + sourceRegistry.PlainHTTP = true + } + + sourceRepository, err := sourceRegistry.Repository(ctx, ref.Context().RepositoryStr()) + if err != nil { + return "", fmt.Errorf("error creating source repository: %w", err) + } + + desc, err := oras.Copy(ctx, sourceRepository, ref.Identifier(), r.Repository, ref.Identifier(), oras.CopyOptions{ + CopyGraphOptions: oras.CopyGraphOptions{ + PreCopy: func(_ context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + + return nil + }, + PostCopy: func(_ context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + + return nil + }, + OnCopySkipped: func(_ context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + + return nil + }, + }, + }) + if err != nil { + return "", fmt.Errorf("error copying OCI artifact: %w", err) + } + + return desc.Digest, nil +} + +// CreateRepositoryName creates a name for an OCI repository and returns a hashed string from the passed arguments. The +// purpose of this function is to sanitize any passed string to an OCI repository compliant name. +func CreateRepositoryName(args ...string) (string, error) { + hash, err := hashstructure.Hash(args, hashstructure.FormatV2, nil) + if err != nil { + return "", fmt.Errorf("failed to hash identity: %w", err) + } + + repositoryName := fmt.Sprintf("sha-%d", hash) + + match, err := regexp.MatchString(v1alpha1.OCIRepositoryNameConstraints, repositoryName) + if err != nil { + return "", fmt.Errorf("failed to check OCI repository repositoryName constraints: %w", err) + } + + if !match { + return "", fmt.Errorf("repositoryName '%s' failed to match OCI repository repositoryName constraints: %w", repositoryName, err) + } + + return repositoryName, nil +} diff --git a/pkg/artifact/resource.go b/pkg/ociartifact/resource.go similarity index 61% rename from pkg/artifact/resource.go rename to pkg/ociartifact/resource.go index e4829a3a..01e20ec4 100644 --- a/pkg/artifact/resource.go +++ b/pkg/ociartifact/resource.go @@ -1,6 +1,7 @@ -package artifact +package ociartifact import ( + "bytes" "context" "errors" "fmt" @@ -10,11 +11,9 @@ import ( "github.com/containers/image/v5/pkg/compression" "github.com/fluxcd/pkg/runtime/conditions" - "github.com/openfluxcd/controller-manager/storage" "sigs.k8s.io/controller-runtime/pkg/client" fluxtar "github.com/fluxcd/pkg/tar" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/util" @@ -25,7 +24,8 @@ var ( ErrNotYetReady = errors.New("not yet ready") ) -// Content is an interface that represents the content of an artifact. +// Content is an interface that represents the content of a stored OCI artifact. +// I.e. either component descriptors or resource content. type Content interface { // Open returns a reader for instruction. It can be a tarball, a file, etc. // The caller is responsible for closing the reader. @@ -35,71 +35,66 @@ type Content interface { // It returns an error if the source cannot be unpacked. UnpackIntoDirectory(path string) (err error) - // RevisionAndDigest returns the revision and digest of the artifact content. + // RevisionAndDigest returns the revision and digest of the OCI artifact content. util.RevisionAndDigest } -func NewContentBackedByComponentResourceArtifact( - storage *storage.Storage, +func NewContentBackedByComponentResource( + registry RegistryType, component *v1alpha1.Component, resource *v1alpha1.Resource, - artifact *artifactv1.Artifact, ) Content { - return &ContentBackedByStorageAndComponent{ - Storage: storage, + return &ContentBackedByArtifactAndComponent{ + Registry: registry, Component: component, Resource: resource, - Artifact: artifact, } } -type ContentBackedByStorageAndComponent struct { - Storage *storage.Storage +// ContentBackedByArtifactAndComponent contains information to get the component, resource and their respective OCI +// artifacts. +type ContentBackedByArtifactAndComponent struct { + Registry RegistryType Component *v1alpha1.Component Resource *v1alpha1.Resource - Artifact *artifactv1.Artifact } -func (r *ContentBackedByStorageAndComponent) GetDigest() (string, error) { - return r.Artifact.Spec.Digest, nil +func (r *ContentBackedByArtifactAndComponent) GetDigest() (string, error) { + return r.Resource.GetBlobDigest(), nil } -func (r *ContentBackedByStorageAndComponent) GetRevision() string { +func (r *ContentBackedByArtifactAndComponent) GetRevision() string { return fmt.Sprintf( - "artifact %s in revision %s (from resource %s, based on component %s)", - r.Artifact.GetName(), - r.Artifact.Spec.Revision, + "OCIArtifact (Repository) %s in revision %s (from resource %s, based on component %s)", + r.Resource.GetOCIRepository(), + r.Resource.GetBlobDigest(), r.Resource.GetName(), r.Component.GetName(), ) } -func (r *ContentBackedByStorageAndComponent) Open() (io.ReadCloser, error) { +func (r *ContentBackedByArtifactAndComponent) Open() (io.ReadCloser, error) { return r.open() } -func (r *ContentBackedByStorageAndComponent) open() (io.ReadCloser, error) { - path := r.Storage.LocalPath(r.Artifact) - - unlock, err := r.Storage.Lock(r.Artifact) +func (r *ContentBackedByArtifactAndComponent) open() (io.ReadCloser, error) { + ctx := context.Background() + repository, err := r.Registry.NewRepository(context.Background(), r.Resource.GetOCIRepository()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to open repository: %w", err) } - readCloser, err := os.OpenFile(path, os.O_RDONLY, 0o600) + data, err := repository.FetchArtifact(ctx, r.Resource.GetManifestDigest()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to fetch OCI artifact: %w", err) } - return &lockedReadCloser{ - ReadCloser: readCloser, - unlock: unlock, - }, nil + return io.NopCloser(bytes.NewReader(data)), nil } var _ io.ReadCloser = &lockedReadCloser{} -func (r *ContentBackedByStorageAndComponent) UnpackIntoDirectory(path string) (err error) { +func (r *ContentBackedByArtifactAndComponent) UnpackIntoDirectory(path string) (err error) { fi, err := os.Stat(path) if err == nil && fi.IsDir() { return ErrAlreadyUnpacked @@ -110,6 +105,9 @@ func (r *ContentBackedByStorageAndComponent) UnpackIntoDirectory(path string) (e } data, err := r.open() + if err != nil { + return fmt.Errorf("failed to get data: %w", err) + } defer func() { err = errors.Join(err, data.Close()) }() @@ -122,12 +120,14 @@ func (r *ContentBackedByStorageAndComponent) UnpackIntoDirectory(path string) (e err = errors.Join(err, decompressed.Close()) }() + // If it is a tar it must be a directory isTar, reader := util.IsTar(decompressed) if isTar { return fluxtar.Untar(reader, path, fluxtar.WithSkipGzip()) } - path = filepath.Join(path, filepath.Base(r.Storage.LocalPath(r.Artifact))) + // If it is not a tar it should be a file + path = filepath.Join(path, r.GetResource().GetName()) file, err := os.Create(path) if err != nil { return fmt.Errorf("failed to unpack file at %s: %w", path, err) @@ -142,11 +142,11 @@ func (r *ContentBackedByStorageAndComponent) UnpackIntoDirectory(path string) (e return nil } -func (r *ContentBackedByStorageAndComponent) GetComponent() *v1alpha1.Component { +func (r *ContentBackedByArtifactAndComponent) GetComponent() *v1alpha1.Component { return r.Component } -func (r *ContentBackedByStorageAndComponent) GetResource() *v1alpha1.Resource { +func (r *ContentBackedByArtifactAndComponent) GetResource() *v1alpha1.Resource { return r.Resource } @@ -166,30 +166,30 @@ func (l *lockedReadCloser) Close() error { func GetContentBackedByArtifactFromComponent( ctx context.Context, clnt client.Reader, - strg *storage.Storage, + registry RegistryType, ref *v1alpha1.ConfigurationReference, ) (Content, error) { if ref.APIVersion == "" { ref.APIVersion = v1alpha1.GroupVersion.String() } - component, resource, artifact, err := GetComponentResourceArtifactFromReference(ctx, clnt, strg, ref) + component, resource, err := GetComponentResourceFromReference(ctx, clnt, registry, ref) if err != nil { return nil, err } - return NewContentBackedByComponentResourceArtifact(strg, component, resource, artifact), nil + return NewContentBackedByComponentResource(registry, component, resource), nil } type ObjectWithTargetReference interface { GetTarget() *v1alpha1.ConfigurationReference } -func GetComponentResourceArtifactFromReference( +func GetComponentResourceFromReference( ctx context.Context, clnt client.Reader, - strg *storage.Storage, + registry RegistryType, ref *v1alpha1.ConfigurationReference, -) (*v1alpha1.Component, *v1alpha1.Resource, *artifactv1.Artifact, error) { +) (*v1alpha1.Component, *v1alpha1.Resource, error) { var ( resource client.Object err error @@ -203,20 +203,20 @@ func GetComponentResourceArtifactFromReference( case v1alpha1.KindResource: resource = &v1alpha1.Resource{} default: - return nil, nil, nil, fmt.Errorf("unsupported reference kind: %s", ref.Kind) + return nil, nil, fmt.Errorf("unsupported reference kind: %s", ref.Kind) } if err = clnt.Get(ctx, client.ObjectKey{Namespace: ref.Namespace, Name: ref.Name}, resource); err != nil { - return nil, nil, nil, fmt.Errorf("failed to fetch resource %s: %w", ref.Name, err) + return nil, nil, fmt.Errorf("failed to fetch resource %s: %w", ref.Name, err) } if !resource.GetDeletionTimestamp().IsZero() { - return nil, nil, nil, fmt.Errorf("resource %s was marked for deletion and cannot be used, waiting for recreation", ref.Name) + return nil, nil, fmt.Errorf("resource %s was marked for deletion and cannot be used, waiting for recreation", ref.Name) } if conditionCheckable, ok := resource.(conditions.Getter); ok { if !conditions.IsReady(conditionCheckable) { - return nil, nil, nil, fmt.Errorf("%w: resource %s", ErrNotYetReady, ref.Name) + return nil, nil, fmt.Errorf("%w: resource %s", ErrNotYetReady, ref.Name) } } @@ -227,26 +227,18 @@ func GetComponentResourceArtifactFromReference( Namespace: res.GetNamespace(), Name: res.Spec.ComponentRef.Name, }, component); err != nil { - return nil, nil, nil, fmt.Errorf("failed to fetch component %s to which resource %s belongs: %w", res.Spec.ComponentRef.Name, ref.Name, err) - } - - art := &artifactv1.Artifact{} - if err = clnt.Get(ctx, client.ObjectKey{ - Namespace: res.GetNamespace(), - Name: res.Status.ArtifactRef.Name, - }, art); err != nil { - return nil, nil, nil, fmt.Errorf("failed to fetch artifact %s belonging to resource %s: %w", res.Status.ArtifactRef.Name, ref.Name, err) + return nil, nil, fmt.Errorf("failed to fetch component %s to which resource %s belongs: %w", res.Spec.ComponentRef.Name, ref.Name, err) } - return component, res, art, nil + return component, res, nil } targetable, ok := resource.(ObjectWithTargetReference) if !ok { - return nil, nil, nil, fmt.Errorf("unsupported reference type: %T", resource) + return nil, nil, fmt.Errorf("unsupported reference type: %T", resource) } - return GetComponentResourceArtifactFromReference(ctx, clnt, strg, targetable.GetTarget()) + return GetComponentResourceFromReference(ctx, clnt, registry, targetable.GetTarget()) } // UniqueIDsForArtifactContentCombination returns a set of unique identifiers for the combination of two Content. diff --git a/pkg/ocm/artifact.go b/pkg/ocm/artifact.go deleted file mode 100644 index 48a85a64..00000000 --- a/pkg/ocm/artifact.go +++ /dev/null @@ -1,138 +0,0 @@ -package ocm - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/openfluxcd/controller-manager/storage" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/yaml" - "ocm.software/ocm/api/ocm/compdesc" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - ctrl "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" -) - -// GetComponentSetForArtifact returns the component descriptor set for the given artifact. -func GetComponentSetForArtifact(storage *storage.Storage, artifact *artifactv1.Artifact) (_ *compdesc.ComponentVersionSet, retErr error) { - tmp, err := os.MkdirTemp("", "component-*") - if err != nil { - return nil, fmt.Errorf("failed to create temporary directory: %w", err) - } - defer func() { - retErr = errors.Join(retErr, os.RemoveAll(tmp)) - }() - - // Instead of using the http-functionality of the storage-server, we use the storage directly for performance reasons. - // This assumes that the controllers and the storage are running in the same pod. - unlock, err := storage.Lock(artifact) - if err != nil { - return nil, fmt.Errorf("failed to lock artifact: %w", err) - } - defer unlock() - - filePath := filepath.Join(tmp, v1alpha1.OCMComponentDescriptorList) - - if err := storage.CopyToPath(artifact, v1alpha1.OCMComponentDescriptorList, filePath); err != nil { - return nil, fmt.Errorf("failed to copy artifact to path: %w", err) - } - - // Read component descriptor list - file, err := os.Open(filePath) - if err != nil { - return nil, fmt.Errorf("failed to open component descriptor: %w", err) - } - defer func() { - retErr = errors.Join(retErr, file.Close()) - }() - - // Get component descriptor set - cds := &Descriptors{} - - if err := yaml.NewYAMLToJSONDecoder(file).Decode(cds); err != nil { - return nil, fmt.Errorf("failed to unmarshal component descriptors: %w", err) - } - - return compdesc.NewComponentVersionSet(cds.List...), nil -} - -// GetAndVerifyArtifactForCollectable gets the artifact for the given collectable and verifies it against the given strg. -// If the artifact is not found, an error is returned. -func GetAndVerifyArtifactForCollectable( - ctx context.Context, - reader ctrl.Reader, - strg *storage.Storage, - collectable storage.Collectable, -) (*artifactv1.Artifact, error) { - artifact := strg.NewArtifactFor(collectable.GetKind(), collectable.GetObjectMeta(), "", "") - if err := reader.Get(ctx, types.NamespacedName{Name: artifact.Name, Namespace: artifact.Namespace}, artifact); err != nil { - return nil, fmt.Errorf("failed to get artifact: %w", err) - } - - // Check the digest of the archive and compare it to the one in the artifact - if err := strg.VerifyArtifact(artifact); err != nil { - return nil, fmt.Errorf("failed to verify artifact: %w", err) - } - - return artifact, nil -} - -// ValidateArtifactForCollectable verifies if the artifact for the given collectable is valid. -// This means that the artifact must be present in the cluster the reader is connected to and -// the artifact must be present in the storage. -// Additionally, the digest of the artifact must be different from the file name of the artifact. -// -// This method can be used to determine if an artifact needs an update or not because an artifact that does not -// fulfill these conditions can be considered out of date (not in the cluster, not in the storage, or mismatching digest). -// -// Prerequisite for this method is that the artifact name is based on its original digest. -func ValidateArtifactForCollectable( - ctx context.Context, - reader ctrl.Reader, - strg *storage.Storage, - collectable storage.Collectable, - digest string, -) (bool, error) { - artifact, err := GetAndVerifyArtifactForCollectable(ctx, reader, strg, collectable) - if errors.Is(err, os.ErrNotExist) { - return false, nil - } - if ctrl.IgnoreNotFound(err) != nil { - return false, fmt.Errorf("failed to get artifact: %w", err) - } - if artifact == nil { - return false, nil - } - - existingFile := filepath.Base(strg.LocalPath(artifact)) - - return existingFile != digest, nil -} - -// RemoveArtifactForCollectable removes the artifact for the given collectable from the given storage. -func RemoveArtifactForCollectable( - ctx context.Context, - client ctrl.Client, - strg *storage.Storage, - collectable storage.Collectable, -) error { - artifact, err := GetAndVerifyArtifactForCollectable(ctx, client, strg, collectable) - if ctrl.IgnoreNotFound(err) != nil { - return fmt.Errorf("failed to get artifact: %w", err) - } - - if artifact != nil { - if err := strg.Remove(artifact); err != nil { - if !os.IsNotExist(err) { - return fmt.Errorf("failed to remove artifact: %w", err) - } - } - } - - return nil -} diff --git a/pkg/ocm/ocm.go b/pkg/ocm/ocm.go index 6823d073..de402410 100644 --- a/pkg/ocm/ocm.go +++ b/pkg/ocm/ocm.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "regexp" + "strings" "github.com/Masterminds/semver/v3" "github.com/mandelsoft/goutils/matcher" @@ -13,6 +14,7 @@ import ( "ocm.software/ocm/api/ocm/compdesc" "ocm.software/ocm/api/ocm/cpi" "ocm.software/ocm/api/ocm/extensions/attrs/signingattr" + "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg" "ocm.software/ocm/api/ocm/tools/signing" "ocm.software/ocm/api/utils/runtime" "ocm.software/ocm/api/utils/semverutils" @@ -322,3 +324,8 @@ func NewRedirectedResourceAccess(r cpi.ResourceAccess, bacc cpi.DataAccess) (cpi RedirectedAccessMethod: rm, }, nil } + +// NormalizeVersion replace eventual '+' character according to OCI spec. +func NormalizeVersion(v string) string { + return strings.ReplaceAll(v, "+", genericocireg.META_SEPARATOR) +} diff --git a/pkg/ocm/ocm_test.go b/pkg/ocm/ocm_test.go index aba47149..6be98fce 100644 --- a/pkg/ocm/ocm_test.go +++ b/pkg/ocm/ocm_test.go @@ -26,7 +26,6 @@ import ( "ocm.software/ocm/api/utils/mime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -382,7 +381,6 @@ consumers: utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(v1alpha1.AddToScheme(scheme)) - utilruntime.Must(artifactv1.AddToScheme(scheme)) BeforeEach(func() { bldr = fake.NewClientBuilder() diff --git a/pkg/test/component.go b/pkg/test/component.go new file mode 100644 index 00000000..2adc11b5 --- /dev/null +++ b/pkg/test/component.go @@ -0,0 +1,89 @@ +package test + +import ( + "context" + "strings" + "time" + + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/onsi/gomega" + + "github.com/fluxcd/pkg/runtime/patch" + "github.com/opencontainers/go-digest" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" +) + +type MockComponentOptions struct { + Registry ociartifact.RegistryType + Client client.Client + Recorder record.EventRecorder + Info v1alpha1.ComponentInfo + Repository string +} + +func SetupComponentWithDescriptorList( + ctx context.Context, + name, namespace string, + descriptorListData []byte, + options *MockComponentOptions, +) *v1alpha1.Component { + component := &v1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.ComponentSpec{ + RepositoryRef: v1alpha1.ObjectKey{Name: options.Repository, Namespace: namespace}, + Component: options.Info.Component, + }, + } + Expect(options.Client.Create(ctx, component)).To(Succeed()) + + patchHelper := patch.NewSerialPatcher(component, options.Client) + + repositoryName, err := ociartifact.CreateRepositoryName(options.Repository, name) + Expect(err).ToNot(HaveOccurred()) + + repository, err := options.Registry.NewRepository(ctx, repositoryName) + Expect(err).ToNot(HaveOccurred()) + + manifestDigest, err := repository.PushArtifact(ctx, options.Info.Version, descriptorListData) + Expect(err).ToNot(HaveOccurred()) + + component.Status.OCIArtifact = &v1alpha1.OCIArtifactInfo{ + Repository: repositoryName, + Digest: manifestDigest.String(), + Blob: &v1alpha1.BlobInfo{ + Digest: digest.FromBytes(descriptorListData).String(), + Tag: options.Info.Version, + Size: int64(len(descriptorListData)), + }, + } + + component.Status.Component = options.Info + + Eventually(func(ctx context.Context) error { + status.MarkReady(options.Recorder, component, "applied mock component") + + return status.UpdateStatus(ctx, patchHelper, component, options.Recorder, time.Hour, nil) + }).WithContext(ctx).Should(Succeed()) + + return component +} + +func GenerateComponentName(testName string) string { + replaced := strings.ToLower(strings.ReplaceAll(testName, " ", "-")) + maxLength := 63 // RFC 1123 Label Names + if len(replaced) > maxLength { + return replaced[:maxLength] + } + + return replaced +} diff --git a/pkg/test/namespace.go b/pkg/test/namespace.go new file mode 100644 index 00000000..683926cc --- /dev/null +++ b/pkg/test/namespace.go @@ -0,0 +1,13 @@ +package test + +import "strings" + +func GenerateNamespace(testName string) string { + replaced := strings.ToLower(strings.ReplaceAll(testName, " ", "-")) + maxLength := 63 // RFC 1123 Label Names + if len(replaced) > maxLength { + return replaced[:maxLength] + } + + return replaced +} diff --git a/pkg/test/resource.go b/pkg/test/resource.go new file mode 100644 index 00000000..ccbe1a5a --- /dev/null +++ b/pkg/test/resource.go @@ -0,0 +1,103 @@ +package test + +import ( + "context" + "io" + "time" + + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/onsi/gomega" + + "github.com/fluxcd/pkg/runtime/patch" + "github.com/opencontainers/go-digest" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" +) + +type MockResourceOptions struct { + // option one to create a resource: directly pass the Data + Data io.Reader + // option two to create a resource: pass the path to the Data + DataPath string + + ComponentRef v1alpha1.ObjectKey + + Registry ociartifact.RegistryType + Clnt client.Client + Recorder record.EventRecorder +} + +func SetupMockResourceWithData( + ctx context.Context, + name, namespace string, + options *MockResourceOptions, +) *v1alpha1.Resource { + resource := &v1alpha1.Resource{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: v1alpha1.ResourceSpec{ + Resource: v1alpha1.ResourceID{ + ByReference: v1alpha1.ResourceReference{ + Resource: v1.NewIdentity(name), + }, + }, + ComponentRef: corev1.LocalObjectReference{ + Name: options.ComponentRef.Name, + }, + }, + } + Expect(options.Clnt.Create(ctx, resource)).To(Succeed()) + + patchHelper := patch.NewSerialPatcher(resource, options.Clnt) + + var data []byte + var err error + + if options.Data != nil { + data, err = io.ReadAll(options.Data) + Expect(err).ToNot(HaveOccurred()) + } + + if options.DataPath != "" { + data, err = compression.CreateTGZFromPath(options.DataPath) + Expect(err).ToNot(HaveOccurred()) + } + + version := "dummy" + repositoryName, err := ociartifact.CreateRepositoryName(options.ComponentRef.Name, name) + Expect(err).ToNot(HaveOccurred()) + repository, err := options.Registry.NewRepository(ctx, repositoryName) + Expect(err).ToNot(HaveOccurred()) + + manifestDigest, err := repository.PushArtifact(ctx, version, data) + Expect(err).ToNot(HaveOccurred()) + + resource.Status.OCIArtifact = &v1alpha1.OCIArtifactInfo{ + Repository: repositoryName, + Digest: manifestDigest.String(), + Blob: &v1alpha1.BlobInfo{ + Digest: digest.FromBytes(data).String(), + Tag: version, + Size: int64(len(data)), + }, + } + + Eventually(func(ctx context.Context) error { + status.MarkReady(options.Recorder, resource, "applied mock resource") + + return status.UpdateStatus(ctx, patchHelper, resource, options.Recorder, time.Hour, nil) + }).WithContext(ctx).Should(Succeed()) + + return resource +} diff --git a/pkg/test/util.go b/pkg/test/util.go deleted file mode 100644 index bb0296aa..00000000 --- a/pkg/test/util.go +++ /dev/null @@ -1,233 +0,0 @@ -package test - -import ( - "context" - "fmt" - "io" - "os" - "path/filepath" - "time" - - //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL - . "github.com/onsi/ginkgo/v2" - //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL - . "github.com/onsi/gomega" - //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL - . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - - "github.com/fluxcd/pkg/runtime/patch" - "github.com/mandelsoft/vfs/pkg/memoryfs" - "github.com/mandelsoft/vfs/pkg/vfs" - "github.com/openfluxcd/controller-manager/storage" - "k8s.io/client-go/tools/record" - "ocm.software/ocm/api/utils/tarutils" - "sigs.k8s.io/controller-runtime/pkg/client" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" - - "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" -) - -type MockResourceOptions struct { - BasePath string - - // option one to create a resource: directly pass the Data - Data io.Reader - // option two to create a resource: pass the path to the Data - DataPath string - - ComponentRef v1alpha1.ObjectKey - - Strg *storage.Storage - Clnt client.Client - Recorder record.EventRecorder -} - -func SetupMockResourceWithData( - ctx context.Context, - name, namespace string, - options *MockResourceOptions, -) *v1alpha1.Resource { - res := &v1alpha1.Resource{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - Spec: v1alpha1.ResourceSpec{ - Resource: v1alpha1.ResourceID{ - ByReference: v1alpha1.ResourceReference{ - Resource: v1.NewIdentity(name), - }, - }, - ComponentRef: corev1.LocalObjectReference{ - Name: options.ComponentRef.Name, - }, - }, - } - Expect(options.Clnt.Create(ctx, res)).To(Succeed()) - - patchHelper := patch.NewSerialPatcher(res, options.Clnt) - - path := options.BasePath - - err := options.Strg.ReconcileArtifact( - ctx, - res, - name, - path, - fmt.Sprintf("%s.tar.gz", name), - func(artifact *artifactv1.Artifact, _ string) error { - // Archive directory to storage - if options.Data != nil { - if err := options.Strg.Copy(artifact, options.Data); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } - } - if options.DataPath != "" { - abs, err := filepath.Abs(options.DataPath) - if err != nil { - return fmt.Errorf("unable to get absolute path: %w", err) - } - if err := options.Strg.Archive(artifact, abs, nil); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } - } - - res.Status.ArtifactRef = corev1.LocalObjectReference{ - Name: artifact.Name, - } - - return nil - }) - Expect(err).ToNot(HaveOccurred()) - - art := &artifactv1.Artifact{} - art.Name = res.Status.ArtifactRef.Name - art.Namespace = res.Namespace - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - - Eventually(func(ctx context.Context) error { - status.MarkReady(options.Recorder, res, "applied mock resource") - - return status.UpdateStatus(ctx, patchHelper, res, options.Recorder, time.Hour, nil) - }).WithContext(ctx).Should(Succeed()) - - return res -} - -type MockComponentOptions struct { - BasePath string - Strg *storage.Storage - Client client.Client - Recorder record.EventRecorder - Info v1alpha1.ComponentInfo - Repository string -} - -func SetupComponentWithDescriptorList( - ctx context.Context, - name, namespace string, - descriptorListData []byte, - options *MockComponentOptions, -) *v1alpha1.Component { - dir := filepath.Join(options.BasePath, "descriptor") - CreateTGZ(dir, map[string][]byte{ - v1alpha1.OCMComponentDescriptorList: descriptorListData, - }) - component := &v1alpha1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: v1alpha1.ComponentSpec{ - RepositoryRef: v1alpha1.ObjectKey{Name: options.Repository, Namespace: namespace}, - Component: options.Info.Component, - }, - Status: v1alpha1.ComponentStatus{ - ArtifactRef: corev1.LocalObjectReference{ - Name: name, - }, - Component: options.Info, - }, - } - Expect(options.Client.Create(ctx, component)).To(Succeed()) - - patchHelper := patch.NewSerialPatcher(component, options.Client) - - Expect(options.Strg.ReconcileArtifact( - ctx, - component, - name, - options.BasePath, - fmt.Sprintf("%s.tar.gz", name), - func(artifact *artifactv1.Artifact, _ string) error { - if err := options.Strg.Archive(artifact, dir, nil); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } - - component.Status.ArtifactRef = corev1.LocalObjectReference{ - Name: artifact.Name, - } - component.Status.Component = options.Info - - return nil - }), - ).To(Succeed()) - - art := &artifactv1.Artifact{} - art.Name = component.Status.ArtifactRef.Name - art.Namespace = component.Namespace - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - - Eventually(func(ctx context.Context) error { - status.MarkReady(options.Recorder, component, "applied mock component") - - return status.UpdateStatus(ctx, patchHelper, component, options.Recorder, time.Hour, nil) - }).WithContext(ctx).Should(Succeed()) - - return component -} - -func VerifyArtifact(strg *storage.Storage, art *artifactv1.Artifact, files map[string]func(data []byte)) { - GinkgoHelper() - - art = art.DeepCopy() - - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - - localized := strg.LocalPath(art) - Expect(localized).To(BeAnExistingFile()) - - memFs := vfs.New(memoryfs.New()) - localizedArchiveData, err := os.OpenFile(localized, os.O_RDONLY, 0o600) - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(func() { - Expect(localizedArchiveData.Close()).To(Succeed()) - }) - Expect(tarutils.UnzipTarToFs(memFs, localizedArchiveData)).To(Succeed()) - - for fileName, assert := range files { - data, err := memFs.ReadFile(fileName) - Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("expected %s to be present and be readable", fileName)) - assert(data) - } -} - -func CreateTGZ(tgzPackageDir string, data map[string][]byte) { - GinkgoHelper() - Expect(os.Mkdir(tgzPackageDir, os.ModePerm|os.ModeDir)).To(Succeed()) - for path, data := range data { - path = filepath.Join(tgzPackageDir, path) - writer, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.ModePerm) - Expect(err).ToNot(HaveOccurred()) - defer func() { - Expect(writer.Close()).To(Succeed()) - }() - _, err = writer.Write(data) - Expect(err).ToNot(HaveOccurred()) - } -} diff --git a/pkg/test/zot-registry.go b/pkg/test/zot-registry.go new file mode 100644 index 00000000..dedbfc72 --- /dev/null +++ b/pkg/test/zot-registry.go @@ -0,0 +1,60 @@ +package test + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "time" + + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/onsi/gomega" + + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ociartifact" +) + +const ( + timeout = 30 * time.Second +) + +func SetupRegistry(binPath, rootDir, address, port string) (*exec.Cmd, *ociartifact.Registry) { + config := []byte(fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},"http":{"address":"%s","port": "%s"}}`, rootDir, address, port)) + configFile := filepath.Join(rootDir, "config.json") + err := os.WriteFile(configFile, config, 0o600) + Expect(err).NotTo(HaveOccurred()) + + // Start zot-registry + zotCmd := exec.Command(binPath, "serve", configFile) + err = zotCmd.Start() + Expect(err).NotTo(HaveOccurred(), "Failed to start Zot") + + // Wait for Zot to be ready + Eventually(func() error { + url := fmt.Sprintf("http://%s/v2/", net.JoinHostPort(address, port)) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("could not connect to Zot") + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil + }, timeout).Should(Succeed(), "Zot registry did not start in time") + + registry, err := ociartifact.NewRegistry(fmt.Sprintf("%s:%s", address, port)) + Expect(err).NotTo(HaveOccurred()) + registry.PlainHTTP = true + + return zotCmd, registry +} diff --git a/test/utils/utils.go b/test/utils/utils.go index bcdb0d09..8014f0db 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -29,6 +29,7 @@ import ( . "github.com/onsi/ginkgo/v2" //nolint:golint,revive,stylecheck // ginkgo... + // TODO: Replacement required! "github.com/openfluxcd/artifact/test/utils" )