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..2d0f9556 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. @@ -178,6 +182,7 @@ 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 +218,12 @@ 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. + wget "https://github.com/project-zot/zot/releases/download/$(ZOT_VERSION)/zot-$(OS)-$(ARCH)-minimal" \ + -O $(LOCALBIN)/zot-registry \ + && chmod u+x $(LOCALBIN)/zot-registry + .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) diff --git a/PROJECT b/PROJECT index c8ec47cf..f172fb06 100644 --- a/PROJECT +++ b/PROJECT @@ -71,4 +71,13 @@ resources: kind: Replication path: github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1 version: v1alpha1 -version: "3" \ No newline at end of file +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: ocm.software + group: delivery + kind: Snapshot + path: github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index 7e67d0f8..861c058f 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -113,3 +113,15 @@ 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"` +} diff --git a/api/v1alpha1/component_types.go b/api/v1alpha1/component_types.go index c005248a..3fa2d179 100644 --- a/api/v1alpha1/component_types.go +++ b/api/v1alpha1/component_types.go @@ -100,11 +100,11 @@ type ComponentStatus struct { // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` - // ArtifactRef references the generated artifact containing a list of + // SnapshotRef references the generated snapshot 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"` + SnapshotRef corev1.LocalObjectReference `json:"snapshotRef,omitempty"` // Component specifies the concrete version of the component that was // fetched after based on the semver constraints during the last successful @@ -180,6 +180,10 @@ func (in *Component) GetVerifications() []Verification { return in.Spec.Verify } +func (in *Component) GetSnapshotName() string { + return in.Status.SnapshotRef.Name +} + // +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..fbd031f5 100644 --- a/api/v1alpha1/condition_types.go +++ b/api/v1alpha1/condition_types.go @@ -63,18 +63,54 @@ const ( // ReconcileArtifactFailedReason is used when we fail in creating an Artifact. ReconcileArtifactFailedReason = "ReconcileArtifactFailed" + // MarshalFailedReason is used when we fail to marshal a struct. + MarshalFailedReason = "MarshalFailed" + + // CreateOCIRepositoryNameFailedReason is used when we fail to create an OCI repository name. + CreateOCIRepositoryNameFailedReason = "CreateOCIRepositoryNameFailed" + + // CreateOCIRepositoryFailedReason is used when we fail to create a OCI repository. + CreateOCIRepositoryFailedReason = "CreateOCIRepositoryFailed" + + // CreateSnapshotFailedReason is used when we fail to create a snapshot. + CreateSnapshotFailedReason = "CreateSnapshotFailed" + // GetArtifactFailedReason is used when we fail in getting an Artifact. GetArtifactFailedReason = "GetArtifactFailed" + // GetSnapshotFailedReason is used when we fail in getting a Snapshot. + GetSnapshotFailedReason = "GetSnapshotFailed" + // ResolveResourceFailedReason is used when we fail in resolving a resource. ResolveResourceFailedReason = "ResolveResourceFailed" // GetResourceAccessFailedReason is used when we fail in getting a resource access(es). GetResourceAccessFailedReason = "GetResourceAccessFailed" + // 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" + + // PushSnapshotFailedReason is used when we fail to push a snapshot. + PushSnapshotFailedReason = "PushSnapshotFailed" + + // FetchSnapshotFailedReason is used when we fail to fetch a snapshot. + FetchSnapshotFailedReason = "FetchSnapshotFailed" + + // DeleteSnapshotFailedReason is used when we fail to delete a snapshot. + DeleteSnapshotFailedReason = "DeleteSnapshotFailed" + // GetComponentForArtifactFailedReason is used when we fail in getting a component for an artifact. GetComponentForArtifactFailedReason = "GetComponentForArtifactFailed" + // GetComponentForSnapshotFailedReason is used when we fail in getting a component for a snapshot. + GetComponentForSnapshotFailedReason = "GetComponentForSnapshotFailed" + // StatusSetFailedReason is used when we fail to set the component status. StatusSetFailedReason = "StatusSetFailed" diff --git a/api/v1alpha1/configuredresource_types.go b/api/v1alpha1/configuredresource_types.go index d44807b5..37f6b077 100644 --- a/api/v1alpha1/configuredresource_types.go +++ b/api/v1alpha1/configuredresource_types.go @@ -21,6 +21,7 @@ import ( "github.com/fluxcd/pkg/apis/meta" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -92,8 +93,8 @@ type ConfiguredResourceStatus struct { // 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. - ArtifactRef *ObjectKey `json:"artifactRef,omitempty"` + // It is filled once the Snapshot is created and the configuration completed. + SnapshotRef corev1.LocalObjectReference `json:"snapshotRef,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 @@ -113,6 +114,10 @@ type ConfiguredResource struct { Status ConfiguredResourceStatus `json:"status,omitempty"` } +func (in *ConfiguredResource) GetSnapshotName() string { + return in.Status.SnapshotRef.Name +} + // +kubebuilder:object:root=true // ConfiguredResourceList contains a list of ConfiguredResource. diff --git a/api/v1alpha1/constants.go b/api/v1alpha1/constants.go index 45891aa8..d50a9603 100644 --- a/api/v1alpha1/constants.go +++ b/api/v1alpha1/constants.go @@ -37,11 +37,16 @@ const ( // Finalizers for controllers. const ( - // ArtifactFinalizer is the finalizer that is added to artifacts created by the ocm controllers. - ArtifactFinalizer = "finalizers.ocm.software/artifact" + // SnapshotFinalizer is the finalizter that is added to snapshot created by the ocm controllers. + SnapshotFinalizer = "finalizers.ocm.software/snapshot" ) // External CRDs. const ( ArtifactCrd = "https://github.com/openfluxcd/artifact/releases/download/v0.1.1/openfluxcd.ocm.software_artifacts.yaml" ) + +// OCI Schema. +const ( + OCISchemaVersion = 2 +) diff --git a/api/v1alpha1/localizedresource_types.go b/api/v1alpha1/localizedresource_types.go index 395d60d4..857859e0 100644 --- a/api/v1alpha1/localizedresource_types.go +++ b/api/v1alpha1/localizedresource_types.go @@ -3,6 +3,7 @@ package v1alpha1 import ( "fmt" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -73,6 +74,10 @@ func (in *LocalizedResource) SetTarget(v *ConfigurationReference) { v.DeepCopyInto(&in.Spec.Target) } +func (in *LocalizedResource) GetSnapshotName() string { + return in.Status.SnapshotRef.Name +} + type LocalizedResourceSpec struct { // Target that is to be localized Target ConfigurationReference `json:"target"` @@ -91,8 +96,8 @@ 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 LocalizedResource reports an SnapshotRef which contains the content of the Resource after Localization + SnapshotRef corev1.LocalObjectReference `json:"snapshotRef,omitempty"` // The LocalizedResource reports a ConfiguredResourceRef which contains a reference to the ConfiguredResource // that is responsible for generating the ArtifactRef. diff --git a/api/v1alpha1/resource_types.go b/api/v1alpha1/resource_types.go index b8ec88a1..5c17f9bd 100644 --- a/api/v1alpha1/resource_types.go +++ b/api/v1alpha1/resource_types.go @@ -62,10 +62,11 @@ type ResourceStatus struct { // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` - // ArtifactRef points to the Artifact which represents the output of the - // last successful Resource sync. + // SnapshotRef references the generated snapshot 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"` + SnapshotRef corev1.LocalObjectReference `json:"snapshotRef,omitempty"` // +optional Resource *ResourceInfo `json:"resource,omitempty"` @@ -131,6 +132,10 @@ func (in *Resource) GetEffectiveOCMConfig() []OCMConfiguration { return in.Status.EffectiveOCMConfig } +func (in *Resource) GetSnapshotName() string { + return in.Status.SnapshotRef.Name +} + // +kubebuilder:object:root=true // ResourceList contains a list of Resource. diff --git a/api/v1alpha1/snapshot_types.go b/api/v1alpha1/snapshot_types.go new file mode 100644 index 00000000..1ae3a25f --- /dev/null +++ b/api/v1alpha1/snapshot_types.go @@ -0,0 +1,108 @@ +package v1alpha1 + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SnapshotWriter defines any object which produces a snapshot +// +k8s:deepcopy-gen=false +type SnapshotWriter interface { + client.Object + GetSnapshotName() string + GetKind() string + GetNamespace() string + GetName() string +} + +// SnapshotSpec defines the desired state of Snapshot. +type SnapshotSpec 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"` + + // Suspend stops all operations on this object. + // +optional + Suspend bool `json:"suspend,omitempty"` +} + +// SnapshotStatus defines the observed state of Snapshot. +type SnapshotStatus struct { + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // Digest is calculated by the caching layer. + // +optional + LastReconciledDigest string `json:"digest,omitempty"` + + // Tag defines the explicit tag that was used to create the related snapshot and cache entry. + // +optional + LastReconciledTag string `json:"tag,omitempty"` + + // ObservedGeneration is the last reconciled generation. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +func (in *Snapshot) GetVID() map[string]string { + metadata := make(map[string]string) + metadata[GroupVersion.Group+"/snapshot_digest"] = in.Status.LastReconciledDigest + + return metadata +} + +func (in *Snapshot) SetObservedGeneration(v int64) { + in.Status.ObservedGeneration = v +} + +// GetDigest returns the last reconciled digest for the snapshot. +func (in Snapshot) GetDigest() string { + return in.Spec.Digest +} + +// GetConditions returns the status conditions of the object. +func (in Snapshot) GetConditions() []metav1.Condition { + return in.Status.Conditions +} + +// SetConditions sets the status conditions on the object. +func (in *Snapshot) SetConditions(conditions []metav1.Condition) { + in.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=snap +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" + +// Snapshot is the Schema for the snapshots API. +type Snapshot struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SnapshotSpec `json:"spec,omitempty"` + Status SnapshotStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// SnapshotList contains a list of Snapshot. +type SnapshotList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Snapshot `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Snapshot{}, &SnapshotList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d7b6a68d..6b2ac782 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,7 +142,7 @@ func (in *ComponentStatus) DeepCopyInto(out *ComponentStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - out.ArtifactRef = in.ArtifactRef + out.SnapshotRef = in.SnapshotRef in.Component.DeepCopyInto(&out.Component) if in.EffectiveOCMConfig != nil { in, out := &in.EffectiveOCMConfig, &out.EffectiveOCMConfig @@ -379,11 +394,7 @@ 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 - } + out.SnapshotRef = in.SnapshotRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfiguredResourceStatus. @@ -710,11 +721,7 @@ 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 - } + out.SnapshotRef = in.SnapshotRef if in.ConfiguredResourceRef != nil { in, out := &in.ConfiguredResourceRef, &out.ConfiguredResourceRef *out = new(ObjectKey) @@ -1247,7 +1254,7 @@ func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - out.ArtifactRef = in.ArtifactRef + out.SnapshotRef = in.SnapshotRef if in.Resource != nil { in, out := &in.Resource, &out.Resource *out = new(ResourceInfo) @@ -1270,6 +1277,103 @@ func (in *ResourceStatus) DeepCopy() *ResourceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Snapshot) DeepCopyInto(out *Snapshot) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snapshot. +func (in *Snapshot) DeepCopy() *Snapshot { + if in == nil { + return nil + } + out := new(Snapshot) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Snapshot) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SnapshotList) DeepCopyInto(out *SnapshotList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Snapshot, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotList. +func (in *SnapshotList) DeepCopy() *SnapshotList { + if in == nil { + return nil + } + out := new(SnapshotList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SnapshotList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SnapshotSpec) DeepCopyInto(out *SnapshotSpec) { + *out = *in + out.Blob = in.Blob +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotSpec. +func (in *SnapshotSpec) DeepCopy() *SnapshotSpec { + if in == nil { + return nil + } + out := new(SnapshotSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SnapshotStatus) DeepCopyInto(out *SnapshotStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotStatus. +func (in *SnapshotStatus) DeepCopy() *SnapshotStatus { + if in == nil { + return nil + } + out := new(SnapshotStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TransferStatus) DeepCopyInto(out *TransferStatus) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index cff8706f..037c7511 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,18 +17,20 @@ limitations under the License. package main import ( + // +kubebuilder:scaffold:imports "context" "crypto/tls" + "crypto/x509" + "errors" "flag" + "net/http" "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" @@ -49,11 +51,11 @@ 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/internal/controller/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) -// +kubebuilder:scaffold:imports - var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -70,17 +72,18 @@ func init() { //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 + storagePath string + storageAddr string + storageAdvAddr string + eventsAddr string + registryAddr string + rootCA string + registryInsecureSkipVerify bool ) 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") @@ -96,6 +99,9 @@ func main() { 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.") + flag.StringVar(&rootCA, "rootCA", "", "root CA certificate required to establish https connection to the registry.") + flag.BoolVar(®istryInsecureSkipVerify, "registry-insecure-skip-verify", true, "Skip verification of the certificate that the registry is using.") opts := zap.Options{ Development: true, @@ -171,10 +177,26 @@ func main() { os.Exit(1) } - storage, artifactServer, err := server.NewArtifactStore(mgr.GetClient(), mgr.GetScheme(), - storagePath, storageAddr, storageAdvAddr, artifactRetentionTTL, artifactRetentionRecords) + registry, err := snapshotRegistry.NewRegistry(registryAddr) + registry.PlainHTTP = registryInsecureSkipVerify if err != nil { - setupLog.Error(err, "unable to initialize storage") + setupLog.Error(err, "unable to initialize registry object") + os.Exit(1) + } + + // If HTTPS is enabled, the root CA certificate must be configured. + if !registryInsecureSkipVerify { + httpClient, err := getHTTPClientWithTLS(rootCA) + if err != nil { + setupLog.Error(err, "unable to create http client with TLS configuration") + os.Exit(1) + } + + registry.Client = httpClient + } + + if err := registry.Ping(ctx); err != nil { + setupLog.Error(err, "unable to ping OCI registry") os.Exit(1) } @@ -184,7 +206,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 +218,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 +230,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 +243,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 +260,18 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Replication") os.Exit(1) } + if err = (&snapshot.Reconciler{ + BaseReconciler: &ocm.BaseReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: eventsRecorder, + }, + Registry: registry, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Snapshot") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -254,10 +288,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") @@ -266,3 +296,28 @@ func main() { os.Exit(1) } } + +func getHTTPClientWithTLS(rootCAFile string) (*http.Client, error) { + if rootCAFile == "" { + return nil, errors.New("path to rootCA file is empty") + } + + var c []byte + var err error + if c, err = os.ReadFile(rootCAFile); err != nil { + return nil, err + } + + rootCAs := x509.NewCertPool() + if ok := rootCAs.AppendCertsFromPEM(c); !ok { + return nil, errors.New("failed to append root CA certificate to pool") + } + + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + }, + }, + }, nil +} diff --git a/config/crd/bases/delivery.ocm.software_components.yaml b/config/crd/bases/delivery.ocm.software_components.yaml index 5325f24f..5a092f95 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,23 @@ spec: object. format: int64 type: integer + snapshotRef: + description: |- + SnapshotRef references the generated snapshot 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 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..a91dbddf 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 @@ -177,6 +164,23 @@ spec: observedGeneration: format: int64 type: integer + snapshotRef: + description: |- + The configuration reconcile loop generates an artifact, which contains the + ConfiguredResourceSpec.Target ConfigurationReference after configuration. + It is filled once the Snapshot is created and the configuration completed. + 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 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..a203ab9f 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 @@ -196,6 +185,21 @@ spec: observedGeneration: format: int64 type: integer + snapshotRef: + description: The LocalizedResource reports an SnapshotRef which contains + the content of the Resource after Localization + 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 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..5a0dbc1f 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: @@ -300,6 +284,23 @@ spec: - name - type type: object + snapshotRef: + description: |- + SnapshotRef references the generated snapshot 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 type: object required: - spec diff --git a/config/crd/bases/delivery.ocm.software_snapshots.yaml b/config/crd/bases/delivery.ocm.software_snapshots.yaml new file mode 100644 index 00000000..6f78e919 --- /dev/null +++ b/config/crd/bases/delivery.ocm.software_snapshots.yaml @@ -0,0 +1,161 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.0 + name: snapshots.delivery.ocm.software +spec: + group: delivery.ocm.software + names: + kind: Snapshot + listKind: SnapshotList + plural: snapshots + shortNames: + - snap + singular: snapshot + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Snapshot is the Schema for the snapshots API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: SnapshotSpec defines the desired state of Snapshot. + 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 + suspend: + description: Suspend stops all operations on this object. + type: boolean + required: + - blob + - digest + - repository + type: object + status: + description: SnapshotStatus defines the observed state of Snapshot. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + digest: + description: Digest is calculated by the caching layer. + type: string + observedGeneration: + description: ObservedGeneration is the last reconciled generation. + format: int64 + type: integer + tag: + description: Tag defines the explicit tag that was used to create + the related snapshot and cache entry. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 73f37caa..0f5cad00 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -10,6 +10,7 @@ resources: - bases/delivery.ocm.software_localizedresources.yaml - bases/delivery.ocm.software_configuredresources.yaml - bases/delivery.ocm.software_resourceconfigs.yaml +- bases/delivery.ocm.software_snapshots.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/default-zot-https/kustomization.yaml b/config/default-zot-https/kustomization.yaml index a0a31ea2..a11d38be 100644 --- a/config/default-zot-https/kustomization.yaml +++ b/config/default-zot-https/kustomization.yaml @@ -4,5 +4,6 @@ resources: - certificate.yaml - ../default patches: -- path: configmap-patch.yaml -- path: deployment-patch.yaml +- path: zot-configmap-patch.yaml +- path: zot-deployment-patch.yaml +- path: mgr-deployment-patch.yaml diff --git a/config/default-zot-https/mgr-deployment-patch.yaml b/config/default-zot-https/mgr-deployment-patch.yaml new file mode 100644 index 00000000..b7d42b11 --- /dev/null +++ b/config/default-zot-https/mgr-deployment-patch.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager +spec: + template: + spec: + containers: + - name: manager + args: + - --health-probe-bind-address=:8081 + - --storage-adv-addr=ocm-k8s-toolkit-artifact-service.ocm-k8s-toolkit-system.svc.cluster.local. + - --zap-log-level=4 + - --registry-insecure-skip-verify=false + - --rootCA=/reg/tls/tls.crt + volumeMounts: + - name: reg-tls + mountPath: /reg/tls + readOnly: true + volumes: + - name: reg-tls + secret: + secretName: ocm-k8s-toolkit-registry-tls-certs +# Registry is accessible in-cluster under: +# https://ocm-k8s-toolkit-zot-registry.ocm-k8s-toolkit-system.svc.cluster.local:5000/v2/_catalog diff --git a/config/default-zot-https/configmap-patch.yaml b/config/default-zot-https/zot-configmap-patch.yaml similarity index 100% rename from config/default-zot-https/configmap-patch.yaml rename to config/default-zot-https/zot-configmap-patch.yaml diff --git a/config/default-zot-https/deployment-patch.yaml b/config/default-zot-https/zot-deployment-patch.yaml similarity index 100% rename from config/default-zot-https/deployment-patch.yaml rename to config/default-zot-https/zot-deployment-patch.yaml diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index c78c5884..8a396089 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -11,7 +11,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager - namespace: system labels: control-plane: controller-manager app.kubernetes.io/name: ocm-k8s-toolkit diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 087bea97..9e818c94 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -13,6 +13,8 @@ resources: # default, aiding admins in cluster management. Those roles are # not used by the Project itself. You can comment the following lines # if you do not want those helpers be installed with your Project. +- snapshot_editor_role.yaml +- snapshot_viewer_role.yaml - replication_editor_role.yaml - replication_viewer_role.yaml - configuredresource_editor_role.yaml @@ -22,4 +24,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..699a25d3 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -36,6 +36,7 @@ rules: - ocmrepositories - replications - resources + - snapshots verbs: - create - delete @@ -48,13 +49,8 @@ rules: - delivery.ocm.software resources: - components/finalizers - - configuredresources/finalizers - - localizedresources/finalizers - - ocmrepositories/finalizers - - replications/finalizers - - resources/finalizers verbs: - - update + - updat - apiGroups: - delivery.ocm.software resources: @@ -64,10 +60,21 @@ rules: - ocmrepositories/status - replications/status - resources/status + - snapshots/status verbs: - get - patch - update +- apiGroups: + - delivery.ocm.software + resources: + - configuredresources/finalizers + - localizedresources/finalizers + - ocmrepositories/finalizers + - replications/finalizers + - snapshots/finalizers + verbs: + - update - apiGroups: - delivery.ocm.software resources: diff --git a/config/rbac/snapshot_editor_role.yaml b/config/rbac/snapshot_editor_role.yaml new file mode 100644 index 00000000..3c99c21a --- /dev/null +++ b/config/rbac/snapshot_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit snapshots. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: ocm-k8s-toolkit + app.kubernetes.io/managed-by: kustomize + name: snapshot-editor-role +rules: +- apiGroups: + - delivery.ocm.software + resources: + - snapshots + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - delivery.ocm.software + resources: + - snapshots/status + verbs: + - get diff --git a/config/rbac/snapshot_viewer_role.yaml b/config/rbac/snapshot_viewer_role.yaml new file mode 100644 index 00000000..906f8d6b --- /dev/null +++ b/config/rbac/snapshot_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view snapshots. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: ocm-k8s-toolkit + app.kubernetes.io/managed-by: kustomize + name: snapshot-viewer-role +rules: +- apiGroups: + - delivery.ocm.software + resources: + - snapshots + verbs: + - get + - list + - watch +- apiGroups: + - delivery.ocm.software + resources: + - snapshots/status + verbs: + - get diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 520434b7..cf502cfa 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -4,4 +4,5 @@ resources: - delivery_v1alpha1_component.yaml - delivery_v1alpha1_resource.yaml - delivery_v1alpha1_replication.yaml +- delivery_v1alpha1_snapshot.yaml # +kubebuilder:scaffold:manifestskustomizesamples 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/go.mod b/go.mod index c91cc8d1..3a94a8ba 100644 --- a/go.mod +++ b/go.mod @@ -15,14 +15,14 @@ require ( github.com/fluxcd/pkg/runtime v0.53.0 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.1 @@ -30,6 +30,7 @@ require ( k8s.io/apimachinery v0.32.1 k8s.io/client-go v0.32.1 ocm.software/ocm v0.19.1 + oras.land/oras-go/v2 v2.5.0 sigs.k8s.io/controller-runtime v0.20.1 sigs.k8s.io/yaml v1.4.0 ) @@ -142,8 +143,6 @@ require ( 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 @@ -151,9 +150,6 @@ require ( github.com/ghodss/yaml v1.0.0 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-errors/errors v1.5.1 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.1 // indirect - github.com/go-git/go-git/v5 v5.13.1 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -195,7 +191,7 @@ require ( github.com/gorilla/mux v1.8.1 // 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/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect @@ -206,19 +202,18 @@ require ( github.com/huandu/xstrings v1.5.0 // indirect github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // 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.7 // 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 @@ -245,8 +240,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 @@ -266,6 +259,7 @@ require ( github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect github.com/segmentio/ksuid v1.0.4 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sigstore/cosign/v2 v2.4.1 // indirect @@ -303,21 +297,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 @@ -342,7 +334,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect helm.sh/helm/v3 v3.16.3 // indirect diff --git a/go.sum b/go.sum index 6174c732..0e914f13 100644 --- a/go.sum +++ b/go.sum @@ -383,18 +383,12 @@ github.com/fluxcd/pkg/apis/event v0.16.0 h1:ffKc/3erowPnh72lFszz7sPQhLZ7bhqNrq+p 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.0 h1:IgDSLVQtgyXvZWIeDy1I+0EgzgUHNwEegSyI5UMObhw= github.com/fluxcd/pkg/runtime v0.53.0/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.41.1 h1:VW87zsLYAKUvCxJhuEH7VzxVh3SxaU+PyApCT6gKjTk= github.com/fluxcd/pkg/ssa v0.41.1/go.mod h1:7cbyLHqFd5FpcKvhxbHG3DkMm3cZteW45Mi78B0hg8g= 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= @@ -420,12 +414,6 @@ github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyN github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= -github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= -github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= -github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= @@ -580,8 +568,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.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -630,8 +618,6 @@ github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= @@ -667,9 +653,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= @@ -729,6 +712,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= @@ -801,8 +786,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= @@ -810,8 +793,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= @@ -1034,15 +1015,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= @@ -1065,8 +1039,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= @@ -1085,8 +1059,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= @@ -1277,6 +1251,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -1295,8 +1270,6 @@ gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1338,6 +1311,8 @@ ocm.software/ocm v0.19.1 h1:sWcQB+G9zcURqZfKvTnAfeA+rcDwlbI222o/fPkm6ls= ocm.software/ocm v0.19.1/go.mod h1:JCGMa/y8PPXvRhD+8SnnNHco4aAXMaXxiJKe8gyRHTQ= oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk= oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= sigs.k8s.io/controller-runtime v0.20.1 h1:JbGMAG/X94NeM3xvjenVUaBjy6Ui4Ogd/J5ZtjZnHaE= sigs.k8s.io/controller-runtime v0.20.1/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= diff --git a/internal/controller/component/component_controller.go b/internal/controller/component/component_controller.go index 70c179a4..ea74c392 100644 --- a/internal/controller/component/component_controller.go +++ b/internal/controller/component/component_controller.go @@ -20,39 +20,37 @@ 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/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/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" ) // Reconciler reconciles a Component object. type Reconciler struct { *ocm.BaseReconciler - Storage *storage.Storage + Registry snapshot.RegistryType } var _ ocm.Reconciler = (*Reconciler)(nil) @@ -60,22 +58,24 @@ var _ ocm.Reconciler = (*Reconciler)(nil) // SetupWithManager sets up the controller with the Manager. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). + // TODO: Check if we should watch for the snapshots that are created by this controller For(&v1alpha1.Component{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Complete(r) } // +kubebuilder:rbac:groups=delivery.ocm.software,resources=components,verbs=get;list;watch;create;update;patch;delete // +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=delivery.ocm.software,resources=components/finalizers,verbs=updat // +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 +// TODO: Remove +// +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 + // Reconcile the component object. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, retErr error) { component := &v1alpha1.Component{} @@ -137,6 +137,10 @@ func (r *Reconciler) reconcile(ctx context.Context, component *v1alpha1.Componen 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") @@ -167,6 +171,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) @@ -238,26 +243,68 @@ func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context return ctrl.Result{}, err } - err = r.Storage.ReconcileStorage(ctx, component) + // Store descriptors and create snapshot + // TODO: Can I check beforehand if the CD is already downloaded and in the OCI Registry (cached)? + // Compare digest/hash from manifest of the CD from the source storage + + logger.Info("pushing descriptors to storage") + ociRepositoryName, err := snapshot.CreateRepositoryName(component.Spec.RepositoryRef.Name, component.GetName()) + if err != nil { + status.MarkNotReady(r.EventRecorder, component, v1alpha1.CreateOCIRepositoryNameFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + ociRepository, err := r.Registry.NewRepository(ctx, ociRepositoryName) if err != nil { - status.MarkNotReady(r.EventRecorder, component, v1alpha1.StorageReconcileFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, component, v1alpha1.CreateOCIRepositoryFailedReason, 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) + descriptorsBytes, err := yaml.Marshal(descriptors) + if err != nil { + status.MarkNotReady(r.EventRecorder, component, v1alpha1.MarshalFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + manifestDigest, err := ociRepository.PushSnapshot(ctx, version, descriptorsBytes) if err != nil { status.MarkNotReady(r.EventRecorder, component, v1alpha1.ReconcileArtifactFailedReason, err.Error()) return ctrl.Result{}, err } - // Update status - r.setComponentStatus(component, configs, v1alpha1.ComponentInfo{ + logger.Info("creating snapshot") + snapshotCR := snapshot.Create(component, ociRepositoryName, manifestDigest.String(), version, digest.FromBytes(descriptorsBytes).String(), int64(len(descriptorsBytes))) + + if _, err = controllerutil.CreateOrUpdate(ctx, r.GetClient(), snapshotCR, func() error { + if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { + if err := controllerutil.SetControllerReference(component, snapshotCR, r.GetScheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + } + + component.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), + } + + return nil + }); err != nil { + status.MarkNotReady(r.EventRecorder, component, v1alpha1.CreateSnapshotFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + logger.Info("updating status") + 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 +403,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..3b5bda50 100644 --- a/internal/controller/component/component_controller_test.go +++ b/internal/controller/component/component_controller_test.go @@ -19,36 +19,34 @@ package component import ( "context" "fmt" - "net/http" + "io" "os" "time" . "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" + "ocm.software/ocm/api/utils/accessobj" + "sigs.k8s.io/yaml" "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/types" - "ocm.software/ocm/api/ocm/extensions/repositories/ctf" - "ocm.software/ocm/api/utils/accessio" - "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" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" 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/snapshot" ) const ( @@ -142,37 +140,30 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).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("checking that the component has been reconciled successfully") + Eventually(komega.Object(component), "5m").Should( + HaveField("Status.ObservedGeneration", Equal(int64(1)))) - By("check if the component descriptor list can be retrieved from the artifact server") - r := Must(http.Get(artifact.Spec.URL)) + By("checking that the snapshot has been created successfully") + Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) - tmpdir := Must(os.MkdirTemp("/tmp", "descriptors-")) - DeferCleanup(func() error { - return os.RemoveAll(tmpdir) - }) - MustBeSuccessful(tar.Untar(r.Body, tmpdir)) + By("checking that the snapshot contains the correct content") + snapshotRepository := Must(registry.NewRepository(ctx, snapshotComponent.Spec.Repository)) + snapshotComponentContentReader := Must(snapshotRepository.FetchSnapshot(ctx, snapshotComponent.GetDigest())) + snapshotComponentContent := Must(io.ReadAll(snapshotComponentContentReader)) + snapshotDescriptors := &ocm.Descriptors{} + MustBeSuccessful(yaml.Unmarshal(snapshotComponentContent, snapshotDescriptors)) 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)) + expectedDescriptors := Must(ocm.ListComponentDescriptors(ctx, cv, repo)) - data := Must(os.ReadFile(filepath.Join(tmpdir, v1alpha1.OCMComponentDescriptorList))) - descs := &ocm.Descriptors{} - MustBeSuccessful(yaml.Unmarshal(data, descs)) - Expect(descs).To(YAMLEqual(expecteddescs)) + Expect(snapshotDescriptors).To(YAMLEqual(expectedDescriptors)) }) It("does not reconcile when the repository is not ready", func() { @@ -198,10 +189,13 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + }) - By("check that no artifact has been created") + By("check that no snapshot has been created") Eventually(komega.Object(component), "15s").Should( - HaveField("Status.ArtifactRef.Name", BeEmpty())) + HaveField("Status.SnapshotRef.Name", BeEmpty())) }) It("grabs the new version when it becomes available", func() { @@ -223,11 +217,17 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + }) - By("check that artifact has been created successfully") + By("checking that the component has been reconciled successfully") + Eventually(komega.Object(component), "5m").Should( + HaveField("Status.ObservedGeneration", Equal(int64(1)))) - Eventually(komega.Object(component), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + By("checking that the snapshot has been created successfully") + Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) Expect(component.Status.Component.Version).To(Equal(Version1)) @@ -280,10 +280,17 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + }) - By("check that artifact has been created successfully") + By("checking that the component has been reconciled successfully") + Eventually(komega.Object(component), "5m").Should( + HaveField("Status.ObservedGeneration", Equal(int64(1)))) - Eventually(komega.Object(component), "15s").Should(HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + By("checking that the snapshot has been created successfully") + Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) 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")) @@ -329,9 +336,17 @@ var _ = Describe("Component Controller", func() { }, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + }) - By("check that artifact has been created successfully") - Eventually(komega.Object(component), "15s").Should(HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + By("checking that the component has been reconciled successfully") + Eventually(komega.Object(component), "5m").Should( + HaveField("Status.ObservedGeneration", Equal(int64(1)))) + + By("checking that the snapshot has been created successfully") + Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) 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")) @@ -375,11 +390,17 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + }) - By("check that artifact has been created successfully") + By("checking that the component has been reconciled successfully") + Eventually(komega.Object(component), "5m").Should( + HaveField("Status.ObservedGeneration", Equal(int64(1)))) - Eventually(komega.Object(component), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + By("checking that the snapshot has been created successfully") + Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) 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")) @@ -391,7 +412,7 @@ var _ = Describe("Component Controller", func() { Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) return component.Status.Component.Version == "0.0.2" - }).WithTimeout(15 * time.Second).Should(BeTrue()) + }).WithTimeout(60 * time.Second).Should(BeTrue()) }) }) @@ -585,6 +606,14 @@ var _ = Describe("Component Controller", func() { } Expect(k8sClient.Create(ctx, component)).To(Succeed()) + By("checking that the component has been reconciled successfully") + Eventually(komega.Object(component), "5m").Should( + HaveField("Status.ObservedGeneration", Equal(int64(1)))) + + By("checking that the snapshot has been created successfully") + Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + Eventually(komega.Object(component), "15s").Should( HaveField("Status.EffectiveOCMConfig", ConsistOf( v1alpha1.OCMConfiguration{ diff --git a/internal/controller/component/suite_test.go b/internal/controller/component/suite_test.go index c7c66166..96fc491e 100644 --- a/internal/controller/component/suite_test.go +++ b/internal/controller/component/suite_test.go @@ -16,9 +16,9 @@ package component import ( "context" "fmt" - "io" "net/http" "os" + "os/exec" "path/filepath" "runtime" "testing" @@ -27,27 +27,23 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - "github.com/openfluxcd/controller-manager/server" + artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) // +kubebuilder:scaffold:imports @@ -55,15 +51,13 @@ 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 *snapshot.Registry +var zotRootDir string func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -76,26 +70,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 +83,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()) @@ -130,10 +110,36 @@ 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)) + // Create zot-registry config file + zotRootDir = Must(os.MkdirTemp("", "")) + zotAddress := "0.0.0.0" + zotPort := "8080" + zotConfig := []byte(fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},"http":{"address":"%s","port": "%s"}}`, zotRootDir, zotAddress, zotPort)) + zotConfigFile := filepath.Join(zotRootDir, "config.json") + MustBeSuccessful(os.WriteFile(zotConfigFile, zotConfig, 0644)) + + // Start zot-registry + zotCmd = exec.Command(filepath.Join("..", "..", "..", "bin", "zot-registry"), "serve", zotConfigFile) + err = zotCmd.Start() + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to start Zot")) + + // Wait for Zot to be ready + Eventually(func() error { + resp, err := http.Get(fmt.Sprintf("http://%s:%s/v2/", zotAddress, zotPort)) + if err != nil { + return fmt.Errorf("could not connect to Zot") + } + + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil + }, 30*time.Second, 1*time.Second).Should(Succeed(), "Zot registry did not start in time") + + registry, err = snapshot.NewRegistry(fmt.Sprintf("%s:%s", zotAddress, zotPort)) + registry.PlainHTTP = true Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ @@ -144,7 +150,7 @@ var _ = BeforeSuite(func() { IncludeObject: true, }, }, - Storage: storage, + Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) ctx, cancel := context.WithCancel(context.Background()) @@ -157,12 +163,18 @@ var _ = BeforeSuite(func() { } Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) - go func() { - defer GinkgoRecover() - Expect(artifactServer.Start(ctx)).To(Succeed()) - }() go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) }() }) + +var _ = AfterSuite(func() { + if zotCmd != nil { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") + + // Clean up root directory + MustBeSuccessful(os.RemoveAll(zotRootDir)) + } +}) diff --git a/internal/controller/configuration/client/client.go b/internal/controller/configuration/client/client.go index 68755254..c65a600e 100644 --- a/internal/controller/configuration/client/client.go +++ b/internal/controller/configuration/client/client.go @@ -4,7 +4,6 @@ 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" @@ -12,6 +11,7 @@ import ( "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" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) type Client interface { @@ -24,24 +24,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(c client.Client, r snapshotRegistry.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, + Client: c, + Registry: r, + scheme: scheme, + encoder: encoder, } } type localStorageBackedClient struct { - client.Reader - *storage.Storage - scheme *runtime.Scheme - encoder runtime.Encoder + client.Client + Registry snapshotRegistry.RegistryType + scheme *runtime.Scheme + encoder runtime.Encoder } var _ Client = &localStorageBackedClient{} @@ -57,7 +57,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 artifactutil.GetContentBackedBySnapshotFromComponent(ctx, clnt.Client, clnt.Registry, &ref) default: return nil, fmt.Errorf("unsupported configuration target kind: %s", ref.Kind) } @@ -66,9 +66,9 @@ 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 artifactutil.GetContentBackedBySnapshotFromComponent(ctx, clnt.Client, clnt.Registry, &ref) case v1alpha1.KindResourceConfig: - return GetResourceConfigFromKubernetes(ctx, clnt.Reader, clnt.encoder, ref) + return GetResourceConfigFromKubernetes(ctx, clnt.Client, clnt.encoder, ref) default: return nil, fmt.Errorf("unsupported configuration source kind: %s", ref.Kind) } diff --git a/internal/controller/configuration/configuration_controller.go b/internal/controller/configuration/configuration_controller.go index 5fdd2a22..25cf487a 100644 --- a/internal/controller/configuration/configuration_controller.go +++ b/internal/controller/configuration/configuration_controller.go @@ -21,16 +21,16 @@ import ( "errors" "fmt" "os" + "path/filepath" "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" + corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" @@ -38,7 +38,9 @@ import ( "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/ocm" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // SetupWithManager sets up the controller with the Manager. @@ -51,7 +53,7 @@ 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{}). + Owns(&v1alpha1.Snapshot{}). // Update when a resource specified as target changes Watches(&v1alpha1.Resource{}, onTargetChange). Watches(&v1alpha1.LocalizedResource{}, onTargetChange). @@ -69,8 +71,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 snapshotRegistry.RegistryType } // +kubebuilder:rbac:groups=delivery.ocm.software,resources=configuredresources,verbs=get;list;watch;create;update;patch;delete @@ -85,6 +87,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,25 +98,12 @@ 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) - } - } + if configuration.GetDeletionTimestamp() != nil { + logger.Info("configuration is being deleted and cannot be used", "name", configuration.Name) return ctrl.Result{}, nil } - if added := controllerutil.AddFinalizer(configuration, v1alpha1.ArtifactFinalizer); added { - return ctrl.Result{Requeue: true}, r.Update(ctx, configuration) - } - return r.reconcileWithStatusUpdate(ctx, configuration) } @@ -131,13 +122,10 @@ func (r *Reconciler) reconcileWithStatusUpdate(ctx context.Context, localization return result, nil } +//nolint:funlen,gocognit // we do not want to cut function at an arbitrary point 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,26 +148,31 @@ 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) + // TODO: Find out what digest and revision this is. And what filename? + // I think this should just work well + digest, revision, _, err := artifact.UniqueIDsForSnapshotContentCombination(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) } + // Check if a snapshot of the configuration resource already exists and if it holds the same calculated digest + // from above logger.V(1).Info("verifying configuration", "digest", digest, "revision", revision) - hasValidArtifact, err := ocm.ValidateArtifactForCollectable( + hasValidArtifact, err := ocm.ValidateSnapshotForOwner( ctx, r.Client, - r.Storage, + r.Registry, configuration, digest, ) if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to check if artifact is valid: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to check if snapshot is valid: %w", err) } - var configured string + // TODO: Cleanup + //nolint:nestif // TODO: Add description if !hasValidArtifact { logger.V(1).Info("configuring", "digest", digest, "revision", revision) basePath, err := os.MkdirTemp("", "configured-") @@ -192,43 +185,72 @@ 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 + tarfile := filepath.Join(basePath, "config.tar") + if err := test.CreateTGZFromPath(configured, tarfile); err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) - 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) + return ctrl.Result{}, fmt.Errorf("failed to configure: %w", err) + } + + data, err := os.ReadFile(tarfile) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to configure: %w", err) + } + + repositoryName, err := snapshotRegistry.CreateRepositoryName(configuration.GetName()) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to configure: %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) + } + + manifestDigest, err := repository.PushSnapshot(ctx, configuration.GetResourceVersion(), data) + 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 digest calculated above for the blob-info digest, so we can compare for any changes + snapshotCR := snapshotRegistry.Create(configuration, repositoryName, manifestDigest.String(), configuration.GetResourceVersion(), digest, int64(len(data))) + + if _, err = controllerutil.CreateOrUpdate(ctx, r.GetClient(), snapshotCR, func() error { + if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { + if err := controllerutil.SetControllerReference(configuration, snapshotCR, r.GetScheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) } } - configuration.Status.ArtifactRef = &v1alpha1.ObjectKey{ - Name: artifact.Name, - Namespace: artifact.Namespace, + configuration.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), } return nil - }, - ); err != nil { - status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ReconcileArtifactFailedReason, err.Error()) + }); err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.CreateSnapshotFailedReason, err.Error()) - return ctrl.Result{}, fmt.Errorf("failed to reconcile artifact: %w", err) + return ctrl.Result{}, err + } } - logger.Info("configuration successful", "artifact", configuration.Status.ArtifactRef) + configuration.Status.Digest = digest + + logger.Info("configuration successful", "snapshot", configuration.Status.SnapshotRef) 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..fdc5037e 100644 --- a/internal/controller/configuration/configuration_controller_test.go +++ b/internal/controller/configuration/configuration_controller_test.go @@ -7,6 +7,7 @@ import ( _ "embed" + . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" @@ -15,7 +16,6 @@ import ( "github.com/mandelsoft/vfs/pkg/projectionfs" "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 +23,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/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) @@ -65,7 +66,7 @@ var _ = Describe("ConfiguredResource Controller", func() { fileContentAfterConfiguration := []byte(`mykey: "substituted"`) dir := filepath.Join(tmp, "test") - test.CreateTGZ(dir, map[string][]byte{ + test.CreateTGZFromData(dir, map[string][]byte{ fileToConfigure: fileContentBeforeConfiguration, }) @@ -79,7 +80,7 @@ var _ = Describe("ConfiguredResource Controller", func() { Namespace: Namespace, Name: component.GetName(), }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, @@ -135,13 +136,11 @@ var _ = Describe("ConfiguredResource Controller", func() { }) Eventually(Object(configuredResource), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) - art := &artifactv1.Artifact{} - art.Name = configuredResource.Status.ArtifactRef.Name - art.Namespace = configuredResource.Namespace + snapshotCR := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, configuredResource)) - test.VerifyArtifact(strg, art, map[string]func(data []byte){ + test.VerifyArtifact(ctx, registry, snapshotCR, map[string]func(data []byte){ fileToConfigure: func(data []byte) { Expect(data).To(MatchYAML(fileContentAfterConfiguration)) }, @@ -157,7 +156,7 @@ func NoOpComponent(ctx context.Context, basePath string) *v1alpha1.Component { nil, &test.MockComponentOptions{ BasePath: basePath, - Strg: strg, + Registry: registry, Client: k8sClient, Recorder: recorder, Info: v1alpha1.ComponentInfo{ diff --git a/internal/controller/configuration/suite_test.go b/internal/controller/configuration/suite_test.go index 04f11f25..01023891 100644 --- a/internal/controller/configuration/suite_test.go +++ b/internal/controller/configuration/suite_test.go @@ -16,36 +16,34 @@ 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" + artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" "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/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) // +kubebuilder:scaffold:imports @@ -61,8 +59,10 @@ 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 *snapshot.Registry +var zotRootDir string func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -75,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/. @@ -104,6 +88,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()) @@ -130,37 +116,66 @@ 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, } + // Create zot-registry config file + zotRootDir = Must(os.MkdirTemp("", "")) + zotAddress := "0.0.0.0" + zotPort := "8083" + zotConfig := []byte(fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},"http":{"address":"%s","port": "%s"}}`, zotRootDir, zotAddress, zotPort)) + zotConfigFile := filepath.Join(zotRootDir, "config.json") + MustBeSuccessful(os.WriteFile(zotConfigFile, zotConfig, 0644)) + + // Start zot-registry + zotCmd = exec.Command(filepath.Join("..", "..", "..", "bin", "zot-registry"), "serve", zotConfigFile) + err = zotCmd.Start() + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to start Zot")) + + // Wait for Zot to be ready + Eventually(func() error { + resp, err := http.Get(fmt.Sprintf("http://%s:%s/v2/", zotAddress, zotPort)) + if err != nil { + return fmt.Errorf("could not connect to Zot") + } + + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil + }, 30*time.Second, 1*time.Second).Should(Succeed(), "Zot registry did not start in time") + + registry, err = snapshot.NewRegistry(fmt.Sprintf("%s:%s", zotAddress, zotPort)) + registry.PlainHTTP = true + 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() { + if zotCmd != nil { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") + + // Clean up root directory + MustBeSuccessful(os.RemoveAll(zotRootDir)) + } +}) diff --git a/internal/controller/localization/client/client.go b/internal/controller/localization/client/client.go index 40f53fdf..0630aa75 100644 --- a/internal/controller/localization/client/client.go +++ b/internal/controller/localization/client/client.go @@ -4,7 +4,6 @@ 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" @@ -12,6 +11,7 @@ import ( "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/snapshot" ) type Client interface { @@ -27,24 +27,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 *snapshot.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 *snapshot.Registry + scheme *runtime.Scheme + encoder runtime.Encoder } func (clnt *localStorageBackedClient) Scheme() *runtime.Scheme { @@ -63,7 +63,7 @@ func (clnt *localStorageBackedClient) GetLocalizationTarget( case v1alpha1.KindLocalizedResource: fallthrough case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return artifactutil.GetContentBackedBySnapshotFromComponent(ctx, clnt.Client, clnt.Registry, &ref) default: return nil, fmt.Errorf("unsupported localization target kind: %s", ref.Kind) } @@ -75,9 +75,9 @@ func (clnt *localStorageBackedClient) GetLocalizationConfig( ) (types.LocalizationConfig, error) { switch ref.Kind { case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return artifactutil.GetContentBackedBySnapshotFromComponent(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) } diff --git a/internal/controller/localization/localization_controller.go b/internal/controller/localization/localization_controller.go index 1cd8c4f0..766bf629 100644 --- a/internal/controller/localization/localization_controller.go +++ b/internal/controller/localization/localization_controller.go @@ -9,7 +9,6 @@ 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" "ocm.software/ocm/api/ocm/compdesc" @@ -23,7 +22,6 @@ 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" @@ -35,6 +33,7 @@ import ( "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/ocm" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" "github.com/open-component-model/ocm-k8s-toolkit/pkg/util" ) @@ -42,8 +41,8 @@ import ( // Reconciler reconciles a LocalizationRules object. type Reconciler struct { *ocm.BaseReconciler - *storage.Storage LocalizationClient localizationclient.Client + Registry snapshotRegistry.RegistryType } var _ ocm.Reconciler = (*Reconciler)(nil) @@ -84,6 +83,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,29 +94,12 @@ 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) - } - } + if localization.GetDeletionTimestamp() != nil { + logger.Info("localization is being deleted and cannot be used", "name", localization.Name) 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) - } - - return ctrl.Result{Requeue: true}, nil - } - return r.reconcileWithStatusUpdate(ctx, localization) } @@ -137,10 +121,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 +132,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.(LocalizableSnapshotContent) 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 +151,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,26 +231,25 @@ 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) + snapshotCR, err := snapshotRegistry.GetSnapshotForOwner(ctx, r.Client, configuredResource) + if err != nil { + status.MarkNotReady(r.EventRecorder, localization, v1alpha1.GetSnapshotFailedReason, err.Error()) + + return ctrl.Result{}, 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) + snapshotOp, err := controllerutil.CreateOrUpdate(ctx, r.Client, snapshotCR, func() error { + if err := controllerutil.SetOwnerReference(localization, snapshotCR, r.Scheme); err != nil { + return fmt.Errorf("failed to set indirect owner reference on snapshot: %w", err) } - if art.GetAnnotations() == nil { - art.SetAnnotations(map[string]string{}) + if snapshotCR.GetAnnotations() == nil { + snapshotCR.SetAnnotations(map[string]string{}) } - a := art.GetAnnotations() - a["ocm.software/artifact-purpose"] = "localization" + a := snapshotCR.GetAnnotations() + a["ocm.software/snapshot-purpose"] = "localization" a["ocm.software/localization"] = fmt.Sprintf("%s/%s", localization.GetNamespace(), localization.GetName()) - art.SetAnnotations(a) + snapshotCR.SetAnnotations(a) return nil }) @@ -279,9 +258,9 @@ func (r *Reconciler) reconcileExists(ctx context.Context, localization *v1alpha1 return ctrl.Result{}, fmt.Errorf("failed to create or update artifact: %w", err) } - logger.V(1).Info(fmt.Sprintf("artifact %s", artOp)) + logger.V(1).Info(fmt.Sprintf("snapshot %s", snapshotOp)) - localization.Status.ArtifactRef = configuredResource.Status.ArtifactRef + localization.Status.SnapshotRef = configuredResource.Status.SnapshotRef localization.Status.Digest = configuredResource.Status.Digest localization.Status.ConfiguredResourceRef = &v1alpha1.ObjectKey{ Name: configuredResource.GetName(), @@ -296,8 +275,8 @@ func (r *Reconciler) reconcileExists(ctx context.Context, localization *v1alpha1 func localizeRules( ctx context.Context, c client.Client, - s *storage.Storage, - content LocalizableArtifactContent, + r snapshotRegistry.RegistryType, + content LocalizableSnapshotContent, cfg types.LocalizationConfig, ) ( []v1alpha1.ConfigurationRule, @@ -308,7 +287,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, c, r, content.GetComponent()) if err != nil { return nil, fmt.Errorf("failed to get content descriptor and set: %w", err) } @@ -355,9 +334,9 @@ func localizeRules( return localizedRules, nil } -// LocalizableArtifactContent is an artifact content that is backed by a component and resource, allowing it +// LocalizableSnapshotContent 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 { +type LocalizableSnapshotContent interface { artifact.Content GetComponent() *v1alpha1.Component GetResource() *v1alpha1.Resource @@ -365,18 +344,25 @@ type LocalizableArtifactContent interface { func ComponentDescriptorAndSetFromResource( ctx context.Context, - clnt client.Reader, - strg *storage.Storage, + clnt client.Client, + r snapshotRegistry.RegistryType, baseComponent *v1alpha1.Component, ) (compdesc.ComponentVersionResolver, *compdesc.ComponentDescriptor, error) { - art, err := util.GetNamespaced[artifactv1.Artifact](ctx, clnt, baseComponent.Status.ArtifactRef, baseComponent.Namespace) + snapshotResource, err := snapshotRegistry.GetSnapshotForOwner(ctx, clnt, baseComponent) if err != nil { - return nil, nil, fmt.Errorf("failed to Get artifact: %w", err) + return nil, nil, fmt.Errorf("failed to get snapshot: %w", err) } - componentSet, err := ocm.GetComponentSetForArtifact(strg, art) + + repository, err := r.NewRepository(ctx, snapshotResource.Spec.Repository) if err != nil { - return nil, nil, fmt.Errorf("failed to Get component version set: %w", err) + return nil, nil, fmt.Errorf("failed to create repository: %w", err) } + + componentSet, err := ocm.GetComponentSetForSnapshot(ctx, repository, snapshotResource) + if err != nil { + return nil, nil, fmt.Errorf("failed to get component version set: %w", err) + } + 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) diff --git a/internal/controller/localization/localization_controller_test.go b/internal/controller/localization/localization_controller_test.go index baa69bd8..a415b408 100644 --- a/internal/controller/localization/localization_controller_test.go +++ b/internal/controller/localization/localization_controller_test.go @@ -3,12 +3,12 @@ package localization import ( "bytes" "context" - "os" "path/filepath" "text/template" _ "embed" + . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" @@ -21,14 +21,13 @@ import ( "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" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ocmbuilder "ocm.software/ocm/api/helper/builder" 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/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) @@ -71,16 +70,6 @@ var _ = Describe("Localization Controller", func() { 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) { component := test.SetupComponentWithDescriptorList(ctx, ComponentObj, @@ -88,7 +77,7 @@ var _ = Describe("Localization Controller", func() { descriptorListYAML, &test.MockComponentOptions{ BasePath: tmp, - Strg: strg, + Registry: registry, Client: k8sClient, Recorder: recorder, Info: v1alpha1.ComponentInfo{ @@ -114,7 +103,7 @@ var _ = Describe("Localization Controller", func() { Namespace: Namespace, Name: ComponentObj, }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, @@ -133,7 +122,7 @@ var _ = Describe("Localization Controller", func() { Namespace: Namespace, Name: ComponentObj, }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, @@ -153,24 +142,22 @@ var _ = Describe("Localization Controller", func() { }) Eventually(Object(localization), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + + snapshotCR := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, localization)) - art := &artifactv1.Artifact{} - art.Name = localization.Status.ArtifactRef.Name - art.Namespace = localization.Namespace + // TODO: Clean up + // field not present any more.. what was its purpose? + //Eventually(Object(snapshotCR), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) + repository, err := registry.NewRepository(ctx, snapshotCR.Spec.Repository) + Expect(err).ToNot(HaveOccurred()) - localized := strg.LocalPath(art) - Expect(localized).To(BeAnExistingFile()) + data, err := repository.FetchSnapshot(ctx, snapshotCR.GetDigest()) + 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, data)).To(Succeed()) valuesData, err := memFs.ReadFile("values.yaml") Expect(err).ToNot(HaveOccurred()) diff --git a/internal/controller/localization/suite_test.go b/internal/controller/localization/suite_test.go index 3e564fe9..97ef6921 100644 --- a/internal/controller/localization/suite_test.go +++ b/internal/controller/localization/suite_test.go @@ -18,16 +18,19 @@ import ( "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" @@ -48,6 +51,7 @@ import ( 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/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) // +kubebuilder:scaffold:imports @@ -55,16 +59,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 *snapshot.Registry +var zotRootDir string +var ctx context.Context +var cancel context.CancelFunc func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -132,27 +136,63 @@ 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, } + // Create zot-registry config file + zotRootDir = Must(os.MkdirTemp("", "")) + zotAddress := "0.0.0.0" + zotPort := "8082" + zotConfig := []byte(fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},"http":{"address":"%s","port": "%s"}}`, zotRootDir, zotAddress, zotPort)) + zotConfigFile := filepath.Join(zotRootDir, "config.json") + MustBeSuccessful(os.WriteFile(zotConfigFile, zotConfig, 0644)) + + // Start zot-registry + zotCmd = exec.Command(filepath.Join("..", "..", "..", "bin", "zot-registry"), "serve", zotConfigFile) + err = zotCmd.Start() + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to start Zot")) + + // Wait for Zot to be ready + Eventually(func() error { + resp, err := http.Get(fmt.Sprintf("http://%s:%s/v2/", zotAddress, zotPort)) + if err != nil { + return fmt.Errorf("could not connect to Zot") + } + + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil + }, 30*time.Second, 1*time.Second).Should(Succeed(), "Zot registry did not start in time") + + registry, err = snapshot.NewRegistry(fmt.Sprintf("%s:%s", zotAddress, zotPort)) + registry.PlainHTTP = true + + 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 +201,24 @@ 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() { + if zotCmd != nil { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") + + // Clean up root directory + MustBeSuccessful(os.RemoveAll(zotRootDir)) + } +}) 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/resource/resource_controller.go b/internal/controller/resource/resource_controller.go index 81b28216..ab9bd730 100644 --- a/internal/controller/resource/resource_controller.go +++ b/internal/controller/resource/resource_controller.go @@ -21,17 +21,13 @@ import ( "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" "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 +40,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/ocm" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" ) type Reconciler struct { *ocm.BaseReconciler - Storage *storage.Storage + Registry snapshotRegistry.RegistryType } var _ ocm.Reconciler = (*Reconciler)(nil) @@ -81,8 +75,8 @@ 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 snapshot-events that are owned by the resource controller + Owns(&v1alpha1.Snapshot{}). // 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,30 +141,12 @@ 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 removed := controllerutil.RemoveFinalizer(resource, v1alpha1.ArtifactFinalizer); removed { - if err := r.Update(ctx, resource); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err) - } - } + if resource.GetDeletionTimestamp() != nil { + logger.Info("resource is being deleted and cannot be used", "name", resource.Name) return ctrl.Result{}, nil } - if added := controllerutil.AddFinalizer(resource, v1alpha1.ArtifactFinalizer); added { - err := r.Update(ctx, resource) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) - } - - return ctrl.Result{Requeue: true}, nil - } - return r.reconcile(ctx, resource) } @@ -223,6 +194,7 @@ func (r *Reconciler) reconcileOCM(ctx context.Context, resource *v1alpha1.Resour return result, nil } +//nolint:funlen // 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") @@ -244,22 +216,26 @@ 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") + // Get snapshot from component that contains component descriptor + componentSnapshot, err := snapshotRegistry.GetSnapshotForOwner(ctx, r.Client, component) + if err != nil { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.GetSnapshotFailedReason, err.Error()) - return ctrl.Result{}, fmt.Errorf("failed to get component artifact: %w", err) + return ctrl.Result{}, nil + } + + // Create repository from registry for snapshot + repositoryCD, err := r.Registry.NewRepository(ctx, componentSnapshot.Spec.Repository) + if err != nil { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.CreateOCIRepositoryFailedReason, err.Error()) + + return ctrl.Result{}, nil } // Get component descriptor set from artifact - cdSet, err := ocm.GetComponentSetForArtifact(r.Storage, artifactComponent) + cdSet, err := ocm.GetComponentSetForSnapshot(ctx, repositoryCD, componentSnapshot) if err != nil { - status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetComponentForArtifactFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetComponentForSnapshotFailedReason, err.Error()) return ctrl.Result{}, err } @@ -300,30 +276,127 @@ 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: Do not re-download resources that are already present in the OCI registry + // Resolution: + // - Use resource-access-digest as OCI repository name + // - Check if OCI repository name exists + // - If yes, create manifest and point to the previous OCI layer blob + // - How? + + // Create OCI repository to store snapshot. + // 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 snapshot 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.PushSnapshotFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + if resourceAccessSpec == nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.PushSnapshotFailedReason, "access spec is nil") + + return ctrl.Result{}, err + } + + if resourceAccessSpec.GetType() == "ociArtifact" { + manifestDigest, err = repositoryResource.CopySnapshotForResourceAccess(ctx, resourceAccess) + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.PushSnapshotFailedReason, 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{}, err + } + + resourceContent, err := blobAccess.Get() + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetResourceFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + // Push resource to OCI repository + manifestDigest, err = repositoryResource.PushSnapshot(ctx, resourceAccess.Meta().GetVersion(), resourceContent) + if err != nil { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.PushSnapshotFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + blobSize = int64(len(resourceContent)) + } + + // Create respective snapshot CR + snapshotCR := snapshotRegistry.Create( + resource, + repositoryResourceName, + manifestDigest.String(), + resourceAccess.Meta().GetVersion(), + // TODO: Think about using the resource-access as blob-digest + // + Always available (in comparison to OCI artifacts where we cannot calc the digest without downloading the + // manifest or blob + // - Not really the digest of the blob + resourceAccess.Meta().Digest.Value, + blobSize, + ) + + if _, err = controllerutil.CreateOrUpdate(ctx, r.GetClient(), snapshotCR, func() error { + if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { + if err := controllerutil.SetControllerReference(resource, snapshotCR, r.GetScheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + } + + resource.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), + } + + return nil + }); err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.CreateSnapshotFailedReason, err.Error()) return ctrl.Result{}, err } // Update status if err = setResourceStatus(ctx, configs, resource, resourceAccess); err != nil { - status.MarkNotReady(r.EventRecorder, component, v1alpha1.StatusSetFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.StatusSetFailedReason, err.Error()) return ctrl.Result{}, fmt.Errorf("failed to set resource status: %w", err) } @@ -437,123 +510,6 @@ 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 { log.FromContext(ctx).V(1).Info("updating resource status") diff --git a/internal/controller/resource/resource_controller_test.go b/internal/controller/resource/resource_controller_test.go index c93e7ba0..3868972c 100644 --- a/internal/controller/resource/resource_controller_test.go +++ b/internal/controller/resource/resource_controller_test.go @@ -17,43 +17,43 @@ limitations under the License. package resource import ( - "context" + _ "embed" "fmt" "io" - "net/http" "os" - "time" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "ocm.software/ocm/api/helper/builder" + "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" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8smetav1 "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" + ocmPkg "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) +var () + const ( CTFPath = "ocm-k8s-ctfstore--*" Namespace = "test-namespace" @@ -63,180 +63,234 @@ const ( 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 + testNumber int ) + BeforeEach(func() { - ctfPath = Must(os.MkdirTemp("", CTFPath)) + resourceLocalPath = Must(os.MkdirTemp("", CTFPath)) DeferCleanup(func() error { - return os.RemoveAll(ctfPath) + return os.RemoveAll(resourceLocalPath) }) env = NewBuilder(environment.FileSystem(osfs.OsFs)) DeferCleanup(env.Cleanup) - - ctx, cancel = context.WithCancel(context.Background()) - DeferCleanup(cancel) + testNumber++ }) Context("resource controller", func() { - It("can reconcile a resource", func() { - By("creating namespace object") - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: Namespace, - }, - } - Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + It("can reconcile a resource: PlainText", func() { + testComponent := fmt.Sprintf("%s-%d", ComponentObj, testNumber) + testResource := fmt.Sprintf("%s-%d", ResourceObj, testNumber) + resourceType := artifacttypes.PLAIN_TEXT - By("preparing a mock component") - prepareComponent(ctx, env, ctfPath) + 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(testResource, 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") + component := test.SetupComponentWithDescriptorList(ctx, testComponent, Namespace, dataCds, &test.MockComponentOptions{ + BasePath: "", + Registry: registry, + Client: k8sClient, + Recorder: recorder, + Info: v1alpha1.ComponentInfo{ + Component: Component, + Version: ComponentVersion, + RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, + }, + Repository: RepositoryObj, + }) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(k8smetav1.DeletePropagationForeground))).To(Succeed()) + }) By("creating a resource object") resource := &v1alpha1.Resource{ - ObjectMeta: metav1.ObjectMeta{ + ObjectMeta: k8smetav1.ObjectMeta{ Namespace: Namespace, - Name: ResourceObj, + Name: testResource, }, Spec: v1alpha1.ResourceSpec{ ComponentRef: corev1.LocalObjectReference{ - Name: ComponentObj, + Name: testComponent, }, Resource: v1alpha1.ResourceID{ ByReference: v1alpha1.ResourceReference{ - Resource: v1.NewIdentity(ResourceObj), + Resource: v1.NewIdentity(testResource), }, }, - 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.Delete(ctx, resource, client.PropagationPolicy(k8smetav1.DeletePropagationForeground))).To(Succeed()) }) By("checking that the resource has been reconciled successfully") - Eventually(komega.Object(resource), "5m").Should( + Eventually(komega.Object(resource), "15s").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.SnapshotRef.Name", Not(BeEmpty()))) + Expect(resource).To(HaveField("Status.Resource.Name", Equal(testResource))) + Expect(resource).To(HaveField("Status.Resource.Type", Equal(resourceType))) Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) - 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()) + snapshotResource, err := snapshot.GetSnapshotForOwner(ctx, k8sClient, resource) + Expect(err).NotTo(HaveOccurred()) - By("checking that the artifact server provides the resource") - r := Must(http.Get(artifact.Spec.URL)) - Expect(r).Should(HaveHTTPStatus(http.StatusOK)) + By("checking that the snapshot contains the correct content") + snapshotRepository, err := registry.NewRepository(ctx, snapshotResource.Spec.Repository) + Expect(err).NotTo(HaveOccurred()) + snapshotResourceContentReader, err := snapshotRepository.FetchSnapshot(ctx, snapshotResource.GetDigest()) + Expect(err).NotTo(HaveOccurred()) + snapshotResourceContent, err := io.ReadAll(snapshotResourceContentReader) + Expect(err).NotTo(HaveOccurred()) + Expect(string(snapshotResourceContent)).To(Equal(ResourceContent)) - 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()) - }) - Expect(err).To(BeNil()) - resourceContent := Must(io.ReadAll(reader)) + // Compare other fields + resourceAcc, err := cv.GetResource(v1.NewIdentity(testResource)) + Expect(err).NotTo(HaveOccurred()) - Expect(string(resourceContent)).To(Equal(ResourceContent)) + Expect(snapshotResource.Name).To(Equal(fmt.Sprintf("resource-%s", testResource))) + Expect(snapshotResource.Spec.Blob.Digest).To(Equal(resourceAcc.Meta().Digest.Value)) + Expect(snapshotResource.Spec.Blob.Tag).To(Equal(ResourceVersion)) + Expect(snapshotResource.Spec.Blob.Size).To(Equal(int64(len([]byte(ResourceContent))))) }) - }) -}) -// 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)) + It("can reconcile a resource: OCIArtifact", func() { + testComponent := fmt.Sprintf("%s-%d", ComponentObj, testNumber) + testResource := fmt.Sprintf("%s-%d", ResourceObj, testNumber) + resourceType := artifacttypes.OCI_ARTIFACT + + By("creating an OCI artifact") + repository, err := registry.NewRepository(ctx, testResource) + Expect(err).NotTo(HaveOccurred()) + manifestDigest, err := repository.PushSnapshot(ctx, ResourceVersion, []byte(ResourceContent)) + Expect(err).ToNot(HaveOccurred()) + DeferCleanup(func(ctx SpecContext) { + Expect(repository.DeleteSnapshot(ctx, manifestDigest.String())).To(Succeed()) + }) + + 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(testResource, ResourceVersion, resourceType, v1.LocalRelation, func() { + env.Access(ociartifact.New(fmt.Sprintf("http://%s/%s:%s", repository.GetHost(), repository.GetName(), ResourceVersion))) + }) + }) }) }) - }) - }) - 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) + 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") + component := test.SetupComponentWithDescriptorList(ctx, testComponent, Namespace, dataCds, &test.MockComponentOptions{ + BasePath: "", + Registry: registry, + Client: k8sClient, + Recorder: recorder, + Info: v1alpha1.ComponentInfo{ + Component: Component, + Version: ComponentVersion, + RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, + }, + Repository: RepositoryObj, + }) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(k8smetav1.DeletePropagationForeground))).To(Succeed()) + }) + + By("creating a resource object") + resource := &v1alpha1.Resource{ + ObjectMeta: k8smetav1.ObjectMeta{ + Namespace: Namespace, + Name: testResource, + }, + Spec: v1alpha1.ResourceSpec{ + ComponentRef: corev1.LocalObjectReference{ + Name: testComponent, + }, + Resource: v1alpha1.ResourceID{ + ByReference: v1alpha1.ResourceReference{ + Resource: v1.NewIdentity(testResource), + }, + }, + }, } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, resource, client.PropagationPolicy(k8smetav1.DeletePropagationForeground))).To(Succeed()) + }) - artifactName = art.Name - - return nil - }, - )).To(Succeed()) - - 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(k8sClient.Status().Update(ctx, baseComponent)).To(Succeed()) -} + 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.SnapshotRef.Name", Not(BeEmpty()))) + Expect(resource).To(HaveField("Status.Resource.Name", Equal(testResource))) + Expect(resource).To(HaveField("Status.Resource.Type", Equal(resourceType))) + Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) + + snapshotResource, err := snapshot.GetSnapshotForOwner(ctx, k8sClient, resource) + Expect(err).NotTo(HaveOccurred()) + + By("checking that the snapshot contains the correct content") + snapshotRepository, err := registry.NewRepository(ctx, snapshotResource.Spec.Repository) + Expect(err).NotTo(HaveOccurred()) + snapshotResourceContentReader, err := snapshotRepository.FetchSnapshot(ctx, snapshotResource.GetDigest()) + Expect(err).NotTo(HaveOccurred()) + snapshotResourceContent, err := io.ReadAll(snapshotResourceContentReader) + Expect(err).NotTo(HaveOccurred()) + Expect(string(snapshotResourceContent)).To(Equal(ResourceContent)) + + // Compare other fields + resourceAcc, err := cv.GetResource(v1.NewIdentity(testResource)) + Expect(err).NotTo(HaveOccurred()) + + Expect(snapshotResource.Name).To(Equal(fmt.Sprintf("resource-%s", testResource))) + Expect(snapshotResource.Spec.Blob.Digest).To(Equal(resourceAcc.Meta().Digest.Value)) + Expect(snapshotResource.Spec.Blob.Tag).To(Equal(ResourceVersion)) + Expect(snapshotResource.Spec.Blob.Size).To(Equal(int64(0))) + }) + + // TODO: Add more testcases + }) +}) diff --git a/internal/controller/resource/suite_test.go b/internal/controller/resource/suite_test.go index c5f9a2a4..eeeda1d1 100644 --- a/internal/controller/resource/suite_test.go +++ b/internal/controller/resource/suite_test.go @@ -19,6 +19,7 @@ import ( "io" "net/http" "os" + "os/exec" "path/filepath" "runtime" "testing" @@ -27,9 +28,8 @@ import ( . "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" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" @@ -47,6 +47,7 @@ import ( "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/snapshot" ) // +kubebuilder:scaffold:imports @@ -54,16 +55,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_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 *snapshot.Registry +var zotRootDir string +var ctx context.Context +var cancel context.CancelFunc func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -96,8 +97,6 @@ var _ = BeforeSuite(func() { }, 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/. @@ -132,10 +131,44 @@ 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, + } + + // Create zot-registry config file + zotRootDir = Must(os.MkdirTemp("", "")) + zotAddress := "0.0.0.0" + zotPort := "8081" + zotConfig := []byte(fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},"http":{"address":"%s","port": "%s"}}`, zotRootDir, zotAddress, zotPort)) + zotConfigFile := filepath.Join(zotRootDir, "config.json") + MustBeSuccessful(os.WriteFile(zotConfigFile, zotConfig, 0644)) + + // Start zot-registry + zotCmd = exec.Command(filepath.Join("..", "..", "..", "bin", "zot-registry"), "serve", zotConfigFile) + err = zotCmd.Start() + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to start Zot")) + + // Wait for Zot to be ready + Eventually(func() error { + resp, err := http.Get(fmt.Sprintf("http://%s:%s/v2/", zotAddress, zotPort)) + if err != nil { + return fmt.Errorf("could not connect to Zot") + } + + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil + }, 30*time.Second, 1*time.Second).Should(Succeed(), "Zot registry did not start in time") + + registry, err = snapshot.NewRegistry(fmt.Sprintf("%s:%s", zotAddress, zotPort)) + registry.PlainHTTP = true + + ctx, cancel = context.WithCancel(context.Background()) + DeferCleanup(cancel) Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ @@ -146,18 +179,34 @@ var _ = BeforeSuite(func() { IncludeObject: true, }, }, - 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()) - }() + 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()) + }) + go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) }() }) + +var _ = AfterSuite(func() { + if zotCmd != nil { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") + + // Clean up root directory + MustBeSuccessful(os.RemoveAll(zotRootDir)) + } +}) diff --git a/internal/controller/snapshot/controller.go b/internal/controller/snapshot/controller.go new file mode 100644 index 00000000..e53947c1 --- /dev/null +++ b/internal/controller/snapshot/controller.go @@ -0,0 +1,84 @@ +package snapshot + +import ( + "context" + "fmt" + + "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" + + ctrl "sigs.k8s.io/controller-runtime" + + deliveryv1alpha1 "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" +) + +// Reconciler reconciles a Snapshot object. +type Reconciler struct { + *ocm.BaseReconciler + Registry snapshotRegistry.RegistryType +} + +// +kubebuilder:rbac:groups=delivery.ocm.software,resources=snapshots,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=delivery.ocm.software,resources=snapshots/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=delivery.ocm.software,resources=snapshots/finalizers,verbs=update + +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + // Watch for snapshot resources + return ctrl.NewControllerManagedBy(mgr). + For(&deliveryv1alpha1.Snapshot{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Complete(r) +} + +// Reconcile add a finalizer on creation to the snapshot resource and handles the deletion of the snapshot by deleting +// the manifest of the OCI artifact in the OCI registry (The OCI registry GC deletes the blobs if no manifest is +// pointing to it). +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("Reconciling Snapshot") + + snapshotResource := &deliveryv1alpha1.Snapshot{} + if err := r.Get(ctx, req.NamespacedName, snapshotResource); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if snapshotResource.Spec.Suspend { + return ctrl.Result{}, nil + } + + if !snapshotResource.GetDeletionTimestamp().IsZero() { + logger.Info("Deleting snapshot") + + repository, err := r.Registry.NewRepository(ctx, snapshotResource.Spec.Repository) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create a repository: %w", err) + } + + if err := repository.DeleteSnapshot(ctx, snapshotResource.GetDigest()); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to delete snapshot: %w", err) + } + + if removed := controllerutil.RemoveFinalizer(snapshotResource, deliveryv1alpha1.SnapshotFinalizer); removed { + if err := r.Update(ctx, snapshotResource); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err) + } + } + + return ctrl.Result{}, nil + } + + if added := controllerutil.AddFinalizer(snapshotResource, deliveryv1alpha1.SnapshotFinalizer); added { + err := r.Update(ctx, snapshotResource) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) + } + + return ctrl.Result{Requeue: true}, nil + } + + return ctrl.Result{}, nil +} diff --git a/internal/controller/snapshot/controller_test.go b/internal/controller/snapshot/controller_test.go new file mode 100644 index 00000000..0d5f8c6f --- /dev/null +++ b/internal/controller/snapshot/controller_test.go @@ -0,0 +1,81 @@ +package snapshot + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + deliveryv1alpha1 "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" +) + +// TODO: Create tests +var _ = Describe("Snapshot Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + snapshot := &deliveryv1alpha1.Snapshot{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Snapshot") + err := k8sClient.Get(ctx, typeNamespacedName, snapshot) + if err != nil && errors.IsNotFound(err) { + resource := &deliveryv1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: deliveryv1alpha1.SnapshotSpec{ + Repository: "test-repository", + Digest: "sha256:test-digest", + Blob: deliveryv1alpha1.BlobInfo{ + Digest: "sha256:test-digest", + Tag: "1.0.0", + Size: 0, + }, + Suspend: false, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &deliveryv1alpha1.Snapshot{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Snapshot") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &Reconciler{ + BaseReconciler: &ocm.BaseReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + }, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/internal/controller/snapshot/suite_test.go b/internal/controller/snapshot/suite_test.go new file mode 100644 index 00000000..a13a44d8 --- /dev/null +++ b/internal/controller/snapshot/suite_test.go @@ -0,0 +1,81 @@ +package snapshot + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + deliveryv1alpha1 "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "config", "crd", "bases"), + }, + ErrorIfCRDPathMissing: true, + + // 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/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", + 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()) + Expect(cfg).NotTo(BeNil()) + + err = deliveryv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/pkg/artifact/resource.go b/pkg/artifact/resource.go index e4829a3a..5fcaea57 100644 --- a/pkg/artifact/resource.go +++ b/pkg/artifact/resource.go @@ -6,17 +6,15 @@ import ( "fmt" "io" "os" - "path/filepath" "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/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/util" ) @@ -40,35 +38,36 @@ type Content interface { } func NewContentBackedByComponentResourceArtifact( - storage *storage.Storage, + registry snapshot.RegistryType, component *v1alpha1.Component, resource *v1alpha1.Resource, - artifact *artifactv1.Artifact, + snapshot *v1alpha1.Snapshot, ) Content { return &ContentBackedByStorageAndComponent{ - Storage: storage, + Registry: registry, Component: component, Resource: resource, - Artifact: artifact, + Snapshot: snapshot, } } type ContentBackedByStorageAndComponent struct { - Storage *storage.Storage + Registry snapshot.RegistryType Component *v1alpha1.Component Resource *v1alpha1.Resource - Artifact *artifactv1.Artifact + Snapshot *v1alpha1.Snapshot } func (r *ContentBackedByStorageAndComponent) GetDigest() (string, error) { - return r.Artifact.Spec.Digest, nil + return r.Snapshot.Spec.Blob.Digest, nil } func (r *ContentBackedByStorageAndComponent) GetRevision() string { + // TODO: seems not good return fmt.Sprintf( - "artifact %s in revision %s (from resource %s, based on component %s)", - r.Artifact.GetName(), - r.Artifact.Spec.Revision, + "snapshot %s in revision %s (from resource %s, based on component %s)", + r.Snapshot.GetName(), + r.Snapshot.Spec.Blob.Digest, r.Resource.GetName(), r.Component.GetName(), ) @@ -79,22 +78,13 @@ func (r *ContentBackedByStorageAndComponent) Open() (io.ReadCloser, error) { } func (r *ContentBackedByStorageAndComponent) open() (io.ReadCloser, error) { - path := r.Storage.LocalPath(r.Artifact) - - unlock, err := r.Storage.Lock(r.Artifact) + ctx := context.Background() + repository, err := r.Registry.NewRepository(context.Background(), r.Snapshot.Spec.Repository) 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) - if err != nil { - return nil, err - } - - return &lockedReadCloser{ - ReadCloser: readCloser, - unlock: unlock, - }, nil + return repository.FetchSnapshot(ctx, r.Snapshot.GetDigest()) } var _ io.ReadCloser = &lockedReadCloser{} @@ -114,6 +104,7 @@ func (r *ContentBackedByStorageAndComponent) UnpackIntoDirectory(path string) (e err = errors.Join(err, data.Close()) }() + // TODO: AutoDecompress only decompresses if data is compressed. Is this still necessary? decompressed, _, err := compression.AutoDecompress(data) if err != nil { return fmt.Errorf("failed to autodecompress: %w", err) @@ -122,24 +113,14 @@ func (r *ContentBackedByStorageAndComponent) UnpackIntoDirectory(path string) (e err = errors.Join(err, decompressed.Close()) }() + // TODO: Check what happens with this early return. Is this still necessary? 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))) - file, err := os.Create(path) - if err != nil { - return fmt.Errorf("failed to unpack file at %s: %w", path, err) - } - defer func() { - err = errors.Join(err, file.Close()) - }() - if _, err := io.Copy(file, reader); err != nil { - return fmt.Errorf("failed to copy file to %s: %w", path, err) - } - - return nil + // TODO: Clean + return fmt.Errorf("TESTING: it is not a tar") } func (r *ContentBackedByStorageAndComponent) GetComponent() *v1alpha1.Component { @@ -163,33 +144,33 @@ func (l *lockedReadCloser) Close() error { return l.ReadCloser.Close() } -func GetContentBackedByArtifactFromComponent( +func GetContentBackedBySnapshotFromComponent( ctx context.Context, - clnt client.Reader, - strg *storage.Storage, + clnt client.Client, + registry snapshot.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, artifact, err := GetComponentResourceSnapshotFromReference(ctx, clnt, registry, ref) if err != nil { return nil, err } - return NewContentBackedByComponentResourceArtifact(strg, component, resource, artifact), nil + return NewContentBackedByComponentResourceArtifact(registry, component, resource, artifact), nil } type ObjectWithTargetReference interface { GetTarget() *v1alpha1.ConfigurationReference } -func GetComponentResourceArtifactFromReference( +func GetComponentResourceSnapshotFromReference( ctx context.Context, clnt client.Reader, - strg *storage.Storage, + registry snapshot.RegistryType, ref *v1alpha1.ConfigurationReference, -) (*v1alpha1.Component, *v1alpha1.Resource, *artifactv1.Artifact, error) { +) (*v1alpha1.Component, *v1alpha1.Resource, *v1alpha1.Snapshot, error) { var ( resource client.Object err error @@ -230,15 +211,15 @@ func GetComponentResourceArtifactFromReference( 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{} + snapshotResource := &v1alpha1.Snapshot{} 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) + Name: res.Status.SnapshotRef.Name, + }, snapshotResource); err != nil { + return nil, nil, nil, fmt.Errorf("failed to fetch snapshot %s belonging to resource %s: %w", res.Status.SnapshotRef.Name, ref.Name, err) } - return component, res, art, nil + return component, res, snapshotResource, nil } targetable, ok := resource.(ObjectWithTargetReference) @@ -246,15 +227,15 @@ func GetComponentResourceArtifactFromReference( return nil, nil, nil, fmt.Errorf("unsupported reference type: %T", resource) } - return GetComponentResourceArtifactFromReference(ctx, clnt, strg, targetable.GetTarget()) + return GetComponentResourceSnapshotFromReference(ctx, clnt, registry, targetable.GetTarget()) } -// UniqueIDsForArtifactContentCombination returns a set of unique identifiers for the combination of two Content. +// UniqueIDsForSnapshotContentCombination returns a set of unique identifiers for the combination of two Content. // This compromises of // - the digest of 'a' applied to 'b', machine identifiable and unique // - the revision of 'a' applied to 'b', human-readable // - the archive file name of 'a' applied to 'b'. -func UniqueIDsForArtifactContentCombination(a, b Content) (string, string, string, error) { +func UniqueIDsForSnapshotContentCombination(a, b Content) (string, string, string, error) { revisionAndDigest, err := util.NewMappedRevisionAndDigest(a, b) if err != nil { return "", "", "", fmt.Errorf("unable to create unique revision and digest: %w", err) diff --git a/pkg/ocm/artifact.go b/pkg/ocm/artifact.go index 48a85a64..62278d70 100644 --- a/pkg/ocm/artifact.go +++ b/pkg/ocm/artifact.go @@ -5,134 +5,100 @@ import ( "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" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) -// 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-*") +// GetComponentSetForSnapshot returns the component descriptor set for the given artifact. +func GetComponentSetForSnapshot(ctx context.Context, repository snapshot.RepositoryType, snapshotResource *v1alpha1.Snapshot) (*compdesc.ComponentVersionSet, error) { + reader, err := repository.FetchSnapshot(ctx, snapshotResource.GetDigest()) if err != nil { - return nil, fmt.Errorf("failed to create temporary directory: %w", err) + return nil, 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 { + if err := yaml.NewYAMLToJSONDecoder(reader).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. +// GetAndVerifySnapshotForOwner 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( +func GetAndVerifySnapshotForOwner( 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) + registry snapshot.RegistryType, + owner v1alpha1.SnapshotWriter, +) (*v1alpha1.Snapshot, error) { + snapshotRef := owner.GetSnapshotName() + if snapshotRef == "" { + return nil, os.ErrNotExist + } + + snapshotCR := &v1alpha1.Snapshot{} + if err := reader.Get(ctx, types.NamespacedName{Name: snapshotRef, Namespace: owner.GetNamespace()}, snapshotCR); err != nil { + return nil, fmt.Errorf("failed to get snapshot %s: %w", snapshotRef, err) + } + + repository, err := registry.NewRepository(ctx, snapshotCR.Spec.Repository) + if err != nil { + return nil, fmt.Errorf("failed to createry: %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) + exists, err := repository.ExistsSnapshot(ctx, snapshotCR.GetDigest()) + if err != nil { + return nil, fmt.Errorf("failed to check snapshot existence: %w", err) + } + + if !exists { + return nil, fmt.Errorf("snapshot %s does not exist", snapshotRef) } - return artifact, nil + // TODO: Discuss if we need more verification steps (which are even possible?) + // We could check if snapshotCR.Blob.Digest == layer.Digest() + // Problem how to make sure that snapshotCR.Blob.Digest & layer.Digest are calculated the same way? + + return snapshotCR, nil } -// ValidateArtifactForCollectable verifies if the artifact for the given collectable is valid. +// ValidateSnapshotForOwner 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( +func ValidateSnapshotForOwner( ctx context.Context, reader ctrl.Reader, - strg *storage.Storage, - collectable storage.Collectable, + registry snapshot.RegistryType, + owner v1alpha1.SnapshotWriter, digest string, ) (bool, error) { - artifact, err := GetAndVerifyArtifactForCollectable(ctx, reader, strg, collectable) + snapshotCR, err := GetAndVerifySnapshotForOwner(ctx, reader, registry, owner) 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 + return false, fmt.Errorf("failed to get snapshot: %w", err) } - - 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 err != nil { + return false, fmt.Errorf("failed to get and verify snapshot: %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) - } - } + if snapshotCR == nil { + return false, nil } - return nil + return snapshotCR.Spec.Blob.Digest != digest, nil } diff --git a/pkg/snapshot/registry.go b/pkg/snapshot/registry.go new file mode 100644 index 00000000..132d541a --- /dev/null +++ b/pkg/snapshot/registry.go @@ -0,0 +1,40 @@ +package snapshot + +import ( + "context" + "errors" + + "oras.land/oras-go/v2/registry/remote" +) + +// A RegistryType is something that can create a Repository. +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/snapshot/repository.go b/pkg/snapshot/repository.go new file mode 100644 index 00000000..1c2f4efb --- /dev/null +++ b/pkg/snapshot/repository.go @@ -0,0 +1,252 @@ +package snapshot + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/url" + "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" + ocmctx "ocm.software/ocm/api/ocm" + "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" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" +) + +// A RepositoryType is a type that can push and fetch blobs. +type RepositoryType interface { + // PushSnapshot pushes the blob to its repository. It returns the manifest-digest to retrieve the blob. + PushSnapshot(ctx context.Context, reference string, blob []byte) (digest.Digest, error) + + FetchSnapshot(ctx context.Context, reference string) (io.ReadCloser, error) + + DeleteSnapshot(ctx context.Context, digest string) error + + // ExistsSnapshot checks if the manifest and the referenced layer exists. + ExistsSnapshot(ctx context.Context, manifestDigest string) (bool, error) + + CopySnapshotForResourceAccess(ctx context.Context, access ocmctx.ResourceAccess) (digest.Digest, error) + + GetHost() string + GetName() string +} + +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) PushSnapshot(ctx context.Context, tag string, blob []byte) (digest.Digest, error) { + logger := log.FromContext(ctx) + + // Prepare and upload blob + blobDescriptor := ociV1.Descriptor{ + MediaType: ociV1.MediaTypeImageLayer, + 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("oci: error pushing blob: %w", err) + } + + // Prepare and upload image config + emptyImageConfig := []byte("{}") + + imageConfigDescriptor := ociV1.Descriptor{ + MediaType: ociV1.MediaTypeImageConfig, + Digest: digest.FromBytes(emptyImageConfig), + Size: int64(len(emptyImageConfig)), + } + + logger.Info("pushing OCI config") + if err := r.Push(ctx, imageConfigDescriptor, content.NewVerifyReader( + bytes.NewReader(emptyImageConfig), + imageConfigDescriptor, + )); err != nil { + return "", fmt.Errorf("oci: 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("oci: error marshaling manifest: %w", err) + } + + manifestDigest := digest.FromBytes(manifestBytes) + + manifestDescriptor := ociV1.Descriptor{ + MediaType: manifest.MediaType, + Digest: manifestDigest, + Size: int64(len(manifestBytes)), + } + + logger.Info("pushing OCI manifest") + if err := r.Push(ctx, manifestDescriptor, content.NewVerifyReader( + bytes.NewReader(manifestBytes), + manifestDescriptor, + )); err != nil { + return "", fmt.Errorf("oci: error pushing manifest: %w", err) + } + + logger.Info("tagging OCI manifest") + if err := r.Tag(ctx, manifestDescriptor, tag); err != nil { + return "", fmt.Errorf("oci: error tagging manifest: %w", err) + } + + logger.Info("finished pushing snapshot") + + return manifestDigest, nil +} + +func (r *Repository) FetchSnapshot(ctx context.Context, manifestDigest string) (io.ReadCloser, error) { + // Fetch manifest descriptor to get manifest. + manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) + if err != nil { + return nil, fmt.Errorf("oci: error fetching manifest: %w", err) + } + + // Fetch manifest to get layer[0] descriptor. + manifestReader, err := r.Fetch(ctx, manifestDescriptor) + if err != nil { + return nil, fmt.Errorf("oci: error fetching manifest: %w", err) + } + + var manifest ociV1.Manifest + if err := json.NewDecoder(manifestReader).Decode(&manifest); err != nil { + return nil, fmt.Errorf("oci: error parsing manifest: %w", err) + } + + // We only expect single layer artifacts. + if len(manifest.Layers) != 1 { + return nil, fmt.Errorf("oci: expected 1 layer, got %d", len(manifest.Layers)) + } + + return r.Fetch(ctx, manifest.Layers[0]) +} + +func (r *Repository) DeleteSnapshot(ctx context.Context, manifestDigest string) error { + manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) + if err != nil { + return fmt.Errorf("oci: error fetching manifest: %w", err) + } + + return r.Delete(ctx, manifestDescriptor) +} + +func (r *Repository) ExistsSnapshot(ctx context.Context, manifestDigest string) (bool, error) { + manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) + if err != nil { + return false, fmt.Errorf("oci: error fetching manifest: %w", err) + } + + return r.Exists(ctx, manifestDescriptor) +} + +func (r *Repository) CopySnapshotForResourceAccess(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("oci: 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("oci: error parsing image reference: %w", err) + } + + sourceRegistry, err := remote.NewRegistry(ref.Context().RegistryStr()) + if err != nil { + return "", fmt.Errorf("oci: 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("oci: 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(ctx context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + return nil + }, + PostCopy: func(ctx context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + return nil + }, + OnCopySkipped: func(ctx context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + return nil + }, + }, + }) + if err != nil { + return "", fmt.Errorf("oci: error copying snapshot: %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) + } + + return fmt.Sprintf("sha-%d", hash), nil +} diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go new file mode 100644 index 00000000..04c59ec5 --- /dev/null +++ b/pkg/snapshot/snapshot.go @@ -0,0 +1,72 @@ +package snapshot + +import ( + "context" + "errors" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/controller-runtime/pkg/client" + + errorsK8s "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" +) + +// generateName generates a name for a snapshot CR. If the name exceeds the character limit, it will be cut off at 256. +func generateName(obj v1alpha1.SnapshotWriter) string { + name := strings.ToLower(fmt.Sprintf("%s-%s", obj.GetKind(), obj.GetName())) + + if len(name) > validation.DNS1123SubdomainMaxLength { + return name[:validation.DNS1123SubdomainMaxLength] + } + + return name +} + +func Create(owner v1alpha1.SnapshotWriter, ociRepository, manifestDigest, blobVersion, blobDigest string, blobSize int64) *v1alpha1.Snapshot { + return &v1alpha1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: generateName(owner), + Namespace: owner.GetNamespace(), + }, + Spec: v1alpha1.SnapshotSpec{ + Repository: ociRepository, + Digest: manifestDigest, + Blob: v1alpha1.BlobInfo{ + Digest: blobDigest, + Tag: blobVersion, + Size: blobSize, + }, + }, + Status: v1alpha1.SnapshotStatus{}, + } +} + +func GetSnapshotForOwner(ctx context.Context, clientK8s client.Client, owner any) (*v1alpha1.Snapshot, error) { + ownerSnapshot, ok := owner.(v1alpha1.SnapshotWriter) + if !ok { + return nil, errors.New("owner is not a SnapshotWriter") + } + + // List all snapshots in owners namespace + var snapshots v1alpha1.SnapshotList + + if err := clientK8s.List(ctx, &snapshots, client.InNamespace(ownerSnapshot.GetNamespace())); err != nil { + return nil, fmt.Errorf("failed to list snapshots: %w", err) + } + + // Check for snapshot referenced by owner + for _, snapshot := range snapshots.Items { + for _, ref := range snapshot.ObjectMeta.OwnerReferences { + if ownerSnapshot.GetUID() == ref.UID { + return &snapshot, nil + } + } + } + + return nil, errorsK8s.NewNotFound(schema.GroupResource{Resource: "snapshots"}, "snapshot not found") +} diff --git a/pkg/test/component.go b/pkg/test/component.go new file mode 100644 index 00000000..fc87bb27 --- /dev/null +++ b/pkg/test/component.go @@ -0,0 +1,114 @@ +package test + +import ( + "context" + "fmt" + "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" + + "github.com/fluxcd/pkg/runtime/patch" + "github.com/opencontainers/go-digest" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" +) + +type MockComponentOptions struct { + BasePath string + Registry snapshotRegistry.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 { + // TODO: Find a better way........ + // Do we need to write the file? + dir, err := os.MkdirTemp("", "descriptor-list-*") + descriptorDir := filepath.Join(dir, "descriptors") + Expect(err).ToNot(HaveOccurred()) + DeferCleanup(func() error { + return os.RemoveAll(dir) + }) + + CreateTGZFromData(descriptorDir, 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, + }, + } + Expect(options.Client.Create(ctx, component)).To(Succeed()) + + patchHelper := patch.NewSerialPatcher(component, options.Client) + + data, err := os.ReadFile(filepath.Join(descriptorDir, v1alpha1.OCMComponentDescriptorList)) + Expect(err).ToNot(HaveOccurred()) + + // TODO: Clean-up + // Prevent error on NoOp for OCI push + if len(data) == 0 { + data = []byte("empty") + } + + repositoryName, err := snapshotRegistry.CreateRepositoryName(options.Repository, name) + Expect(err).ToNot(HaveOccurred()) + + repository, err := options.Registry.NewRepository(ctx, repositoryName) + Expect(err).ToNot(HaveOccurred()) + + manifestDigest, err := repository.PushSnapshot(ctx, options.Info.Version, data) + Expect(err).ToNot(HaveOccurred()) + + snapshotCR := snapshotRegistry.Create(component, repositoryName, manifestDigest.String(), options.Info.Version, digest.FromBytes(data).String(), int64(len(data))) + + _, err = controllerutil.CreateOrUpdate(ctx, options.Client, snapshotCR, func() error { + if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { + if err := controllerutil.SetControllerReference(component, snapshotCR, options.Client.Scheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + } + + component.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), + } + + component.Status.Component = options.Info + + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + 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 +} diff --git a/pkg/test/helper.go b/pkg/test/helper.go new file mode 100644 index 00000000..a89f9a58 --- /dev/null +++ b/pkg/test/helper.go @@ -0,0 +1,124 @@ +package test + +import ( + "archive/tar" + "context" + "fmt" + "io" + "os" + "path/filepath" + + //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" + + "github.com/mandelsoft/vfs/pkg/memoryfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "ocm.software/ocm/api/utils/tarutils" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" +) + +func VerifyArtifact(ctx context.Context, registry snapshotRegistry.RegistryType, snapshotCR *v1alpha1.Snapshot, files map[string]func(data []byte)) { + GinkgoHelper() + + repository, err := registry.NewRepository(ctx, snapshotCR.Spec.Repository) + Expect(err).ToNot(HaveOccurred()) + + data, err := repository.FetchSnapshot(ctx, snapshotCR.GetDigest()) + Expect(err).ToNot(HaveOccurred()) + + memFs := vfs.New(memoryfs.New()) + Expect(tarutils.UnzipTarToFs(memFs, data)).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 CreateTGZFromData(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()) + } +} + +func CreateTGZFromPath(srcDir, tarPath string) (err error) { + // Create the output tar file + tarFile, err := os.Create(tarPath) + if err != nil { + return fmt.Errorf("could not create file: %w", err) + } + defer func() { + err = tarFile.Close() + }() + + // Create a new tar writer + tarWriter := tar.NewWriter(tarFile) + defer func() { + err = tarWriter.Close() + }() + + // Walk through the source directory + return 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 header + header, err := tar.FileInfoHeader(fileInfo, fileInfo.Name()) + if err != nil { + return fmt.Errorf("could not create tar header: %w", err) + } + + // Use relative path for header.Name to preserve folder structure + relPath, err := filepath.Rel(srcDir, file) + if err != nil { + return fmt.Errorf("could not create relative path: %w", err) + } + header.Name = relPath + + // Write header + err = tarWriter.WriteHeader(header) + if err != nil { + return fmt.Errorf("could not write tar header: %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 + }) +} diff --git a/pkg/test/resource.go b/pkg/test/resource.go new file mode 100644 index 00000000..b9f4a58f --- /dev/null +++ b/pkg/test/resource.go @@ -0,0 +1,133 @@ +package test + +import ( + "context" + "fmt" + "io" + "os" + "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" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + 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" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" +) + +type MockResourceOptions struct { + // TODO: Check removal as this should be the basePath of the removed artifact + 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 + + Registry snapshotRegistry.RegistryType + 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) + + var data []byte + var err error + + if options.Data != nil { + data, err = io.ReadAll(options.Data) + Expect(err).ToNot(HaveOccurred()) + } + + if options.DataPath != "" { + f, err := os.Stat(options.DataPath) + Expect(err).ToNot(HaveOccurred()) + + // If the file is a directory, it must be tarred + if f.IsDir() { + tmpFile, err := os.CreateTemp("", "") + defer func() { + Expect(tmpFile.Close()).To(Succeed()) + }() + Expect(err).ToNot(HaveOccurred()) + + err = CreateTGZFromPath(options.DataPath, tmpFile.Name()) + Expect(err).ToNot(HaveOccurred()) + + data, err = os.ReadFile(tmpFile.Name()) + Expect(err).ToNot(HaveOccurred()) + } else { + data, err = os.ReadFile(options.DataPath) + Expect(err).ToNot(HaveOccurred()) + } + } + + // TODO: Check what about version?! + version := "1.0.0" + repositoryName, err := snapshotRegistry.CreateRepositoryName(options.ComponentRef.Name, name) + Expect(err).ToNot(HaveOccurred()) + repository, err := options.Registry.NewRepository(ctx, repositoryName) + Expect(err).ToNot(HaveOccurred()) + + manifestDigest, err := repository.PushSnapshot(ctx, version, data) + Expect(err).ToNot(HaveOccurred()) + snapshotCR := snapshotRegistry.Create(res, repositoryName, manifestDigest.String(), version, digest.FromBytes(data).String(), int64(len(data))) + + _, err = controllerutil.CreateOrUpdate(ctx, options.Clnt, snapshotCR, func() error { + if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { + if err := controllerutil.SetControllerReference(res, snapshotCR, options.Clnt.Scheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + } + + res.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), + } + + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + 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 +} 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()) - } -}