From d9af52c4bf8f653c031a3d77df85cd061b113ed1 Mon Sep 17 00:00:00 2001 From: Frederic Wilhelm Date: Thu, 6 Feb 2025 18:24:04 +0100 Subject: [PATCH] wip --- api/v1alpha1/component_types.go | 3 - api/v1alpha1/configuredresource_types.go | 9 +- api/v1alpha1/localizedresource_types.go | 9 +- api/v1alpha1/resource_types.go | 3 - api/v1alpha1/zz_generated.deepcopy.go | 14 +- cmd/main.go | 45 ++-- .../delivery.ocm.software_components.yaml | 16 -- ...very.ocm.software_configuredresources.yaml | 30 +-- ...ivery.ocm.software_localizedresources.yaml | 26 ++- .../delivery.ocm.software_resources.yaml | 16 -- config/rbac/role.yaml | 18 +- go.mod | 15 +- go.sum | 31 +-- .../controller/configuration/client/client.go | 26 +-- .../configuration/configuration_controller.go | 122 ++++++---- .../configuration_controller_test.go | 17 +- .../controller/configuration/suite_test.go | 95 ++++---- .../controller/localization/client/client.go | 26 +-- .../localization/localization_controller.go | 96 ++++---- .../localization_controller_test.go | 34 ++- .../controller/localization/suite_test.go | 71 ++++-- .../resource/resource_controller.go | 14 +- pkg/artifact/resource.go | 91 +++----- pkg/mocks/snapshot.go | 47 ---- pkg/ocm/artifact.go | 96 ++++---- pkg/snapshot/repository.go | 12 + pkg/test/util.go | 218 ++++++++++++------ 27 files changed, 586 insertions(+), 614 deletions(-) delete mode 100644 pkg/mocks/snapshot.go diff --git a/api/v1alpha1/component_types.go b/api/v1alpha1/component_types.go index 46b41b4c..3fa2d179 100644 --- a/api/v1alpha1/component_types.go +++ b/api/v1alpha1/component_types.go @@ -106,9 +106,6 @@ type ComponentStatus struct { // +optional SnapshotRef corev1.LocalObjectReference `json:"snapshotRef,omitempty"` - // TODO: Remove - ArtifactRef corev1.LocalObjectReference `json:"artifactRef,omitempty"` - // Component specifies the concrete version of the component that was // fetched after based on the semver constraints during the last successful // reconciliation. 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/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 5e9824f4..5c17f9bd 100644 --- a/api/v1alpha1/resource_types.go +++ b/api/v1alpha1/resource_types.go @@ -68,9 +68,6 @@ type ResourceStatus struct { // +optional SnapshotRef corev1.LocalObjectReference `json:"snapshotRef,omitempty"` - // TODO: Remove - ArtifactRef corev1.LocalObjectReference `json:"artifactRef,omitempty"` - // +optional Resource *ResourceInfo `json:"resource,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 541d3ca6..6b2ac782 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -143,7 +143,6 @@ func (in *ComponentStatus) DeepCopyInto(out *ComponentStatus) { } } out.SnapshotRef = in.SnapshotRef - out.ArtifactRef = in.ArtifactRef in.Component.DeepCopyInto(&out.Component) if in.EffectiveOCMConfig != nil { in, out := &in.EffectiveOCMConfig, &out.EffectiveOCMConfig @@ -395,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. @@ -726,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) @@ -1264,7 +1255,6 @@ func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { } } out.SnapshotRef = in.SnapshotRef - out.ArtifactRef = in.ArtifactRef if in.Resource != nil { in, out := &in.Resource, &out.Resource *out = new(ResourceInfo) diff --git a/cmd/main.go b/cmd/main.go index 2f3c60bf..d3edbea9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,14 +22,12 @@ import ( "crypto/tls" "flag" "os" - "time" // to ensure that exec-entrypoint and run can make use of them. // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) _ "k8s.io/client-go/plugin/pkg/client/auth" "github.com/fluxcd/pkg/runtime/events" - "github.com/openfluxcd/controller-manager/server" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -68,20 +66,18 @@ func init() { // +kubebuilder:scaffold:scheme } -//nolint:funlen,maintidx // this is the main function +//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 ) 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") @@ -172,14 +168,6 @@ func main() { os.Exit(1) } - // TODO: Replace - storage, artifactServer, err := server.NewArtifactStore(mgr.GetClient(), mgr.GetScheme(), - storagePath, storageAddr, storageAdvAddr, artifactRetentionTTL, artifactRetentionRecords) - if err != nil { - setupLog.Error(err, "unable to initialize storage") - os.Exit(1) - } - // TODO: Adjust hardcode with CLI param registry, err := snapshotRegistry.NewRegistry("ocm-k8s-toolkit-zot-registry.ocm-k8s-toolkit-system.svc.cluster.local:5000") registry.PlainHTTP = true @@ -212,7 +200,6 @@ func main() { EventRecorder: eventsRecorder, }, Registry: registry, - Storage: storage, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Resource") os.Exit(1) @@ -224,8 +211,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) @@ -237,8 +224,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) @@ -282,10 +269,6 @@ func main() { // entire process will terminate if we lose leadership, so we don't need // to handle that. <-mgr.Elected() - - if err := artifactServer.Start(ctx); err != nil { - setupLog.Error(err, "unable to start artifact server") - } }() setupLog.Info("starting manager") diff --git a/config/crd/bases/delivery.ocm.software_components.yaml b/config/crd/bases/delivery.ocm.software_components.yaml index 4603e20b..5a092f95 100644 --- a/config/crd/bases/delivery.ocm.software_components.yaml +++ b/config/crd/bases/delivery.ocm.software_components.yaml @@ -180,22 +180,6 @@ spec: status: description: ComponentStatus defines the observed state of Component. properties: - artifactRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - 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 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 edfed58a..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: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - 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: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c96c5b39..699a25d3 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -49,14 +49,8 @@ rules: - delivery.ocm.software resources: - components/finalizers - - configuredresources/finalizers - - localizedresources/finalizers - - ocmrepositories/finalizers - - replications/finalizers - - resources/finalizers - - snapshots/finalizers verbs: - - update + - updat - apiGroups: - delivery.ocm.software resources: @@ -71,6 +65,16 @@ rules: - 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/go.mod b/go.mod index 36cb1b01..3a94a8ba 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ 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 @@ -24,7 +23,6 @@ require ( 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 @@ -145,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 @@ -154,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 @@ -209,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 @@ -248,7 +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/runtime-spec v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pborman/uuid v1.2.1 // indirect @@ -268,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 @@ -305,8 +297,6 @@ 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 @@ -344,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 d5b6b48a..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= @@ -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= @@ -803,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= @@ -812,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= @@ -1036,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= @@ -1279,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= @@ -1297,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= 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..77326605 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..214f2927 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,7 +21,6 @@ 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" @@ -29,6 +28,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" ) @@ -88,7 +88,7 @@ var _ = Describe("Localization Controller", func() { descriptorListYAML, &test.MockComponentOptions{ BasePath: tmp, - Strg: strg, + Registry: registry, Client: k8sClient, Recorder: recorder, Info: v1alpha1.ComponentInfo{ @@ -114,7 +114,7 @@ var _ = Describe("Localization Controller", func() { Namespace: Namespace, Name: ComponentObj, }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, @@ -133,7 +133,7 @@ var _ = Describe("Localization Controller", func() { Namespace: Namespace, Name: ComponentObj, }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, @@ -153,24 +153,22 @@ var _ = Describe("Localization Controller", func() { }) Eventually(Object(localization), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) - art := &artifactv1.Artifact{} - art.Name = localization.Status.ArtifactRef.Name - art.Namespace = localization.Namespace + snapshotCR := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, localization)) - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) + // TODO: Clean up + // field not present any more.. what was its purpose? + //Eventually(Object(snapshotCR), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - localized := strg.LocalPath(art) - Expect(localized).To(BeAnExistingFile()) + repository, err := registry.NewRepository(ctx, snapshotCR.Spec.Repository) + Expect(err).ToNot(HaveOccurred()) - memFs := vfs.New(memoryfs.New()) - localizedArchiveData, err := os.OpenFile(localized, os.O_RDONLY, 0o600) + data, err := repository.FetchSnapshot(ctx, snapshotCR.GetDigest()) Expect(err).ToNot(HaveOccurred()) - DeferCleanup(func() { - Expect(localizedArchiveData.Close()).To(Succeed()) - }) - Expect(tarutils.UnzipTarToFs(memFs, localizedArchiveData)).To(Succeed()) + + memFs := vfs.New(memoryfs.New()) + 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..933daa9d 100644 --- a/internal/controller/localization/suite_test.go +++ b/internal/controller/localization/suite_test.go @@ -18,16 +18,17 @@ 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" - "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 +49,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 @@ -63,8 +65,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) @@ -132,27 +136,50 @@ 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 + 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 +188,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/resource/resource_controller.go b/internal/controller/resource/resource_controller.go index c93101d4..8facc34b 100644 --- a/internal/controller/resource/resource_controller.go +++ b/internal/controller/resource/resource_controller.go @@ -25,7 +25,6 @@ import ( "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/patch" "github.com/opencontainers/go-digest" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/apimachinery/pkg/types" "ocm.software/ocm/api/datacontext" "ocm.software/ocm/api/ocm/compdesc" @@ -55,7 +54,6 @@ import ( type Reconciler struct { *ocm.BaseReconciler - Storage *storage.Storage Registry snapshotRegistry.RegistryType } @@ -227,15 +225,15 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, } // Create repository from registry for snapshot - repositoryDescriptor, err := r.Registry.NewRepository(ctx, componentSnapshot.Spec.Repository) + repositoryCD, err := r.Registry.NewRepository(ctx, componentSnapshot.Spec.Repository) if err != nil { - status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.GetSnapshotFailedReason, err.Error()) + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.CreateOCIRepositoryFailedReason, err.Error()) return ctrl.Result{}, nil } // Get component descriptor set from artifact - cdSet, err := ocm.GetComponentSetForSnapshot(ctx, repositoryDescriptor, componentSnapshot) + cdSet, err := ocm.GetComponentSetForSnapshot(ctx, repositoryCD, componentSnapshot) if err != nil { status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetComponentForSnapshotFailedReason, err.Error()) @@ -312,7 +310,7 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, repositoryResourceName := resourceAccess.Meta().Digest.Value repositoryResource, err := r.Registry.NewRepository(ctx, repositoryResourceName) if err != nil { - status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.GetComponentVersionFailedReason, err.Error()) + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.CreateOCIRepositoryFailedReason, err.Error()) return ctrl.Result{}, err } @@ -347,14 +345,14 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, return nil }); err != nil { - status.MarkNotReady(r.EventRecorder, component, v1alpha1.CreateSnapshotFailedReason, err.Error()) + 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) } 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/mocks/snapshot.go b/pkg/mocks/snapshot.go deleted file mode 100644 index 080ec115..00000000 --- a/pkg/mocks/snapshot.go +++ /dev/null @@ -1,47 +0,0 @@ -package mocks - -import ( - "context" - "io" - - "github.com/opencontainers/go-digest" - "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" -) - -type Registry struct{} - -var _ snapshot.RegistryType = (*Registry)(nil) - -func NewRegistry(_ string) (snapshot.RegistryType, error) { - return &Registry{}, nil -} - -func (r *Registry) NewRepository(ctx context.Context, name string) (snapshot.RepositoryType, error) { - log.FromContext(ctx).Info("mocking repository creation", "name", name) - - return &Repository{}, nil -} - -type Repository struct{} - -var _ snapshot.RepositoryType = (*Repository)(nil) - -func (r *Repository) PushSnapshot(ctx context.Context, _ string, _ []byte) (digest.Digest, error) { - log.FromContext(ctx).Info("mocking snapshot push") - - return digest.FromString("mock"), nil -} - -func (r *Repository) FetchSnapshot(ctx context.Context, _ string) (io.ReadCloser, error) { - log.FromContext(ctx).Info("mocking snapshot fetch") - - return nil, nil -} - -func (r *Repository) DeleteSnapshot(ctx context.Context, _ string) error { - log.FromContext(ctx).Info("mocking snapshot delete") - - return nil -} diff --git a/pkg/ocm/artifact.go b/pkg/ocm/artifact.go index 127584dd..62278d70 100644 --- a/pkg/ocm/artifact.go +++ b/pkg/ocm/artifact.go @@ -5,14 +5,11 @@ 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" @@ -20,7 +17,7 @@ import ( ) // GetComponentSetForSnapshot returns the component descriptor set for the given artifact. -func GetComponentSetForSnapshot(ctx context.Context, repository snapshot.RepositoryType, snapshotResource *v1alpha1.Snapshot) (_ *compdesc.ComponentVersionSet, retErr error) { +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, err @@ -35,82 +32,73 @@ func GetComponentSetForSnapshot(ctx context.Context, repository snapshot.Reposit 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 } - // 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) + 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) } - return artifact, nil + repository, err := registry.NewRepository(ctx, snapshotCR.Spec.Repository) + if err != nil { + return nil, fmt.Errorf("failed to createry: %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) + } + + // 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 -} - -func GetComponentSetForArtifact(_ *storage.Storage, _ *artifactv1.Artifact) (*compdesc.ComponentVersionSet, error) { - return nil, nil + return snapshotCR.Spec.Blob.Digest != digest, nil } diff --git a/pkg/snapshot/repository.go b/pkg/snapshot/repository.go index e7f80ff9..598b6516 100644 --- a/pkg/snapshot/repository.go +++ b/pkg/snapshot/repository.go @@ -27,6 +27,8 @@ type RepositoryType interface { FetchSnapshot(ctx context.Context, reference string) (io.ReadCloser, error) DeleteSnapshot(ctx context.Context, digest string) error + + ExistsSnapshot(ctx context.Context, manifestDigest string) (bool, error) } type Repository struct { @@ -142,6 +144,16 @@ func (r *Repository) DeleteSnapshot(ctx context.Context, manifestDigest string) return r.Delete(ctx, manifestDescriptor) } +// ExistsSnapshot checks if the manifest and the referenced layer exists. +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) +} + // 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) { diff --git a/pkg/test/util.go b/pkg/test/util.go index bb0296aa..92c5f785 100644 --- a/pkg/test/util.go +++ b/pkg/test/util.go @@ -1,6 +1,7 @@ package test import ( + "archive/tar" "context" "fmt" "io" @@ -8,27 +9,28 @@ import ( "path/filepath" "time" + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/mandelsoft/goutils/testutils" //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" + "github.com/opencontainers/go-digest" "k8s.io/client-go/tools/record" "ocm.software/ocm/api/utils/tarutils" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - 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" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" ) @@ -42,7 +44,7 @@ type MockResourceOptions struct { ComponentRef v1alpha1.ObjectKey - Strg *storage.Storage + Registry snapshotRegistry.RegistryType Clnt client.Client Recorder record.EventRecorder } @@ -72,43 +74,58 @@ func SetupMockResourceWithData( 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) - } - } + 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 := Must(snapshotRegistry.CreateRepositoryName(options.ComponentRef.Name, name)) + repository := Must(options.Registry.NewRepository(ctx, repositoryName)) + + manifestDigest := Must(repository.PushSnapshot(ctx, version, data)) + snapshotCR := snapshotRegistry.Create(res, repositoryName, manifestDigest.String(), version, digest.FromBytes(data).String(), int64(len(data))) - res.Status.ArtifactRef = corev1.LocalObjectReference{ - Name: artifact.Name, + _ = Must(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) } + } - return nil - }) - Expect(err).ToNot(HaveOccurred()) + res.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), + } - art := &artifactv1.Artifact{} - art.Name = res.Status.ArtifactRef.Name - art.Namespace = res.Namespace - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) + return nil + })) Eventually(func(ctx context.Context) error { status.MarkReady(options.Recorder, res, "applied mock resource") @@ -121,7 +138,7 @@ func SetupMockResourceWithData( type MockComponentOptions struct { BasePath string - Strg *storage.Storage + Registry snapshotRegistry.RegistryType Client client.Client Recorder record.EventRecorder Info v1alpha1.ComponentInfo @@ -135,7 +152,7 @@ func SetupComponentWithDescriptorList( options *MockComponentOptions, ) *v1alpha1.Component { dir := filepath.Join(options.BasePath, "descriptor") - CreateTGZ(dir, map[string][]byte{ + CreateTGZFromData(dir, map[string][]byte{ v1alpha1.OCMComponentDescriptorList: descriptorListData, }) component := &v1alpha1.Component{ @@ -148,7 +165,7 @@ func SetupComponentWithDescriptorList( Component: options.Info.Component, }, Status: v1alpha1.ComponentStatus{ - ArtifactRef: corev1.LocalObjectReference{ + SnapshotRef: corev1.LocalObjectReference{ Name: name, }, Component: options.Info, @@ -158,30 +175,36 @@ func SetupComponentWithDescriptorList( 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) - } + data, err := os.ReadFile(filepath.Join(dir, v1alpha1.OCMComponentDescriptorList)) + Expect(err).ToNot(HaveOccurred()) + + // TODO: Clean-up + // Prevent error on NoOp for OCI push + if len(data) == 0 { + data = []byte("empty") + } - component.Status.ArtifactRef = corev1.LocalObjectReference{ - Name: artifact.Name, + repositoryName := Must(snapshotRegistry.CreateRepositoryName(options.Repository, name)) + repository := Must(options.Registry.NewRepository(ctx, repositoryName)) + + manifestDigest := Must(repository.PushSnapshot(ctx, options.Info.Version, data)) + snapshotCR := snapshotRegistry.Create(component, repositoryName, manifestDigest.String(), options.Info.Version, digest.FromBytes(data).String(), int64(len(data))) + + _ = Must(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.Component = options.Info + } - return nil - }), - ).To(Succeed()) + component.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), + } - art := &artifactv1.Artifact{} - art.Name = component.Status.ArtifactRef.Name - art.Namespace = component.Namespace - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) + component.Status.Component = options.Info + + return nil + })) Eventually(func(ctx context.Context) error { status.MarkReady(options.Recorder, component, "applied mock component") @@ -192,23 +215,15 @@ func SetupComponentWithDescriptorList( return component } -func VerifyArtifact(strg *storage.Storage, art *artifactv1.Artifact, files map[string]func(data []byte)) { +func VerifyArtifact(ctx context.Context, registry snapshotRegistry.RegistryType, snapshotCR *v1alpha1.Snapshot, files map[string]func(data []byte)) { GinkgoHelper() - art = art.DeepCopy() - - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) + repository := Must(registry.NewRepository(ctx, snapshotCR.Spec.Repository)) - localized := strg.LocalPath(art) - Expect(localized).To(BeAnExistingFile()) + data := Must(repository.FetchSnapshot(ctx, snapshotCR.GetDigest())) 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()) for fileName, assert := range files { data, err := memFs.ReadFile(fileName) @@ -217,7 +232,7 @@ func VerifyArtifact(strg *storage.Storage, art *artifactv1.Artifact, files map[s } } -func CreateTGZ(tgzPackageDir string, data map[string][]byte) { +func CreateTGZFromData(tgzPackageDir string, data map[string][]byte) { GinkgoHelper() Expect(os.Mkdir(tgzPackageDir, os.ModePerm|os.ModeDir)).To(Succeed()) for path, data := range data { @@ -231,3 +246,58 @@ func CreateTGZ(tgzPackageDir string, data map[string][]byte) { Expect(err).ToNot(HaveOccurred()) } } + +func CreateTGZFromPath(srcDir, tarPath string) error { + GinkgoHelper() + // Create the output tar file + tarFile, err := os.Create(tarPath) + Expect(err).ToNot(HaveOccurred()) + defer func() { + Expect(tarFile.Close()).To(Succeed()) + }() + + // Create a new tar writer + tarWriter := tar.NewWriter(tarFile) + defer func() { + Expect(tarWriter.Close()).To(Succeed()) + }() + + // Walk through the source directory + return filepath.Walk(srcDir, func(file string, fileInfo os.FileInfo, err error) error { + Expect(err).ToNot(HaveOccurred()) + + if !fileInfo.Mode().IsRegular() { + return nil + } + + if fileInfo.IsDir() { + return nil + } + + // Create tar header + header, err := tar.FileInfoHeader(fileInfo, fileInfo.Name()) + Expect(err).ToNot(HaveOccurred()) + + // Use relative path for header.Name to preserve folder structure + relPath, err := filepath.Rel(srcDir, file) + Expect(err).ToNot(HaveOccurred()) + header.Name = relPath + + // Write header + err = tarWriter.WriteHeader(header) + Expect(err).ToNot(HaveOccurred()) + + // Open the file + f, err := os.Open(file) + Expect(err).ToNot(HaveOccurred()) + defer func() { + Expect(f.Close()).To(Succeed()) + }() + + // Copy file data into the tar archive + _, err = io.Copy(tarWriter, f) + Expect(err).ToNot(HaveOccurred()) + + return nil + }) +}