diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index 417d5129..861c058f 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -116,7 +116,6 @@ type ResourceInfo struct { type BlobInfo struct { // Digest is the digest of the blob in the form of ':'. - // +kubebuilder:validation:Pattern="^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$" Digest string `json:"digest"` // Tag/Version of the blob 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/condition_types.go b/api/v1alpha1/condition_types.go index fbd031f5..ece9eaa1 100644 --- a/api/v1alpha1/condition_types.go +++ b/api/v1alpha1/condition_types.go @@ -17,22 +17,16 @@ limitations under the License. package v1alpha1 const ( - // SecretFetchFailedReason is used when the controller failed to fetch its secrets. - SecretFetchFailedReason = "SecretFetchFailed" - // ConfigFetchFailedReason is used when the controller failed to fetch its configs. ConfigFetchFailedReason = "ConfigFetchFailed" - // VerificationsInvalidReason is used when the controller failed to gather the verification information. - VerificationsInvalidReason = "VerificationsInvalid" - // ConfigureContextFailedReason is used when the controller failed to create an authenticated context. ConfigureContextFailedReason = "ConfigureContextFailed" // CheckVersionFailedReason is used when the controller failed to check for new versions. CheckVersionFailedReason = "CheckVersionFailed" - // RepositorySpecInvalidReason is used when the referenced repository spec cannot be unmarshaled and therefore is + // RepositorySpecInvalidReason is used when the referenced repository spec cannot be unmarshalled and therefore is // invalid. RepositorySpecInvalidReason = "RepositorySpecInvalid" @@ -42,7 +36,7 @@ const ( // ComponentIsNotReadyReason is used when the referenced component is not Ready yet. ComponentIsNotReadyReason = "ComponentIsNotReady" - // ComponentIsNotReadyReason is used when the referenced component is not Ready yet. + // ReplicationFailedReason is used when the referenced component is not Ready yet. ReplicationFailedReason = "ReplicationFailed" // VerificationFailedReason is used when the signature verification of a component failed. @@ -57,30 +51,33 @@ const ( // GetComponentVersionFailedReason is used when the component cannot be fetched. GetComponentVersionFailedReason = "GetComponentVersionFailed" - // StorageReconcileFailedReason is used when there was a problem reconciling the artifact storage. - StorageReconcileFailedReason = "StorageReconcileFailed" - - // ReconcileArtifactFailedReason is used when we fail in creating an Artifact. - ReconcileArtifactFailedReason = "ReconcileArtifactFailed" - // MarshalFailedReason is used when we fail to marshal a struct. MarshalFailedReason = "MarshalFailed" + // TGZCreationFailedReason is used when we fail to create a tar-gzip. + TGZCreationFailedReason = "TGZCreationFailed" + // CreateOCIRepositoryNameFailedReason is used when we fail to create an OCI repository name. CreateOCIRepositoryNameFailedReason = "CreateOCIRepositoryNameFailed" - // CreateOCIRepositoryFailedReason is used when we fail to create a OCI repository. + // CreateOCIRepositoryFailedReason is used when we fail to create an OCI repository. CreateOCIRepositoryFailedReason = "CreateOCIRepositoryFailed" + // OCIRepositoryExistsFailedReason is used when we fail to check the existence of an OCI repository. + OCIRepositoryExistsFailedReason = "OCIRepositoryExistsFailed" + // CreateSnapshotFailedReason is used when we fail to create a snapshot. CreateSnapshotFailedReason = "CreateSnapshotFailed" - // GetArtifactFailedReason is used when we fail in getting an Artifact. - GetArtifactFailedReason = "GetArtifactFailed" - // GetSnapshotFailedReason is used when we fail in getting a Snapshot. GetSnapshotFailedReason = "GetSnapshotFailed" + // SnapshotReadyFailedReason is used when the snapshot is not ready. + SnapshotReadyFailedReason = "SnapshotReadyFailed" + + // PushSnapshotFailedReason is used when we fail to push a snapshot. + PushSnapshotFailedReason = "PushSnapshotFailed" + // ResolveResourceFailedReason is used when we fail in resolving a resource. ResolveResourceFailedReason = "ResolveResourceFailed" @@ -96,18 +93,6 @@ const ( // GetResourceFailedReason is used when we fail to get the resource. GetResourceFailedReason = "GetResourceFailed" - // PushSnapshotFailedReason is used when we fail to push a snapshot. - PushSnapshotFailedReason = "PushSnapshotFailed" - - // FetchSnapshotFailedReason is used when we fail to fetch a snapshot. - FetchSnapshotFailedReason = "FetchSnapshotFailed" - - // DeleteSnapshotFailedReason is used when we fail to delete a snapshot. - DeleteSnapshotFailedReason = "DeleteSnapshotFailed" - - // GetComponentForArtifactFailedReason is used when we fail in getting a component for an artifact. - GetComponentForArtifactFailedReason = "GetComponentForArtifactFailed" - // GetComponentForSnapshotFailedReason is used when we fail in getting a component for a snapshot. GetComponentForSnapshotFailedReason = "GetComponentForSnapshotFailed" @@ -120,14 +105,14 @@ const ( // ConfigurationFailedReason is used when a resource was not able to be configured. ConfigurationFailedReason = "ConfigurationFailed" - // LocalizationRuleGenerationFailedReason is used when the controller failed to localize an artifact. + // LocalizationRuleGenerationFailedReason is used when the controller failed to localize an snapshot. LocalizationRuleGenerationFailedReason = "LocalizationRuleGenerationFailed" // LocalizationIsNotReadyReason is used when a controller is waiting to get the localization result. LocalizationIsNotReadyReason = "LocalizationIsNotReady" - // UniqueIDGenerationFailedReason is used when the controller failed to generate a unique identifier for a pending artifact. - // This can happen if the artifact is based on multiple other sources but these sources could not be used + // UniqueIDGenerationFailedReason is used when the controller failed to generate a unique identifier for a pending snapshot. + // This can happen if the snapshot is based on multiple other sources but these sources could not be used // to determine a unique identifier. UniqueIDGenerationFailedReason = "UniqueIDGenerationFailed" diff --git a/api/v1alpha1/configuredresource_types.go b/api/v1alpha1/configuredresource_types.go index d44807b5..6cccd1a5 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" ) @@ -90,13 +91,13 @@ type ConfiguredResourceStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` - // The configuration reconcile loop generates an artifact, which contains the + // The configuration reconcile loop generates a snapshot, 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 + // Digest contains a technical identifier for the snapshot. This technical identifier + // can be used to track changes on the SnapshotRef as it is a combination of the origin // ConfiguredResourceSpec.Config applied to the ConfiguredResourceSpec.Target. Digest string `json:"digest,omitempty"` } @@ -113,6 +114,10 @@ type ConfiguredResource struct { Status ConfiguredResourceStatus `json:"status,omitempty"` } +func (in *ConfiguredResource) GetSnapshotName() string { + return in.Status.SnapshotRef.Name +} + // +kubebuilder:object:root=true // ConfiguredResourceList contains a list of ConfiguredResource. diff --git a/api/v1alpha1/constants.go b/api/v1alpha1/constants.go index a6240dc6..ed1a1373 100644 --- a/api/v1alpha1/constants.go +++ b/api/v1alpha1/constants.go @@ -24,9 +24,6 @@ const ( OCMConfigKey = ".ocmconfig" // OCMLabelDowngradable defines the secret. OCMLabelDowngradable = "ocm.software/ocm-k8s-toolkit/downgradable" - // OCMComponentDescriptorList defines the file name of the component descriptor list exposed as artifact by the - // component controller. - OCMComponentDescriptorList = "component-descriptor-list.yaml" ) // Log levels. @@ -37,16 +34,11 @@ const ( // Finalizers for controllers. const ( - // TODO: Remove ArtifactFinalizer. - - // ArtifactFinalizer is the finalizer that is added to artifacts created by the ocm controllers. - ArtifactFinalizer = "finalizers.ocm.software/artifact" - // SnapshotFinalizer is the finalizter that is added to snapshot created by the ocm controllers. SnapshotFinalizer = "finalizers.ocm.software/snapshot" ) -// External CRDs. +// OCI Schema. const ( - ArtifactCrd = "https://github.com/openfluxcd/artifact/releases/download/v0.1.1/openfluxcd.ocm.software_artifacts.yaml" + OCISchemaVersion = 2 ) diff --git a/api/v1alpha1/localizedresource_types.go b/api/v1alpha1/localizedresource_types.go index 395d60d4..029b6a12 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,15 +96,15 @@ 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. + // that is responsible for generating the SnapshotRef. ConfiguredResourceRef *ObjectKey `json:"configuredResourceRef,omitempty"` // ConfigRef is a reference to the Configuration that was generated by the Localization process - // and is used to setup the ConfiguredResource responsible for generating the ArtifactRef. + // and is used to setup the ConfiguredResource responsible for generating the SnapshotRef. ConfigRef *ObjectKey `json:"configRef,omitempty"` // A unique digest of the combination of the config and target resources applied through a LocalizationStrategy diff --git a/api/v1alpha1/resource_types.go b/api/v1alpha1/resource_types.go index 5e9824f4..8fd47664 100644 --- a/api/v1alpha1/resource_types.go +++ b/api/v1alpha1/resource_types.go @@ -62,15 +62,11 @@ type ResourceStatus struct { // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` - // SnapshotRef references the generated snapshot containing a list of - // component descriptors. This list can be used by other controllers to - // avoid re-downloading (and potentially also re-verifying) the components. + // SnapshotRef points to the Snapshot which represents the output of the + // last successful Resource sync. // +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/snapshot_types.go b/api/v1alpha1/snapshot_types.go index 1ae3a25f..975d545e 100644 --- a/api/v1alpha1/snapshot_types.go +++ b/api/v1alpha1/snapshot_types.go @@ -1,6 +1,8 @@ package v1alpha1 import ( + "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -12,8 +14,6 @@ type SnapshotWriter interface { client.Object GetSnapshotName() string GetKind() string - GetNamespace() string - GetName() string } // SnapshotSpec defines the desired state of Snapshot. @@ -28,7 +28,7 @@ type SnapshotSpec struct { // Blob // +required - Blob BlobInfo `json:"blob"` + Blob *BlobInfo `json:"blob"` // Suspend stops all operations on this object. // +optional @@ -40,22 +40,15 @@ type SnapshotStatus struct { // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` - // Digest is calculated by the caching layer. - // +optional - LastReconciledDigest string `json:"digest,omitempty"` - - // Tag defines the explicit tag that was used to create the related snapshot and cache entry. - // +optional - LastReconciledTag string `json:"tag,omitempty"` - // ObservedGeneration is the last reconciled generation. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` } func (in *Snapshot) GetVID() map[string]string { + vid := fmt.Sprintf("%s:%s", in.GetNamespace(), in.GetName()) metadata := make(map[string]string) - metadata[GroupVersion.Group+"/snapshot_digest"] = in.Status.LastReconciledDigest + metadata[GroupVersion.Group+"/snapshot_version"] = vid return metadata } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 541d3ca6..636a6712 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) @@ -1292,7 +1282,7 @@ func (in *Snapshot) DeepCopyInto(out *Snapshot) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -1349,7 +1339,11 @@ func (in *SnapshotList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SnapshotSpec) DeepCopyInto(out *SnapshotSpec) { *out = *in - out.Blob = in.Blob + if in.Blob != nil { + in, out := &in.Blob, &out.Blob + *out = new(BlobInfo) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotSpec. diff --git a/cmd/main.go b/cmd/main.go index 2f3c60bf..bed80e92 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,20 +22,17 @@ 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" "sigs.k8s.io/controller-runtime/pkg/webhook" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" @@ -64,24 +61,19 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(v1alpha1.AddToScheme(scheme)) - utilruntime.Must(artifactv1.AddToScheme(scheme)) // +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 + eventsAddr string + registryAddr string ) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metric endpoint binds to. "+ "Use the port :8080. If not set, it will be 0 in order to disable the metrics server") @@ -93,10 +85,8 @@ func main() { "If set the metrics endpoint is served securely") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") - flag.StringVar(&storageAddr, "storage-addr", ":9090", "The address the static file server binds to.") - flag.StringVar(&storageAdvAddr, "storage-adv-addr", "", "The advertised address of the static file server.") - flag.StringVar(&storagePath, "storage-path", "/data", "The local storage path.") flag.StringVar(&eventsAddr, "events-addr", "", "The address of the events receiver.") + flag.StringVar(®istryAddr, "registry-addr", "ocm-k8s-toolkit-zot-registry.ocm-k8s-toolkit-system.svc.cluster.local:5000", "The address of the registry.") opts := zap.Options{ Development: true, @@ -172,16 +162,7 @@ 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, err := snapshotRegistry.NewRegistry(registryAddr) registry.PlainHTTP = true if err != nil { setupLog.Error(err, "unable to initialize registry object") @@ -212,7 +193,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 +204,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 +217,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) @@ -254,6 +234,7 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Replication") os.Exit(1) } + if err = (&snapshot.Reconciler{ BaseReconciler: &ocm.BaseReconciler{ Client: mgr.GetClient(), @@ -282,10 +263,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..ce354ce4 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 @@ -170,13 +157,30 @@ spec: type: array digest: description: |- - Digest contains a technical identifier for the artifact. This technical identifier - can be used to track changes on the ArtifactRef as it is a combination of the origin + Digest contains a technical identifier for the snapshot. This technical identifier + can be used to track changes on the SnapshotRef as it is a combination of the origin ConfiguredResourceSpec.Config applied to the ConfiguredResourceSpec.Target. type: string observedGeneration: format: int64 type: integer + snapshotRef: + description: |- + The configuration reconcile loop generates a snapshot, 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..a3a814a8 100644 --- a/config/crd/bases/delivery.ocm.software_localizedresources.yaml +++ b/config/crd/bases/delivery.ocm.software_localizedresources.yaml @@ -98,17 +98,6 @@ spec: type: object status: properties: - artifactRef: - description: The LocalizedResource reports an ArtifactRef which contains - the content of the Resource after Localization - properties: - name: - type: string - namespace: - type: string - required: - - name - type: object conditions: items: description: Condition contains details for one aspect of the current @@ -168,7 +157,7 @@ spec: configRef: description: |- ConfigRef is a reference to the Configuration that was generated by the Localization process - and is used to setup the ConfiguredResource responsible for generating the ArtifactRef. + and is used to setup the ConfiguredResource responsible for generating the SnapshotRef. properties: name: type: string @@ -180,7 +169,7 @@ spec: configuredResourceRef: description: |- The LocalizedResource reports a ConfiguredResourceRef which contains a reference to the ConfiguredResource - that is responsible for generating the ArtifactRef. + that is responsible for generating the SnapshotRef. properties: name: type: string @@ -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..877d74e6 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: @@ -302,9 +286,8 @@ spec: type: object snapshotRef: description: |- - SnapshotRef references the generated snapshot containing a list of - component descriptors. This list can be used by other controllers to - avoid re-downloading (and potentially also re-verifying) the components. + SnapshotRef points to the Snapshot which represents the output of the + last successful Resource sync. properties: name: default: "" diff --git a/config/crd/bases/delivery.ocm.software_snapshots.yaml b/config/crd/bases/delivery.ocm.software_snapshots.yaml index f4de7fda..5afbb696 100644 --- a/config/crd/bases/delivery.ocm.software_snapshots.yaml +++ b/config/crd/bases/delivery.ocm.software_snapshots.yaml @@ -53,7 +53,6 @@ spec: properties: digest: description: Digest is the digest of the blob in the form of ':'. - pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ type: string size: description: |- @@ -143,17 +142,10 @@ spec: - type type: object type: array - digest: - description: Digest is calculated by the caching layer. - type: string observedGeneration: description: ObservedGeneration is the last reconciled generation. format: int64 type: integer - tag: - description: Tag defines the explicit tag that was used to create - the related snapshot and cache entry. - type: string type: object type: object served: true diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index c78c5884..c151df93 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -36,7 +36,6 @@ spec: args: # - --leader-elect We dont need it for testing - --health-probe-bind-address=:8081 - - --storage-adv-addr=ocm-k8s-toolkit-artifact-service.ocm-k8s-toolkit-system.svc.cluster.local. - --zap-log-level=4 image: controller:latest name: manager diff --git a/config/manager/service.yaml b/config/manager/service.yaml deleted file mode 100644 index 76d3cf2e..00000000 --- a/config/manager/service.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: artifact-service -spec: - type: ClusterIP - selector: - control-plane: controller-manager - ports: - - name: http - port: 80 - protocol: TCP - targetPort: http diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c96c5b39..1f312844 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -53,7 +53,6 @@ rules: - localizedresources/finalizers - ocmrepositories/finalizers - replications/finalizers - - resources/finalizers - snapshots/finalizers verbs: - update @@ -90,29 +89,3 @@ rules: - patch - update - watch -- apiGroups: - - openfluxcd.ocm.software - resources: - - artifacts - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - openfluxcd.ocm.software - resources: - - artifacts/finalizers - verbs: - - update -- apiGroups: - - openfluxcd.ocm.software - resources: - - artifacts/status - verbs: - - get - - patch - - update diff --git a/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/component/component_controller.go b/internal/controller/component/component_controller.go index ce8c0ff2..8bc05f10 100644 --- a/internal/controller/component/component_controller.go +++ b/internal/controller/component/component_controller.go @@ -26,12 +26,14 @@ import ( "github.com/fluxcd/pkg/runtime/patch" "github.com/mandelsoft/goutils/sliceutils" "github.com/opencontainers/go-digest" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" "ocm.software/ocm/api/datacontext" "ocm.software/ocm/api/ocm/resolvers" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -42,6 +44,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" @@ -57,25 +60,65 @@ var _ ocm.Reconciler = (*Reconciler)(nil) // SetupWithManager sets up the controller with the Manager. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + const ( + ocmRepositoryKey = "spec.ocmRepositoryRef.name" + ) + + // Create an index to watch for OCMRepository changes. + if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &v1alpha1.Component{}, ocmRepositoryKey, func(rawObj client.Object) []string { + component, ok := rawObj.(*v1alpha1.Component) + if !ok { + return nil + } + + ns := component.Spec.RepositoryRef.Namespace + if ns == "" { + ns = component.GetNamespace() + } + + return []string{fmt.Sprintf("%s/%s", ns, component.Spec.RepositoryRef.Name)} + }); err != nil { + return fmt.Errorf("failed setting index fields: %w", err) + } + return ctrl.NewControllerManagedBy(mgr). - // TODO: Check if we should watch for the snapshots that are created by this controller For(&v1alpha1.Component{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Watches(&v1alpha1.OCMRepository{}, handler.EnqueueRequestsFromMapFunc(r.findOCMRepositories(ocmRepositoryKey))). + Owns(&v1alpha1.Snapshot{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Complete(r) } +func (r *Reconciler) findOCMRepositories(key string) handler.MapFunc { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + repository := &v1alpha1.OCMRepositoryList{} + if err := r.List(ctx, repository, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(key, client.ObjectKeyFromObject(obj).String()), + }); err != nil { + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, len(repository.Items)) + for i, item := range repository.Items { + requests[i] = reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: item.GetName(), + Namespace: item.GetNamespace(), + }, + } + } + + return requests + } +} + // +kubebuilder:rbac:groups=delivery.ocm.software,resources=components,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=delivery.ocm.software,resources=components/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=delivery.ocm.software,resources=components/finalizers,verbs=updat +// +kubebuilder:rbac:groups=delivery.ocm.software,resources=components/finalizers,verbs=update // +kubebuilder:rbac:groups="",resources=secrets;configmaps;serviceaccounts,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch -// TODO: Remove -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=openfluxcd.ocm.software,resources=artifacts/finalizers,verbs=update - // Reconcile the component object. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, retErr error) { component := &v1alpha1.Component{} @@ -105,7 +148,7 @@ func (r *Reconciler) reconcileExists(ctx context.Context, component *v1alpha1.Co if component.GetDeletionTimestamp() != nil { logger.Info("component is being deleted and cannot be used", "name", component.Name) - return ctrl.Result{}, nil + return ctrl.Result{Requeue: true}, nil } if component.Spec.Suspend { @@ -134,6 +177,7 @@ func (r *Reconciler) reconcile(ctx context.Context, component *v1alpha1.Componen err := errors.New("repository is being deleted, please do not use it") logger.Error(err, "repository is being deleted, please do not use it", "name", component.Spec.RepositoryRef.Name) + // Triggered through cache return ctrl.Result{}, nil } @@ -145,7 +189,8 @@ func (r *Reconciler) reconcile(ctx context.Context, component *v1alpha1.Componen logger.Info("repository is not ready", "name", component.Spec.RepositoryRef.Name) status.MarkNotReady(r.EventRecorder, component, v1alpha1.RepositoryIsNotReadyReason, "repository is not ready yet") - return ctrl.Result{Requeue: true}, nil + // Triggered through cache + return ctrl.Result{}, nil } return r.reconcileOCM(ctx, component, repo) @@ -183,14 +228,16 @@ func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context if err != nil { status.MarkNotReady(r.GetEventRecorder(), component, v1alpha1.ConfigureContextFailedReason, err.Error()) - return ctrl.Result{}, nil + return ctrl.Result{}, err } + verifications, err := ocm.GetVerifications(ctx, r.GetClient(), component) if err != nil { status.MarkNotReady(r.GetEventRecorder(), component, v1alpha1.ConfigureContextFailedReason, err.Error()) - return ctrl.Result{}, nil + return ctrl.Result{}, err } + err = ocm.ConfigureContext(ctx, octx, r.GetClient(), configs, verifications) if err != nil { status.MarkNotReady(r.GetEventRecorder(), component, v1alpha1.ConfigureContextFailedReason, err.Error()) @@ -203,7 +250,7 @@ func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context logger.Error(err, "failed to parse repository spec") status.MarkNotReady(r.EventRecorder, component, v1alpha1.RepositorySpecInvalidReason, "RepositorySpec is invalid") - return ctrl.Result{}, nil + return ctrl.Result{}, err } repo, err := session.LookupRepository(octx, spec) @@ -244,9 +291,6 @@ func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context } // Store descriptors and create snapshot - // TODO: Can I check beforehand if the CD is already downloaded and in the OCI Registry (cached)? - // Compare digest/hash from manifest of the CD from the source storage - logger.Info("pushing descriptors to storage") ociRepositoryName, err := snapshot.CreateRepositoryName(component.Spec.RepositoryRef.Name, component.GetName()) if err != nil { @@ -269,21 +313,35 @@ func (r *Reconciler) reconcileComponent(ctx context.Context, octx ocmctx.Context return ctrl.Result{}, err } - manifestDigest, err := ociRepository.PushSnapshot(ctx, version, descriptorsBytes) + descriptorTGZ, err := compression.CreateTGZForData(descriptorsBytes) if err != nil { - status.MarkNotReady(r.EventRecorder, component, v1alpha1.ReconcileArtifactFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, component, v1alpha1.TGZCreationFailedReason, err.Error()) return ctrl.Result{}, err } - logger.Info("creating snapshot") - snapshotCR := snapshot.Create(component, ociRepositoryName, manifestDigest.String(), version, digest.FromBytes(descriptorsBytes).String(), int64(len(descriptorsBytes))) + manifestDigest, err := ociRepository.PushSnapshot(ctx, version, descriptorTGZ) + if err != nil { + status.MarkNotReady(r.EventRecorder, component, v1alpha1.PushSnapshotFailedReason, err.Error()) - if _, err = controllerutil.CreateOrUpdate(ctx, r.GetClient(), &snapshotCR, func() error { - if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { - if err := controllerutil.SetControllerReference(component, &snapshotCR, r.GetScheme()); err != nil { - return fmt.Errorf("failed to set controller reference: %w", err) - } + return ctrl.Result{}, err + } + + logger.Info("creating snapshot") + snapshotCR := snapshot.Create( + component, + ociRepositoryName, + manifestDigest.String(), + &v1alpha1.BlobInfo{ + Digest: digest.FromBytes(descriptorTGZ).String(), + Tag: version, + Size: int64(len(descriptorTGZ)), + }, + ) + + if _, err = controllerutil.CreateOrUpdate(ctx, r.GetClient(), snapshotCR, func() error { + if err := controllerutil.SetControllerReference(component, snapshotCR, r.GetScheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) } component.Status.SnapshotRef = corev1.LocalObjectReference{ diff --git a/internal/controller/component/component_controller_test.go b/internal/controller/component/component_controller_test.go index 3b5bda50..af0990c8 100644 --- a/internal/controller/component/component_controller_test.go +++ b/internal/controller/component/component_controller_test.go @@ -19,25 +19,25 @@ package component import ( "context" "fmt" - "io" "os" "time" + "github.com/fluxcd/pkg/apis/meta" . "github.com/mandelsoft/goutils/testutils" "github.com/mandelsoft/vfs/pkg/vfs" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" . "ocm.software/ocm/api/helper/builder" "ocm.software/ocm/api/utils/accessobj" "sigs.k8s.io/yaml" - "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" "github.com/mandelsoft/vfs/pkg/osfs" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" environment "ocm.software/ocm/api/helper/env" "ocm.software/ocm/api/ocm/extensions/repositories/ctf" "ocm.software/ocm/api/utils/accessio" @@ -45,6 +45,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest/komega" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) @@ -140,30 +141,45 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) By("checking that the component has been reconciled successfully") - Eventually(komega.Object(component), "5m").Should( - HaveField("Status.ObservedGeneration", Equal(int64(1)))) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + return conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) By("checking that the snapshot has been created successfully") - Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + Eventually(komega.Object(component), "15s").Should( + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + By("validating the snapshot") + ownersReference := snapshotComponent.GetOwnerReferences() + Expect(len(ownersReference)).To(Equal(1), "expected only one ownersReference") + Expect(ownersReference[0].Name).To(Equal(component.GetName()), "expected to be a ownersReference of the component") + By("checking that the snapshot contains the correct content") snapshotRepository := Must(registry.NewRepository(ctx, snapshotComponent.Spec.Repository)) - snapshotComponentContentReader := Must(snapshotRepository.FetchSnapshot(ctx, snapshotComponent.GetDigest())) - snapshotComponentContent := Must(io.ReadAll(snapshotComponentContentReader)) - snapshotDescriptors := &ocm.Descriptors{} - MustBeSuccessful(yaml.Unmarshal(snapshotComponentContent, snapshotDescriptors)) + snapshotComponentContent := Must(snapshotRepository.FetchSnapshot(ctx, snapshotComponent.GetDigest())) + snapshotComponentContentExtracted := Must(compression.ExtractDataFromTGZ(snapshotComponentContent)) + snapshotDescriptors := &ocm.Descriptors{} + MustBeSuccessful(yaml.Unmarshal(snapshotComponentContentExtracted, snapshotDescriptors)) repo := Must(ctf.Open(env, accessobj.ACC_WRITABLE, ctfpath, vfs.FileMode(vfs.O_RDWR), env)) cv := Must(repo.LookupComponentVersion(Component, Version1)) expectedDescriptors := Must(ocm.ListComponentDescriptors(ctx, cv, repo)) - Expect(snapshotDescriptors).To(YAMLEqual(expectedDescriptors)) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + Expect(k8sClient.Delete(ctx, snapshotComponent)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) It("does not reconcile when the repository is not ready", func() { @@ -175,7 +191,7 @@ var _ = Describe("Component Controller", func() { component := &v1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ Namespace: Namespace, - Name: ComponentObj + "-not-ready", + Name: fmt.Sprintf("%s-%d", ComponentObj, testNumber), }, Spec: v1alpha1.ComponentSpec{ RepositoryRef: v1alpha1.ObjectKey{ @@ -189,13 +205,27 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) - By("check that no snapshot has been created") - Eventually(komega.Object(component), "15s").Should( - HaveField("Status.SnapshotRef.Name", BeEmpty())) + By("checking that the component has not been reconciled successfully") + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + + return !conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) + + By("checking that the snapshot has not been created successfully") + Expect(component).To(HaveField("Status.SnapshotRef.Name", BeEmpty())) + + By("deleting the resources manually") + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) It("grabs the new version when it becomes available", func() { @@ -217,17 +247,20 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) By("checking that the component has been reconciled successfully") - Eventually(komega.Object(component), "5m").Should( - HaveField("Status.ObservedGeneration", Equal(int64(1)))) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + return conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) By("checking that the snapshot has been created successfully") - Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) - Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + Eventually(komega.Object(component), "15s").Should( + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) Expect(component.Status.Component.Version).To(Equal(Version1)) @@ -246,6 +279,14 @@ var _ = Describe("Component Controller", func() { return component.Status.Component.Version == Version2 }).WithTimeout(15 * time.Second).Should(BeTrue()) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + Expect(k8sClient.Delete(ctx, snapshotComponent)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) It("grabs lower version if downgrade is allowed", func() { @@ -280,17 +321,20 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) By("checking that the component has been reconciled successfully") - Eventually(komega.Object(component), "5m").Should( - HaveField("Status.ObservedGeneration", Equal(int64(1)))) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + return conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) By("checking that the snapshot has been created successfully") - Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) - Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + Eventually(komega.Object(component), "15s").Should( + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) Expect(component.Status.Component.Version).To(Equal("0.0.3")) @@ -303,6 +347,14 @@ var _ = Describe("Component Controller", func() { return component.Status.Component.Version == "0.0.2" }).WithTimeout(15 * time.Second).Should(BeTrue()) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + Expect(k8sClient.Delete(ctx, snapshotComponent)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) It("does not grab lower version if downgrade is denied", func() { @@ -336,17 +388,20 @@ var _ = Describe("Component Controller", func() { }, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) By("checking that the component has been reconciled successfully") - Eventually(komega.Object(component), "5m").Should( - HaveField("Status.ObservedGeneration", Equal(int64(1)))) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + return conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) By("checking that the snapshot has been created successfully") - Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) - Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + Eventually(komega.Object(component), "15s").Should( + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) Expect(component.Status.Component.Version).To(Equal("0.0.3")) @@ -360,6 +415,14 @@ var _ = Describe("Component Controller", func() { cond := conditions.Get(component, meta.ReadyCondition) return cond.Message == "terminal error: component version cannot be downgraded from version 0.0.3 to version 0.0.2" }).WithTimeout(15 * time.Second).Should(BeTrue()) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + Expect(k8sClient.Delete(ctx, snapshotComponent)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) It("can force downgrade even if not allowed by the component", func() { @@ -390,17 +453,20 @@ var _ = Describe("Component Controller", func() { Status: v1alpha1.ComponentStatus{}, } Expect(k8sClient.Create(ctx, component)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, component, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) By("checking that the component has been reconciled successfully") - Eventually(komega.Object(component), "5m").Should( - HaveField("Status.ObservedGeneration", Equal(int64(1)))) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + return conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) By("checking that the snapshot has been created successfully") - Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) - Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + Eventually(komega.Object(component), "15s").Should( + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: component.Name, Namespace: component.Namespace}, component)).To(Succeed()) Expect(component.Status.Component.Version).To(Equal("0.0.3")) @@ -413,6 +479,14 @@ var _ = Describe("Component Controller", func() { return component.Status.Component.Version == "0.0.2" }).WithTimeout(60 * time.Second).Should(BeTrue()) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + Expect(k8sClient.Delete(ctx, snapshotComponent)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) }) @@ -607,12 +681,18 @@ var _ = Describe("Component Controller", func() { Expect(k8sClient.Create(ctx, component)).To(Succeed()) By("checking that the component has been reconciled successfully") - Eventually(komega.Object(component), "5m").Should( - HaveField("Status.ObservedGeneration", Equal(int64(1)))) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + if err != nil { + return false + } + return conditions.IsReady(component) + }, "15s").WithContext(ctx).Should(BeTrue()) By("checking that the snapshot has been created successfully") - Expect(component).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) - Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + Eventually(komega.Object(component), "15s").Should( + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) Eventually(komega.Object(component), "15s").Should( HaveField("Status.EffectiveOCMConfig", ConsistOf( @@ -634,7 +714,16 @@ var _ = Describe("Component Controller", func() { }, Policy: v1alpha1.ConfigurationPolicyDoNotPropagate, }, - ))) + )), + ) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + Expect(k8sClient.Delete(ctx, snapshotComponent)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(component), component) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) }) }) }) diff --git a/internal/controller/component/suite_test.go b/internal/controller/component/suite_test.go index 96fc491e..8ab936e1 100644 --- a/internal/controller/component/suite_test.go +++ b/internal/controller/component/suite_test.go @@ -16,18 +16,15 @@ package component import ( "context" "fmt" - "net/http" "os" "os/exec" "path/filepath" "runtime" "testing" - "time" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" @@ -43,7 +40,8 @@ import ( "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + snapshotPkg "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // +kubebuilder:scaffold:imports @@ -56,7 +54,7 @@ var k8sClient client.Client var k8sManager ctrl.Manager var testEnv *envtest.Environment var zotCmd *exec.Cmd -var registry *snapshot.Registry +var registry *snapshotPkg.Registry var zotRootDir string func TestControllers(t *testing.T) { @@ -92,7 +90,6 @@ var _ = BeforeSuite(func() { DeferCleanup(testEnv.Stop) Expect(v1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) - Expect(artifactv1.AddToScheme(scheme.Scheme)).Should(Succeed()) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme @@ -110,36 +107,13 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - // Create zot-registry config file + // Setup zot registry and start it up zotRootDir = Must(os.MkdirTemp("", "")) - zotAddress := "0.0.0.0" - zotPort := "8080" - zotConfig := []byte(fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},"http":{"address":"%s","port": "%s"}}`, zotRootDir, zotAddress, zotPort)) - zotConfigFile := filepath.Join(zotRootDir, "config.json") - MustBeSuccessful(os.WriteFile(zotConfigFile, zotConfig, 0644)) - - // Start zot-registry - zotCmd = exec.Command(filepath.Join("..", "..", "..", "bin", "zot-registry"), "serve", zotConfigFile) - err = zotCmd.Start() - Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to start Zot")) - - // Wait for Zot to be ready - Eventually(func() error { - resp, err := http.Get(fmt.Sprintf("http://%s:%s/v2/", zotAddress, zotPort)) - if err != nil { - return fmt.Errorf("could not connect to Zot") - } - - defer resp.Body.Close() - if resp.StatusCode != 200 { - return fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - return nil - }, 30*time.Second, 1*time.Second).Should(Succeed(), "Zot registry did not start in time") - - registry, err = snapshot.NewRegistry(fmt.Sprintf("%s:%s", zotAddress, zotPort)) - registry.PlainHTTP = true + DeferCleanup(func() { + Expect(os.RemoveAll(zotRootDir)).To(Succeed()) + }) + + zotCmd, registry = test.SetupRegistry(filepath.Join("..", "..", "..", "bin", "zot-registry"), zotRootDir, "0.0.0.0", "8080") Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ @@ -170,11 +144,6 @@ var _ = BeforeSuite(func() { }) 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)) - } + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") }) diff --git a/internal/controller/configuration/client/client.go b/internal/controller/configuration/client/client.go index 68755254..5db02281 100644 --- a/internal/controller/configuration/client/client.go +++ b/internal/controller/configuration/client/client.go @@ -4,14 +4,13 @@ import ( "context" "fmt" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/configuration/types" - artifactutil "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) type Client interface { @@ -24,24 +23,24 @@ type Client interface { GetTarget(ctx context.Context, ref v1alpha1.ConfigurationReference) (target types.ConfigurationTarget, err error) } -func NewClientWithLocalStorage(r client.Reader, s *storage.Storage, scheme *runtime.Scheme) Client { +func NewClientWithRegistry(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 +56,7 @@ func (clnt *localStorageBackedClient) GetTarget(ctx context.Context, ref v1alpha case v1alpha1.KindLocalizedResource: fallthrough case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return snapshotRegistry.GetContentBackedBySnapshotFromComponent(ctx, clnt.Client, clnt.Registry, &ref) default: return nil, fmt.Errorf("unsupported configuration target kind: %s", ref.Kind) } @@ -66,9 +65,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 snapshotRegistry.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) } @@ -90,5 +89,5 @@ func GetResourceConfigFromKubernetes(ctx context.Context, clnt client.Reader, en return nil, fmt.Errorf("failed to fetch localization config %s: %w", reference.Name, err) } - return &artifactutil.ObjectConfig{Object: &cfg, Encoder: encoder}, nil + return &snapshotRegistry.ObjectConfig{Object: &cfg, Encoder: encoder}, nil } diff --git a/internal/controller/configuration/configuration_controller.go b/internal/controller/configuration/configuration_controller.go index 5fdd2a22..aa3efb8e 100644 --- a/internal/controller/configuration/configuration_controller.go +++ b/internal/controller/configuration/configuration_controller.go @@ -20,25 +20,30 @@ import ( "context" "errors" "fmt" + "io" "os" + "path/filepath" + "strings" "github.com/fluxcd/pkg/runtime/patch" - "github.com/openfluxcd/controller-manager/storage" + "github.com/opencontainers/go-digest" "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" configurationclient "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/configuration/client" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" "github.com/open-component-model/ocm-k8s-toolkit/pkg/index" "github.com/open-component-model/ocm-k8s-toolkit/pkg/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 +56,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 +74,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 +90,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 +101,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 +125,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,28 +151,35 @@ func (r *Reconciler) reconcileExists(ctx context.Context, configuration *v1alpha return ctrl.Result{}, fmt.Errorf("failed to fetch cfg: %w", err) } - digest, revision, filename, err := artifact.UniqueIDsForArtifactContentCombination(cfg, target) + combinedDigest, revision, _, err := snapshotRegistry.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) + return ctrl.Result{}, fmt.Errorf("failed to map combinedDigest from config to target: %w", err) } - logger.V(1).Info("verifying configuration", "digest", digest, "revision", revision) - hasValidArtifact, err := ocm.ValidateArtifactForCollectable( + // TODO: we cannot use `combinedDigest` to determine a change as the combinedDigest calculation is incorrect (it takes a k8s object + // with managed fields that change on every update). + // Thus, for the moment, we will check on the status + + // Check if a snapshot of the configuration resource already exists and if it holds the same calculated combinedDigest + // from above + logger.V(1).Info("verifying configuration", "combinedDigest", combinedDigest, "revision", revision) + hasValidSnapshot, err := ocm.ValidateSnapshotForOwner( ctx, r.Client, - r.Storage, + r.Registry, configuration, - digest, + combinedDigest, ) 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 - if !hasValidArtifact { - logger.V(1).Info("configuring", "digest", digest, "revision", revision) + // TODO: Cleanup + //nolint:nestif // TODO: Add description + if !hasValidSnapshot { + logger.V(1).Info("configuring", "combinedDigest", combinedDigest, "revision", revision) basePath, err := os.MkdirTemp("", "configured-") if err != nil { return ctrl.Result{}, fmt.Errorf("failed to create temporary directory to perform configuration: %w", err) @@ -192,43 +190,97 @@ 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) + } + + gzipReader, err := compression.AutoCompressAsGzip(ctx, tarFile) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to gzip: %w", err) + } + + data, err := io.ReadAll(gzipReader) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to read gzip: %w", err) + } + + repositoryName, err := snapshotRegistry.CreateRepositoryName(configuration.GetName()) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.CreateOCIRepositoryNameFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to create repository name: %w", err) + } + + repository, err := r.Registry.NewRepository(ctx, repositoryName) + if err != nil { + status.MarkNotReady(r.EventRecorder, configuration, v1alpha1.ConfigurationFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to configure: %w", err) + } + + // TODO: Which version to use?! + // The tag is not relevant for the user, but relevant for the Deployer as it must point to the right artifact + // This digest is not accepted by flux + // When the resource is a HelmChart - we must tag the OCI artifact with the helmchart version. Otherwise + // FluxCDs OCIRepository > HelmRelease won't work + tagDigestAlgo := digest.FromBytes(data).String() + tagDigest := strings.Split(tagDigestAlgo, ":")[1] + + manifestDigest, err := repository.PushSnapshot(ctx, tagDigest, 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 combinedDigest calculated above for the blob-info combinedDigest, so we can compare for any changes + snapshotCR := snapshotRegistry.Create( + configuration, + repositoryName, + manifestDigest.String(), + &v1alpha1.BlobInfo{ + Digest: combinedDigest, + Tag: tagDigest, + Size: 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 = combinedDigest + + 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..3e502331 100644 --- a/internal/controller/configuration/configuration_controller_test.go +++ b/internal/controller/configuration/configuration_controller_test.go @@ -2,11 +2,13 @@ package configuration import ( "context" + "os" "path/filepath" "time" _ "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 +17,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 +24,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,9 +67,16 @@ var _ = Describe("ConfiguredResource Controller", func() { fileContentAfterConfiguration := []byte(`mykey: "substituted"`) dir := filepath.Join(tmp, "test") - test.CreateTGZ(dir, map[string][]byte{ - fileToConfigure: fileContentBeforeConfiguration, - }) + Expect(os.Mkdir(dir, os.ModePerm|os.ModeDir)).To(Succeed()) + + path := filepath.Join(dir, fileToConfigure) + + writer := Must(os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.ModePerm)) + defer func() { + Expect(writer.Close()).To(Succeed()) + }() + + Must(writer.Write(fileContentBeforeConfiguration)) targetResource := test.SetupMockResourceWithData(ctx, TargetResourceObj, @@ -79,7 +88,7 @@ var _ = Describe("ConfiguredResource Controller", func() { Namespace: Namespace, Name: component.GetName(), }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, @@ -135,13 +144,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.VerifySnapshot(ctx, registry, snapshotCR, map[string]func(data []byte){ fileToConfigure: func(data []byte) { Expect(data).To(MatchYAML(fileContentAfterConfiguration)) }, @@ -157,7 +164,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/configure.go b/internal/controller/configuration/configure.go index 8d910121..f267eeac 100644 --- a/internal/controller/configuration/configure.go +++ b/internal/controller/configuration/configure.go @@ -18,7 +18,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/substitute" "github.com/open-component-model/ocm-k8s-toolkit/pkg/substitute/steps" "github.com/open-component-model/ocm-k8s-toolkit/pkg/util" @@ -55,7 +55,7 @@ func Configure(ctx context.Context, ) (string, error) { logger := log.FromContext(ctx) targetDir := filepath.Join(basePath, "target") - if err := target.UnpackIntoDirectory(targetDir); errors.Is(err, artifact.ErrAlreadyUnpacked) { + if err := target.UnpackIntoDirectory(targetDir); errors.Is(err, snapshot.ErrAlreadyUnpacked) { logger.Info("target was already present, reusing existing directory", "path", targetDir) } else if err != nil { return "", fmt.Errorf("failed to get target directory: %w", err) diff --git a/internal/controller/configuration/suite_test.go b/internal/controller/configuration/suite_test.go index 04f11f25..b6b993b9 100644 --- a/internal/controller/configuration/suite_test.go +++ b/internal/controller/configuration/suite_test.go @@ -16,36 +16,32 @@ package configuration import ( "context" "fmt" - "io" - "net/http" + "os" + "os/exec" "path/filepath" "runtime" "testing" - "time" + . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/openfluxcd/controller-manager/server" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/yaml" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" metricserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" cfgclient "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/configuration/client" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // +kubebuilder:scaffold:imports @@ -53,16 +49,14 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -const ( - ARTIFACT_SERVER = "localhost:0" -) - var cfg *rest.Config var k8sClient client.Client var k8sManager ctrl.Manager var testEnv *envtest.Environment -var strg *storage.Storage var recorder record.EventRecorder +var zotCmd *exec.Cmd +var registry *snapshot.Registry +var zotRootDir string func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -75,26 +69,10 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") - // Get external artifact CRD - resp, err := http.Get(v1alpha1.ArtifactCrd) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() error { - return resp.Body.Close() - }) - - crdByte, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - - artifactCRD := &apiextensionsv1.CustomResourceDefinition{} - err = yaml.Unmarshal(crdByte, artifactCRD) - Expect(err).NotTo(HaveOccurred()) - testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, - CRDs: []*apiextensionsv1.CustomResourceDefinition{artifactCRD}, - // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the // default path defined in controller-runtime which is /usr/local/kubebuilder/. @@ -104,6 +82,8 @@ var _ = BeforeSuite(func() { fmt.Sprintf("1.30.0-%s-%s", runtime.GOOS, runtime.GOARCH)), } + var err error + // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) @@ -111,7 +91,6 @@ var _ = BeforeSuite(func() { DeferCleanup(testEnv.Stop) Expect(v1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) - Expect(artifactv1.AddToScheme(scheme.Scheme)).Should(Succeed()) Expect(err).NotTo(HaveOccurred()) @@ -130,37 +109,38 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - tmpdir := GinkgoT().TempDir() - Expect(err).ToNot(HaveOccurred()) - address := ARTIFACT_SERVER - strg, err = server.NewStorage(k8sClient, testEnv.Scheme, tmpdir, address, 0, 0) - Expect(err).ToNot(HaveOccurred()) - artifactServer, err := server.NewArtifactServer(tmpdir, address, time.Millisecond) - Expect(err).ToNot(HaveOccurred()) - recorder = &record.FakeRecorder{ Events: make(chan string, 32), IncludeObject: true, } + // Setup zot registry and start it up + zotRootDir = Must(os.MkdirTemp("", "")) + DeferCleanup(func() { + Expect(os.RemoveAll(zotRootDir)).To(Succeed()) + }) + + zotCmd, registry = test.SetupRegistry(filepath.Join("..", "..", "..", "bin", "zot-registry"), zotRootDir, "0.0.0.0", "8083") + Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ Client: k8sClient, Scheme: testEnv.Scheme, EventRecorder: recorder, }, - ConfigClient: cfgclient.NewClientWithLocalStorage(k8sClient, strg, scheme.Scheme), - Storage: strg, + ConfigClient: cfgclient.NewClientWithRegistry(k8sClient, registry, scheme.Scheme), + Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) ctx, cancel := context.WithCancel(context.Background()) DeferCleanup(cancel) - go func() { - defer GinkgoRecover() - Expect(artifactServer.Start(ctx)).To(Succeed()) - }() go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) }() }) + +var _ = AfterSuite(func() { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") +}) diff --git a/internal/controller/configuration/types/configuration_reference.go b/internal/controller/configuration/types/configuration_reference.go index d6d46aab..42b5f8bb 100644 --- a/internal/controller/configuration/types/configuration_reference.go +++ b/internal/controller/configuration/types/configuration_reference.go @@ -1,6 +1,8 @@ package types -import "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" +import ( + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" +) // ConfigurationReference can be used both as a source (ConfigurationSource), // and as a target (ConfigurationTarget) for configuration. @@ -10,12 +12,12 @@ type ConfigurationReference interface { } // ConfigurationSource is a source of localization. -// It contains instructions on how to localize an artifact.Content. +// It contains instructions on how to localize an snapshot.Content. type ConfigurationSource interface { - artifact.Content + snapshot.Content } // ConfigurationTarget is a target for configuration. type ConfigurationTarget interface { - artifact.Content + snapshot.Content } diff --git a/internal/controller/localization/client/client.go b/internal/controller/localization/client/client.go index 40f53fdf..4d36f318 100644 --- a/internal/controller/localization/client/client.go +++ b/internal/controller/localization/client/client.go @@ -4,14 +4,13 @@ import ( "context" "fmt" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/localization/types" - artifactutil "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" ) type Client interface { @@ -27,24 +26,24 @@ type Client interface { GetLocalizationConfig(ctx context.Context, ref v1alpha1.ConfigurationReference) (source types.LocalizationConfig, err error) } -func NewClientWithLocalStorage(r client.Reader, s *storage.Storage, scheme *runtime.Scheme) Client { +func NewClientWithRegistry(c client.Client, registry *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 +62,7 @@ func (clnt *localStorageBackedClient) GetLocalizationTarget( case v1alpha1.KindLocalizedResource: fallthrough case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return snapshot.GetContentBackedBySnapshotFromComponent(ctx, clnt.Client, clnt.Registry, &ref) default: return nil, fmt.Errorf("unsupported localization target kind: %s", ref.Kind) } @@ -75,9 +74,9 @@ func (clnt *localStorageBackedClient) GetLocalizationConfig( ) (types.LocalizationConfig, error) { switch ref.Kind { case v1alpha1.KindResource: - return artifactutil.GetContentBackedByArtifactFromComponent(ctx, clnt.Reader, clnt.Storage, &ref) + return snapshot.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) } @@ -99,5 +98,5 @@ func GetLocalizationConfigFromKubernetes(ctx context.Context, clnt client.Reader return nil, fmt.Errorf("failed to fetch localization config %s: %w", reference.Name, err) } - return &artifactutil.ObjectConfig{Object: &cfg, Encoder: encoder}, nil + return &snapshot.ObjectConfig{Object: &cfg, Encoder: encoder}, nil } diff --git a/internal/controller/localization/localization_controller.go b/internal/controller/localization/localization_controller.go index 1cd8c4f0..66c62717 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" @@ -32,9 +30,9 @@ import ( "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" localizationclient "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/localization/client" "github.com/open-component-model/ocm-k8s-toolkit/internal/controller/localization/types" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" "github.com/open-component-model/ocm-k8s-toolkit/pkg/index" "github.com/open-component-model/ocm-k8s-toolkit/pkg/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 +40,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 +82,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 +93,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 +120,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 +131,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 +150,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,37 +230,36 @@ 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 }) if err != nil { - status.MarkNotReady(r.EventRecorder, localization, v1alpha1.ReconcileArtifactFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, localization, v1alpha1.CreateSnapshotFailedReason, err.Error()) 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 +274,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 +286,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,28 +333,35 @@ 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 { - artifact.Content +type LocalizableSnapshotContent interface { + snapshotRegistry.Content GetComponent() *v1alpha1.Component GetResource() *v1alpha1.Resource } 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..0692fe1f 100644 --- a/internal/controller/localization/localization_controller_test.go +++ b/internal/controller/localization/localization_controller_test.go @@ -3,12 +3,12 @@ package localization import ( "bytes" "context" - "os" "path/filepath" "text/template" _ "embed" + . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" @@ -21,14 +21,13 @@ import ( "ocm.software/ocm/api/utils/tarutils" "sigs.k8s.io/controller-runtime/pkg/client" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ocmbuilder "ocm.software/ocm/api/helper/builder" environment "ocm.software/ocm/api/helper/env" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) @@ -71,24 +70,14 @@ var _ = Describe("Localization Controller", func() { DeferCleanup(env.Cleanup) }) - BeforeEach(func(ctx SpecContext) { - By("creating namespace object") - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: Namespace, - }, - } - Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) - }) - - It("should localize an artifact from a resource based on a config supplied in a sibling resource", func(ctx SpecContext) { + It("should localize an snapshot from a resource based on a config supplied in a sibling resource", func(ctx SpecContext) { component := test.SetupComponentWithDescriptorList(ctx, ComponentObj, Namespace, descriptorListYAML, &test.MockComponentOptions{ BasePath: tmp, - Strg: strg, + Registry: registry, Client: k8sClient, Recorder: recorder, Info: v1alpha1.ComponentInfo{ @@ -114,7 +103,7 @@ var _ = Describe("Localization Controller", func() { Namespace: Namespace, Name: ComponentObj, }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, @@ -133,7 +122,7 @@ var _ = Describe("Localization Controller", func() { Namespace: Namespace, Name: ComponentObj, }, - Strg: strg, + Registry: registry, Clnt: k8sClient, Recorder: recorder, }, @@ -153,24 +142,22 @@ var _ = Describe("Localization Controller", func() { }) Eventually(Object(localization), "15s").Should( - HaveField("Status.ArtifactRef.Name", Not(BeEmpty()))) + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + + snapshotCR := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, localization)) - art := &artifactv1.Artifact{} - art.Name = localization.Status.ArtifactRef.Name - art.Namespace = localization.Namespace + // TODO: Clean up + // field not present any more.. what was its purpose? + //Eventually(Object(snapshotCR), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) + repository, err := registry.NewRepository(ctx, snapshotCR.Spec.Repository) + Expect(err).ToNot(HaveOccurred()) - localized := strg.LocalPath(art) - Expect(localized).To(BeAnExistingFile()) + data, err := repository.FetchSnapshot(ctx, snapshotCR.GetDigest()) + Expect(err).ToNot(HaveOccurred()) memFs := vfs.New(memoryfs.New()) - localizedArchiveData, err := os.OpenFile(localized, os.O_RDONLY, 0o600) - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(func() { - Expect(localizedArchiveData.Close()).To(Succeed()) - }) - Expect(tarutils.UnzipTarToFs(memFs, localizedArchiveData)).To(Succeed()) + Expect(tarutils.UnzipTarToFs(memFs, bytes.NewReader(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..ce603caa 100644 --- a/internal/controller/localization/suite_test.go +++ b/internal/controller/localization/suite_test.go @@ -16,31 +16,27 @@ package localization import ( "context" "fmt" - "io" - "net/http" + "os" + "os/exec" "path/filepath" "runtime" "testing" - "time" + . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/openfluxcd/controller-manager/server" - "github.com/openfluxcd/controller-manager/storage" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/yaml" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" metricserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" @@ -48,6 +44,8 @@ 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" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // +kubebuilder:scaffold:imports @@ -55,16 +53,16 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -const ( - ARTIFACT_SERVER = "localhost:0" -) - var cfg *rest.Config var k8sClient client.Client var k8sManager ctrl.Manager var testEnv *envtest.Environment -var strg *storage.Storage var recorder record.EventRecorder +var zotCmd *exec.Cmd +var registry *snapshot.Registry +var zotRootDir string +var ctx context.Context +var cancel context.CancelFunc func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -77,26 +75,10 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") - // Get external artifact CRD - resp, err := http.Get(v1alpha1.ArtifactCrd) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() error { - return resp.Body.Close() - }) - - crdByte, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - - artifactCRD := &apiextensionsv1.CustomResourceDefinition{} - err = yaml.Unmarshal(crdByte, artifactCRD) - Expect(err).NotTo(HaveOccurred()) - testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, - CRDs: []*apiextensionsv1.CustomResourceDefinition{artifactCRD}, - // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the // default path defined in controller-runtime which is /usr/local/kubebuilder/. @@ -107,13 +89,12 @@ var _ = BeforeSuite(func() { } // cfg is defined in this file globally. - cfg, err = testEnv.Start() + cfg, err := testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) DeferCleanup(testEnv.Stop) Expect(v1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) - Expect(artifactv1.AddToScheme(scheme.Scheme)).Should(Succeed()) Expect(err).NotTo(HaveOccurred()) @@ -132,27 +113,40 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - tmpdir := GinkgoT().TempDir() - Expect(err).ToNot(HaveOccurred()) - address := ARTIFACT_SERVER - strg, err = server.NewStorage(k8sClient, testEnv.Scheme, tmpdir, address, 0, 0) - Expect(err).ToNot(HaveOccurred()) - artifactServer, err := server.NewArtifactServer(tmpdir, address, time.Millisecond) - Expect(err).ToNot(HaveOccurred()) - recorder = &record.FakeRecorder{ Events: make(chan string, 32), IncludeObject: true, } + // Setup zot registry and start it up + zotRootDir = Must(os.MkdirTemp("", "")) + DeferCleanup(func() { + Expect(os.RemoveAll(zotRootDir)).To(Succeed()) + }) + + zotCmd, registry = test.SetupRegistry(filepath.Join("..", "..", "..", "bin", "zot-registry"), zotRootDir, "0.0.0.0", "8082") + + ctx, cancel = context.WithCancel(context.Background()) + DeferCleanup(cancel) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: Namespace, + }, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, namespace, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + }) + Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ Client: k8sClient, Scheme: testEnv.Scheme, EventRecorder: recorder, }, - LocalizationClient: locclient.NewClientWithLocalStorage(k8sClient, strg, scheme.Scheme), - Storage: strg, + LocalizationClient: locclient.NewClientWithRegistry(k8sClient, registry, scheme.Scheme), + Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) Expect((&configuration.Reconciler{ @@ -161,18 +155,19 @@ var _ = BeforeSuite(func() { Scheme: testEnv.Scheme, EventRecorder: recorder, }, - ConfigClient: cfgclient.NewClientWithLocalStorage(k8sClient, strg, scheme.Scheme), - Storage: strg, + ConfigClient: cfgclient.NewClientWithRegistry(k8sClient, registry, scheme.Scheme), + Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) ctx, cancel := context.WithCancel(context.Background()) DeferCleanup(cancel) - go func() { - defer GinkgoRecover() - Expect(artifactServer.Start(ctx)).To(Succeed()) - }() go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) }() }) + +var _ = AfterSuite(func() { + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") +}) diff --git a/internal/controller/localization/types/localization_reference.go b/internal/controller/localization/types/localization_reference.go index 40253ad3..61764d19 100644 --- a/internal/controller/localization/types/localization_reference.go +++ b/internal/controller/localization/types/localization_reference.go @@ -1,6 +1,8 @@ package types -import "github.com/open-component-model/ocm-k8s-toolkit/pkg/artifact" +import ( + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" +) // LocalizationReference can be used both as a source (LocalizationConfig), // and as a target (LocalizationTarget) for localization. @@ -9,12 +11,12 @@ type LocalizationReference interface { LocalizationTarget } -// LocalizationConfig is a configuration on how to localize an artifact.Content. +// LocalizationConfig is a configuration on how to localize an snapshot.Content. type LocalizationConfig interface { - artifact.Content + snapshot.Content } -// LocalizationTarget is a target artifact.Content for localization. +// LocalizationTarget is a target snapshot.Content for localization. type LocalizationTarget interface { - artifact.Content + snapshot.Content } diff --git a/internal/controller/resource/resource_controller.go b/internal/controller/resource/resource_controller.go index c93101d4..44672632 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" @@ -49,14 +48,13 @@ import ( "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" - snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" ) type Reconciler struct { *ocm.BaseReconciler - Storage *storage.Storage - Registry snapshotRegistry.RegistryType + Registry snapshot.RegistryType } var _ ocm.Reconciler = (*Reconciler)(nil) @@ -78,7 +76,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Resource{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). // Watch for snapshot-events that are owned by the resource controller - Owns(&v1alpha1.Snapshot{}). + Owns(&v1alpha1.Snapshot{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). // Watch for component-events that are referenced by resources Watches( &v1alpha1.Component{}, @@ -196,7 +194,7 @@ func (r *Reconciler) reconcileOCM(ctx context.Context, resource *v1alpha1.Resour return result, nil } -//nolint:funlen // we do not want to cut function at an arbitrary point +//nolint:funlen,cyclop,maintidx // we do not want to cut function at an arbitrary point func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, resource *v1alpha1.Resource, component *v1alpha1.Component) (ctrl.Result, error) { logger := log.FromContext(ctx) logger.V(1).Info("reconciling resource") @@ -211,6 +209,7 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, return ctrl.Result{}, err } + err = ocm.ConfigureContext(ctx, octx, r.GetClient(), configs) if err != nil { status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.ConfigureContextFailedReason, err.Error()) @@ -219,23 +218,29 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, } // Get snapshot from component that contains component descriptor - componentSnapshot, err := snapshotRegistry.GetSnapshotForOwner(ctx, r.Client, component) + componentSnapshot, err := snapshot.GetSnapshotForOwner(ctx, r.Client, component) if err != nil { status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.GetSnapshotFailedReason, err.Error()) - return ctrl.Result{}, nil + return ctrl.Result{}, err + } + + if !conditions.IsReady(componentSnapshot) { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.SnapshotReadyFailedReason, "snapshot not ready") + + return ctrl.Result{}, errors.New("snapshot not ready") } // 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 + return ctrl.Result{}, err } // 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()) @@ -278,65 +283,124 @@ func (r *Reconciler) reconcileResource(ctx context.Context, octx ocmctx.Context, return ctrl.Result{}, err } - // TODO: - // Problem: Do not re-download resources that are already present in the OCI registry - // Resolution: - // - Use resource-access-digest as OCI repository name - // - Check if OCI repository name exists - // - If yes, create manifest and point to the previous OCI layer blob - // - How? - - // Get resource content - // No need to close the blob access as it will be closed automatically - blobAccess, err := getBlobAccess(ctx, resourceAccess) - if err != nil { - status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetBlobAccessFailedReason, err.Error()) - - return ctrl.Result{}, err - } - if err := verifyResource(ctx, resourceAccess, cv, cd); err != nil { status.MarkNotReady(r.EventRecorder, resource, v1alpha1.VerifyResourceFailedReason, err.Error()) return ctrl.Result{}, err } - resourceContent, err := blobAccess.Get() + // TODO: + // Problem: The current implementation would download the resource every reconcile-loop. This could be + // expensive depending on the resource. + // Additionally, there could be two different components that contain the same resource. Also in this case + // we would re-download the resource, even though it is already in the OCI registry. + // To circumvent this problem, we could use the digest provided by the resource-access (prior to the resource + // download) as the repository-name. Then, we could check if such a repository for the resource already + // exists. + // If so, we cannot not return immediately because we need to create a manifest-file to point to the already + // present resource-layer. Otherwise the GC would delete the resource-layer if the previously present manifest + // would be deleted. + + // The digest from the resource access is used, so it can be used to compare resource with the same name/identity + // on a digest-level. + repositoryResourceName := resourceAccess.Meta().Digest.Value + repositoryResource, err := r.Registry.NewRepository(ctx, repositoryResourceName) if err != nil { - status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetResourceFailedReason, err.Error()) + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.CreateOCIRepositoryFailedReason, err.Error()) return ctrl.Result{}, err } - // Create OCI repository - repositoryResourceName := resourceAccess.Meta().Digest.Value - repositoryResource, err := r.Registry.NewRepository(ctx, repositoryResourceName) + var ( + manifestDigest digest.Digest + blobSize int64 + ) + + // If the resource is of type 'ociArtifact' or its access type is 'ociArtifact', the resource will be copied to the + // internal OCI registry + logger.Info("create snapshot for resource", "name", resource.GetName(), "type", resourceAccess.Meta().GetType()) + resourceAccessSpec, err := resourceAccess.Access() if err != nil { - status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.GetComponentVersionFailedReason, err.Error()) + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.PushSnapshotFailedReason, err.Error()) return ctrl.Result{}, err } - // Push resource to OCI repository - manifestDigest, err := repositoryResource.PushSnapshot(ctx, resourceAccess.Meta().GetVersion(), resourceContent) - if err != nil { - status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.PushSnapshotFailedReason, err.Error()) + if resourceAccessSpec == nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.PushSnapshotFailedReason, "access spec is nil") return ctrl.Result{}, err } + if resourceAccessSpec.GetType() == "ociArtifact" { + manifestDigest, err = repositoryResource.CopySnapshotForResourceAccess(ctx, resourceAccess) + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.PushSnapshotFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + // TODO: How to get the blob size, without downloading the resource? + // Do we need the blob-size, when we copy the resource either way? + // We could use the size stored in the manifest. + blobSize = 0 + } else { + // Get resource content + // No need to close the blob access as it will be closed automatically + blobAccess, err := getBlobAccess(ctx, resourceAccess) + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetBlobAccessFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + resourceContent, err := blobAccess.Get() + if err != nil { + status.MarkNotReady(r.EventRecorder, resource, v1alpha1.GetResourceFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + // is gzip? + // If yes, + // extract and check if tarred + // if not tarred + // tar and gzip + // if tarred + // perfect + // If no + // check if tarred + // if not + // tar and gzip + // If yes + // gzip + + // Push resource to OCI repository + manifestDigest, err = repositoryResource.PushSnapshot(ctx, resourceAccess.Meta().GetVersion(), resourceContent) + if err != nil { + status.MarkNotReady(r.GetEventRecorder(), resource, v1alpha1.PushSnapshotFailedReason, err.Error()) + + return ctrl.Result{}, err + } + + blobSize = int64(len(resourceContent)) + } + // Create respective snapshot CR - snapshotCR := snapshotRegistry.Create( + snapshotCR := snapshot.Create( resource, repositoryResourceName, manifestDigest.String(), - resourceAccess.Meta().GetVersion(), - digest.FromBytes(resourceContent).String(), - int64(len(resourceContent))) - - if _, err = controllerutil.CreateOrUpdate(ctx, r.GetClient(), &snapshotCR, func() error { + &v1alpha1.BlobInfo{ + Digest: resourceAccess.Meta().Digest.Value, + Tag: resourceAccess.Meta().GetVersion(), + Size: blobSize, + }, + ) + + if _, err = controllerutil.CreateOrUpdate(ctx, r.GetClient(), snapshotCR, func() error { if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { - if err := controllerutil.SetControllerReference(resource, &snapshotCR, r.GetScheme()); err != nil { + if err := controllerutil.SetControllerReference(resource, snapshotCR, r.GetScheme()); err != nil { return fmt.Errorf("failed to set controller reference: %w", err) } } @@ -347,14 +411,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/internal/controller/resource/resource_controller_test.go b/internal/controller/resource/resource_controller_test.go index ff66484f..0bc4a70e 100644 --- a/internal/controller/resource/resource_controller_test.go +++ b/internal/controller/resource/resource_controller_test.go @@ -18,42 +18,44 @@ package resource import ( "context" + _ "embed" "fmt" - "io" "os" - "path/filepath" - "time" + "github.com/fluxcd/pkg/runtime/conditions" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/opencontainers/go-digest" + "k8s.io/apimachinery/pkg/api/errors" . "ocm.software/ocm/api/helper/builder" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + "ocm.software/ocm/api/utils/mime" + "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "github.com/fluxcd/pkg/runtime/conditions" "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" "ocm.software/ocm/api/ocm/extensions/artifacttypes" "ocm.software/ocm/api/ocm/extensions/repositories/ctf" "ocm.software/ocm/api/utils/accessio" - "ocm.software/ocm/api/utils/accessobj" - "ocm.software/ocm/api/utils/mime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + + "ocm.software/ocm/api/utils/accessobj" "sigs.k8s.io/yaml" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" environment "ocm.software/ocm/api/helper/env" v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" + ocmPkg "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) +var () + const ( CTFPath = "ocm-k8s-ctfstore--*" Namespace = "test-namespace" @@ -68,151 +70,250 @@ const ( var _ = Describe("Resource Controller", func() { var ( - ctx context.Context - cancel context.CancelFunc - env *Builder - ctfPath string + env *Builder + resourceLocalPath string + testNumber int ) + BeforeEach(func() { - ctfPath = Must(os.MkdirTemp("", CTFPath)) + resourceLocalPath = Must(os.MkdirTemp("", CTFPath)) DeferCleanup(func() error { - return os.RemoveAll(ctfPath) + return os.RemoveAll(resourceLocalPath) }) env = NewBuilder(environment.FileSystem(osfs.OsFs)) DeferCleanup(env.Cleanup) - - ctx, cancel = context.WithCancel(context.Background()) - DeferCleanup(cancel) + testNumber++ }) Context("resource controller", func() { - It("can reconcile a resource", func() { - By("creating namespace object") - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: Namespace, - }, - } - Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + It("can reconcile a resource: PlainText", func() { + testComponent := fmt.Sprintf("%s-%d", ComponentObj, testNumber) + testResource := fmt.Sprintf("%s-%d", ResourceObj, testNumber) + resourceType := artifacttypes.PLAIN_TEXT + + By("creating an ocm resource from a plain text") + env.OCMCommonTransport(resourceLocalPath, accessio.FormatDirectory, func() { + env.Component(Component, func() { + env.Version(ComponentVersion, func() { + env.Resource(testResource, ResourceVersion, resourceType, v1.LocalRelation, func() { + env.BlobData(mime.MIME_TEXT, []byte(ResourceContent)) + }) + }) + }) + }) + + repo, err := ctf.Open(env, accessobj.ACC_WRITABLE, resourceLocalPath, vfs.FileMode(vfs.O_RDWR), env) + Expect(err).NotTo(HaveOccurred()) + cv, err := repo.LookupComponentVersion(Component, ComponentVersion) + Expect(err).NotTo(HaveOccurred()) + cd, err := ocmPkg.ListComponentDescriptors(ctx, cv, repo) + Expect(err).NotTo(HaveOccurred()) + dataCds, err := yaml.Marshal(cd) + Expect(err).NotTo(HaveOccurred()) + + spec, err := ctf.NewRepositorySpec(ctf.ACC_READONLY, resourceLocalPath) + specData, err := spec.MarshalJSON() - By("preparing a mock component") - prepareComponent(ctx, env, ctfPath) + By("creating a mocked component") + component := test.SetupComponentWithDescriptorList(ctx, testComponent, Namespace, dataCds, &test.MockComponentOptions{ + BasePath: "", + Registry: registry, + Client: k8sClient, + Recorder: recorder, + Info: v1alpha1.ComponentInfo{ + Component: Component, + Version: ComponentVersion, + RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, + }, + Repository: RepositoryObj, + }) By("creating a resource object") resource := &v1alpha1.Resource{ - ObjectMeta: metav1.ObjectMeta{ + ObjectMeta: k8smetav1.ObjectMeta{ Namespace: Namespace, - Name: ResourceObj, + Name: testResource, }, Spec: v1alpha1.ResourceSpec{ ComponentRef: corev1.LocalObjectReference{ - Name: ComponentObj, + Name: testComponent, }, Resource: v1alpha1.ResourceID{ ByReference: v1alpha1.ResourceReference{ - Resource: v1.NewIdentity(ResourceObj), + Resource: v1.NewIdentity(testResource), }, }, - Interval: metav1.Duration{Duration: time.Minute * 5}, }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, resource, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) - }) By("checking that the resource has been reconciled successfully") - Eventually(komega.Object(resource), "5m").Should( - HaveField("Status.ObservedGeneration", Equal(int64(1)))) - Expect(resource).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) - Expect(resource).To(HaveField("Status.Resource.Name", Equal(ResourceObj))) - Expect(resource).To(HaveField("Status.Resource.Type", Equal(artifacttypes.PLAIN_TEXT))) - Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + if err != nil { + return false + } + return conditions.IsReady(resource) + }, "15s").WithContext(ctx).Should(BeTrue()) By("checking that the snapshot has been created successfully") + Eventually(komega.Object(resource), "15s").Should( + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) snapshotResource := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, resource)) + Expect(resource).To(HaveField("Status.Resource.Name", Equal(testResource))) + Expect(resource).To(HaveField("Status.Resource.Type", Equal(resourceType))) + Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) + By("checking that the snapshot contains the correct content") - snapshotRepository := Must(registry.NewRepository(ctx, snapshotResource.Spec.Repository)) - snapshotResourceContentReader := Must(snapshotRepository.FetchSnapshot(ctx, snapshotResource.GetDigest())) - snapshotResourceContent := Must(io.ReadAll(snapshotResourceContentReader)) + snapshotRepository, err := registry.NewRepository(ctx, snapshotResource.Spec.Repository) + Expect(err).NotTo(HaveOccurred()) + snapshotResourceContent, err := snapshotRepository.FetchSnapshot(ctx, snapshotResource.GetDigest()) + Expect(err).NotTo(HaveOccurred()) Expect(string(snapshotResourceContent)).To(Equal(ResourceContent)) + + // Compare other fields + resourceAcc, err := cv.GetResource(v1.NewIdentity(testResource)) + Expect(err).NotTo(HaveOccurred()) + + Expect(snapshotResource.Name).To(Equal(fmt.Sprintf("resource-%s", testResource))) + Expect(snapshotResource.Spec.Blob.Digest).To(Equal(resourceAcc.Meta().Digest.Value)) + Expect(snapshotResource.Spec.Blob.Tag).To(Equal(ResourceVersion)) + Expect(snapshotResource.Spec.Blob.Size).To(Equal(int64(len([]byte(ResourceContent))))) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Expect(k8sClient.Delete(ctx, snapshotResource)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + Expect(k8sClient.Delete(ctx, snapshotComponent)).To(Succeed()) }) - }) -}) -// prepareComponent essentially mocks the behavior of the component reconciler to provider the necessary component and -// artifact for the resource controller. -func prepareComponent(ctx context.Context, env *Builder, ctfPath string) { - By("creating ocm repositories with a component and resource") - env.OCMCommonTransport(ctfPath, accessio.FormatDirectory, func() { - env.Component(Component, func() { - env.Version(ComponentVersion, func() { - env.Resource(ResourceObj, ResourceVersion, artifacttypes.PLAIN_TEXT, v1.LocalRelation, func() { - env.BlobData(mime.MIME_TEXT, []byte(ResourceContent)) + It("can reconcile a resource: OCIArtifact", func() { + testComponent := fmt.Sprintf("%s-%d", ComponentObj, testNumber) + testResource := fmt.Sprintf("%s-%d", ResourceObj, testNumber) + resourceType := artifacttypes.OCI_ARTIFACT + + By("creating an OCI artifact") + repository, err := registry.NewRepository(ctx, testResource) + Expect(err).NotTo(HaveOccurred()) + manifestDigest, err := repository.PushSnapshot(ctx, ResourceVersion, []byte(ResourceContent)) + Expect(err).ToNot(HaveOccurred()) + DeferCleanup(func(ctx SpecContext) { + Expect(repository.DeleteSnapshot(ctx, manifestDigest.String())).To(Succeed()) + }) + + By("creating an ocm resource from an OCI artifact") + env.OCMCommonTransport(resourceLocalPath, accessio.FormatDirectory, func() { + env.Component(Component, func() { + env.Version(ComponentVersion, func() { + env.Resource(testResource, ResourceVersion, resourceType, v1.LocalRelation, func() { + env.Access(ociartifact.New(fmt.Sprintf("http://%s/%s:%s", repository.GetHost(), repository.GetName(), ResourceVersion))) + }) + }) }) }) + + repo, err := ctf.Open(env, accessobj.ACC_WRITABLE, resourceLocalPath, vfs.FileMode(vfs.O_RDWR), env) + Expect(err).NotTo(HaveOccurred()) + cv, err := repo.LookupComponentVersion(Component, ComponentVersion) + Expect(err).NotTo(HaveOccurred()) + cd, err := ocmPkg.ListComponentDescriptors(ctx, cv, repo) + Expect(err).NotTo(HaveOccurred()) + dataCds, err := yaml.Marshal(cd) + Expect(err).NotTo(HaveOccurred()) + + spec, err := ctf.NewRepositorySpec(ctf.ACC_READONLY, resourceLocalPath) + specData, err := spec.MarshalJSON() + Expect(err).NotTo(HaveOccurred()) + + By("creating a mocked component") + component := test.SetupComponentWithDescriptorList(ctx, testComponent, Namespace, dataCds, &test.MockComponentOptions{ + BasePath: "", + Registry: registry, + Client: k8sClient, + Recorder: recorder, + Info: v1alpha1.ComponentInfo{ + Component: Component, + Version: ComponentVersion, + RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, + }, + Repository: RepositoryObj, + }) + + By("creating a resource object") + resource := &v1alpha1.Resource{ + ObjectMeta: k8smetav1.ObjectMeta{ + Namespace: Namespace, + Name: testResource, + }, + Spec: v1alpha1.ResourceSpec{ + ComponentRef: corev1.LocalObjectReference{ + Name: testComponent, + }, + Resource: v1alpha1.ResourceID{ + ByReference: v1alpha1.ResourceReference{ + Resource: v1.NewIdentity(testResource), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + By("checking that the resource has been reconciled successfully") + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + if err != nil { + return false + } + return conditions.IsReady(resource) + }, "15s").WithContext(ctx).Should(BeTrue()) + + By("checking that the snapshot has been created successfully") + Eventually(komega.Object(resource), "15s").Should( + HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + snapshotResource := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, resource)) + + Expect(resource).To(HaveField("Status.SnapshotRef.Name", Not(BeEmpty()))) + Expect(resource).To(HaveField("Status.Resource.Name", Equal(testResource))) + Expect(resource).To(HaveField("Status.Resource.Type", Equal(resourceType))) + Expect(resource).To(HaveField("Status.Resource.Version", Equal(ResourceVersion))) + + By("checking that the snapshot contains the correct content") + snapshotRepository, err := registry.NewRepository(ctx, snapshotResource.Spec.Repository) + Expect(err).NotTo(HaveOccurred()) + snapshotResourceContent, err := snapshotRepository.FetchSnapshot(ctx, snapshotResource.GetDigest()) + Expect(err).NotTo(HaveOccurred()) + Expect(string(snapshotResourceContent)).To(Equal(ResourceContent)) + + // Compare other fields + resourceAcc, err := cv.GetResource(v1.NewIdentity(testResource)) + Expect(err).NotTo(HaveOccurred()) + + Expect(snapshotResource.Name).To(Equal(fmt.Sprintf("resource-%s", testResource))) + Expect(snapshotResource.Spec.Blob.Digest).To(Equal(resourceAcc.Meta().Digest.Value)) + Expect(snapshotResource.Spec.Blob.Tag).To(Equal(ResourceVersion)) + Expect(snapshotResource.Spec.Blob.Size).To(Equal(int64(0))) + + By("delete resources manually") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Expect(k8sClient.Delete(ctx, snapshotResource)).To(Succeed()) + Eventually(func(ctx context.Context) bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(resource), resource) + return errors.IsNotFound(err) + }, "15s").WithContext(ctx).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, component)).To(Succeed()) + snapshotComponent := Must(snapshot.GetSnapshotForOwner(ctx, k8sClient, component)) + Expect(k8sClient.Delete(ctx, snapshotComponent)).To(Succeed()) }) - }) - By("creating a component descriptor") - tmpDirCd := Must(os.MkdirTemp("/tmp", "descriptors-")) - DeferCleanup(func() error { - return os.RemoveAll(tmpDirCd) + // TODO: Add more testcases }) - repo := Must(ctf.Open(env, accessobj.ACC_WRITABLE, ctfPath, vfs.FileMode(vfs.O_RDWR), env)) - cv := Must(repo.LookupComponentVersion(Component, ComponentVersion)) - cd := Must(ocm.ListComponentDescriptors(ctx, cv, repo)) - dataCds := Must(yaml.Marshal(cd)) - MustBeSuccessful(os.WriteFile(filepath.Join(tmpDirCd, v1alpha1.OCMComponentDescriptorList), dataCds, 0o655)) - - By("creating a component object") - component := &v1alpha1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: Namespace, - Name: ComponentObj, - }, - Spec: v1alpha1.ComponentSpec{ - RepositoryRef: v1alpha1.ObjectKey{ - Namespace: Namespace, - Name: RepositoryObj, - }, - Component: Component, - Semver: ComponentVersion, - Interval: metav1.Duration{Duration: time.Minute * 10}, - }, - } - Expect(k8sClient.Create(ctx, component)).To(Succeed()) - - By("creating an component snapshot") - repositoryName := Must(snapshot.CreateRepositoryName(component.Spec.RepositoryRef.Name, component.GetName())) - repository := Must(registry.NewRepository(ctx, repositoryName)) - - manifestDigest := Must(repository.PushSnapshot(ctx, ComponentVersion, dataCds)) - snapshotCR := snapshot.Create(component, repositoryName, manifestDigest.String(), ComponentVersion, digest.FromBytes(dataCds).String(), int64(len(dataCds))) - - _ = Must(controllerutil.CreateOrUpdate(ctx, k8sClient, &snapshotCR, func() error { - if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { - if err := controllerutil.SetControllerReference(component, &snapshotCR, k8sClient.Scheme()); err != nil { - return fmt.Errorf("failed to set controller reference: %w", err) - } - } - return nil - })) - - By("updating the component object with the respective status") - baseComponent := component.DeepCopy() - ready := *conditions.TrueCondition("Ready", "ready", "message") - ready.LastTransitionTime = metav1.Time{Time: time.Now()} - baseComponent.Status.Conditions = []metav1.Condition{ready} - baseComponent.Status.SnapshotRef = corev1.LocalObjectReference{Name: snapshotCR.GetName()} - spec := Must(ctf.NewRepositorySpec(ctf.ACC_READONLY, ctfPath)) - specData := Must(spec.MarshalJSON()) - baseComponent.Status.Component = v1alpha1.ComponentInfo{ - RepositorySpec: &apiextensionsv1.JSON{Raw: specData}, - Component: Component, - Version: ComponentVersion, - } - Expect(k8sClient.Status().Update(ctx, baseComponent)).To(Succeed()) -} +}) diff --git a/internal/controller/resource/suite_test.go b/internal/controller/resource/suite_test.go index 0381e611..310c4b57 100644 --- a/internal/controller/resource/suite_test.go +++ b/internal/controller/resource/suite_test.go @@ -16,37 +16,32 @@ package resource import ( "context" "fmt" - "io" - "net/http" "os" "os/exec" "path/filepath" "runtime" "testing" - "time" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/yaml" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" metricserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/test" ) // +kubebuilder:scaffold:imports @@ -58,9 +53,12 @@ var cfg *rest.Config var k8sClient client.Client var k8sManager ctrl.Manager var testEnv *envtest.Environment +var recorder record.EventRecorder var zotCmd *exec.Cmd var registry *snapshot.Registry var zotRootDir string +var ctx context.Context +var cancel context.CancelFunc func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -73,20 +71,6 @@ 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"), @@ -103,13 +87,12 @@ var _ = BeforeSuite(func() { } // cfg is defined in this file globally. - cfg, err = testEnv.Start() + cfg, err := testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) DeferCleanup(testEnv.Stop) Expect(v1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) - Expect(artifactv1.AddToScheme(scheme.Scheme)).Should(Succeed()) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme @@ -127,36 +110,21 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - // Create zot-registry config file + recorder = &record.FakeRecorder{ + Events: make(chan string, 32), + IncludeObject: true, + } + + // Setup zot registry and start it up zotRootDir = Must(os.MkdirTemp("", "")) - zotAddress := "0.0.0.0" - zotPort := "8081" - zotConfig := []byte(fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},"http":{"address":"%s","port": "%s"}}`, zotRootDir, zotAddress, zotPort)) - zotConfigFile := filepath.Join(zotRootDir, "config.json") - MustBeSuccessful(os.WriteFile(zotConfigFile, zotConfig, 0644)) - - // Start zot-registry - zotCmd = exec.Command(filepath.Join("..", "..", "..", "bin", "zot-registry"), "serve", zotConfigFile) - err = zotCmd.Start() - Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to start Zot")) - - // Wait for Zot to be ready - Eventually(func() error { - resp, err := http.Get(fmt.Sprintf("http://%s:%s/v2/", zotAddress, zotPort)) - if err != nil { - return fmt.Errorf("could not connect to Zot") - } - - defer resp.Body.Close() - if resp.StatusCode != 200 { - return fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - return nil - }, 30*time.Second, 1*time.Second).Should(Succeed(), "Zot registry did not start in time") - - registry, err = snapshot.NewRegistry(fmt.Sprintf("%s:%s", zotAddress, zotPort)) - registry.PlainHTTP = true + DeferCleanup(func() { + Expect(os.RemoveAll(zotRootDir)).To(Succeed()) + }) + + zotCmd, registry = test.SetupRegistry(filepath.Join("..", "..", "..", "bin", "zot-registry"), zotRootDir, "0.0.0.0", "8081") + + ctx, cancel = context.WithCancel(context.Background()) + DeferCleanup(cancel) Expect((&Reconciler{ BaseReconciler: &ocm.BaseReconciler{ @@ -170,9 +138,19 @@ var _ = BeforeSuite(func() { Registry: registry, }).SetupWithManager(k8sManager)).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel = context.WithCancel(context.Background()) DeferCleanup(cancel) + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: Namespace, + }, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, namespace, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed()) + }) + go func() { defer GinkgoRecover() Expect(k8sManager.Start(ctx)).To(Succeed()) @@ -180,11 +158,6 @@ var _ = BeforeSuite(func() { }) 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)) - } + err := zotCmd.Process.Kill() + Expect(err).NotTo(HaveOccurred(), "Failed to stop Zot registry") }) diff --git a/internal/controller/snapshot/controller.go b/internal/controller/snapshot/controller.go index e53947c1..4240f4e4 100644 --- a/internal/controller/snapshot/controller.go +++ b/internal/controller/snapshot/controller.go @@ -2,8 +2,11 @@ package snapshot import ( "context" + "errors" "fmt" + "time" + "github.com/fluxcd/pkg/runtime/patch" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -15,6 +18,12 @@ import ( deliveryv1alpha1 "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/ocm" snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" +) + +const ( + // TODO: Decide on requeue timer as this is arbitrary. + requeueTimer = 10 * time.Minute ) // Reconciler reconciles a Snapshot object. @@ -37,7 +46,8 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { // Reconcile add a finalizer on creation to the snapshot resource and handles the deletion of the snapshot by deleting // the manifest of the OCI artifact in the OCI registry (The OCI registry GC deletes the blobs if no manifest is // pointing to it). -func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +// TODO: Check status updates and error returns. +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, retErr error) { logger := log.FromContext(ctx) logger.Info("Reconciling Snapshot") @@ -50,6 +60,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, nil } + patchHelper := patch.NewSerialPatcher(snapshotResource, r.Client) + // Always attempt to patch the object and status after each reconciliation. + defer func() { + if err := status.UpdateStatus(ctx, patchHelper, snapshotResource, r.EventRecorder, requeueTimer, retErr); err != nil { + retErr = errors.Join(retErr, err) + } + }() + + //nolint:nestif // Only complex for the linter if !snapshotResource.GetDeletionTimestamp().IsZero() { logger.Info("Deleting snapshot") @@ -58,8 +77,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, fmt.Errorf("failed to create a repository: %w", err) } - if err := repository.DeleteSnapshot(ctx, snapshotResource.GetDigest()); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to delete snapshot: %w", err) + exists, err := repository.ExistsSnapshot(ctx, snapshotResource.GetDigest()) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to check if snapshot exists: %w", err) + } + + if exists { + if err := repository.DeleteSnapshot(ctx, snapshotResource.GetDigest()); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to delete snapshot: %w", err) + } } if removed := controllerutil.RemoveFinalizer(snapshotResource, deliveryv1alpha1.SnapshotFinalizer); removed { @@ -80,5 +106,28 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{Requeue: true}, nil } + // Verify that snapshot actually exists. + repository, err := r.Registry.NewRepository(ctx, snapshotResource.Spec.Repository) + if err != nil { + status.MarkNotReady(r.EventRecorder, snapshotResource, deliveryv1alpha1.CreateOCIRepositoryFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to create a repository: %w", err) + } + + exists, err := repository.ExistsSnapshot(ctx, snapshotResource.GetDigest()) + if err != nil { + status.MarkNotReady(r.EventRecorder, snapshotResource, deliveryv1alpha1.OCIRepositoryExistsFailedReason, err.Error()) + + return ctrl.Result{}, fmt.Errorf("failed to check existence of OCI repository: %w", err) + } + + if !exists { + status.MarkNotReady(r.EventRecorder, snapshotResource, deliveryv1alpha1.OCIRepositoryExistsFailedReason, "OCI repository does not exist") + + return ctrl.Result{Requeue: true}, nil + } + + status.MarkReady(r.EventRecorder, snapshotResource, "snapshot and OCI repository exist") + return ctrl.Result{}, nil } diff --git a/internal/controller/snapshot/controller_test.go b/internal/controller/snapshot/controller_test.go index 0d5f8c6f..bdd82221 100644 --- a/internal/controller/snapshot/controller_test.go +++ b/internal/controller/snapshot/controller_test.go @@ -40,7 +40,7 @@ var _ = Describe("Snapshot Controller", func() { Spec: deliveryv1alpha1.SnapshotSpec{ Repository: "test-repository", Digest: "sha256:test-digest", - Blob: deliveryv1alpha1.BlobInfo{ + Blob: &deliveryv1alpha1.BlobInfo{ Digest: "sha256:test-digest", Tag: "1.0.0", Size: 0, diff --git a/pkg/compression/util.go b/pkg/compression/util.go index 82cbef16..b3a49911 100644 --- a/pkg/compression/util.go +++ b/pkg/compression/util.go @@ -1,7 +1,9 @@ package compression import ( + "archive/tar" "bytes" + "compress/gzip" "context" "errors" "fmt" @@ -11,22 +13,22 @@ import ( "github.com/containers/image/v5/pkg/compression" "sigs.k8s.io/controller-runtime/pkg/log" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" ) -type WriterToStorageFromArtifact interface { - Copy(art *artifactv1.Artifact, reader io.Reader) error +type WriterToStorageFromSnapshot interface { + Copy(snapshot *v1alpha1.Snapshot, reader io.Reader) error } -// AutoCompressAsGzipAndArchiveFile compresses the file if it is not already compressed and archives it in the storage. +// AutoCompressAsGzip compresses the file if it is not already compressed and archives it in the storage. // If the file is already compressed as gzip, it will be archived as is. // If the file is not compressed or not in gzip format, it will be attempting to recompress to gzip and then archive. // This is because some source controllers such as kustomize expect this compression format in their artifacts. -func AutoCompressAsGzipAndArchiveFile(ctx context.Context, art *artifactv1.Artifact, storage WriterToStorageFromArtifact, path string) (retErr error) { - logger := log.FromContext(ctx).WithValues("artifact", art.Name, "path", path) +func AutoCompressAsGzip(ctx context.Context, path string) (_ io.Reader, retErr error) { + logger := log.FromContext(ctx) file, err := os.OpenFile(path, os.O_RDONLY, 0o400) if err != nil { - return fmt.Errorf("failed to open file: %w", err) + return nil, fmt.Errorf("failed to open file: %w", err) } defer func() { retErr = errors.Join(retErr, file.Close()) @@ -34,7 +36,7 @@ func AutoCompressAsGzipAndArchiveFile(ctx context.Context, art *artifactv1.Artif algo, decompressor, reader, err := compression.DetectCompressionFormat(file) if err != nil { - return fmt.Errorf("failed to detect compression format: %w", err) + return nil, fmt.Errorf("failed to detect compression format: %w", err) } // If the file is @@ -46,7 +48,7 @@ func AutoCompressAsGzipAndArchiveFile(ctx context.Context, art *artifactv1.Artif if decompressor != nil { decompressed, err := decompressor(reader) if err != nil { - return fmt.Errorf("failed to decompress: %w", err) + return nil, fmt.Errorf("failed to decompress: %w", err) } defer func() { retErr = errors.Join(retErr, decompressed.Close()) @@ -58,21 +60,19 @@ func AutoCompressAsGzipAndArchiveFile(ctx context.Context, art *artifactv1.Artif // TODO: this loads the single file into memory which can be expensive, but orchestrating an io.Pipe here is not trivial var buf bytes.Buffer if err := compressViaBuffer(&buf, reader); err != nil { - return err - } - if err := storage.Copy(art, &buf); err != nil { - return fmt.Errorf("failed to copy: %w", err) + return nil, err } - return nil + return &buf, nil } - logger.V(1).Info("archiving already compressed file from path") - if err := storage.Copy(art, reader); err != nil { - return fmt.Errorf("failed to copy file: %w", err) + // TODO: Clean up + file2, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file2: %w", err) } - return nil + return file2, nil } func compressViaBuffer(buf *bytes.Buffer, reader io.Reader) (retErr error) { @@ -83,9 +83,77 @@ func compressViaBuffer(buf *bytes.Buffer, reader io.Reader) (retErr error) { defer func() { retErr = errors.Join(retErr, compressToBuf.Close()) }() + if _, err := io.Copy(compressToBuf, reader); err != nil { return fmt.Errorf("failed to copy: %w", err) } return nil } + +func CreateTGZForData(data []byte) (_ []byte, retErr error) { + var buf bytes.Buffer + + // Create a gzip writer + gzipWriter := gzip.NewWriter(&buf) + defer func() { + retErr = gzipWriter.Close() + }() + + // Create a tar writer + tarWriter := tar.NewWriter(gzipWriter) + defer func() { + retErr = tarWriter.Close() + }() + + // Create tar header + header := &tar.Header{ + Size: int64(len(data)), + } + + if err := tarWriter.WriteHeader(header); err != nil { + return nil, fmt.Errorf("failed to write tar header: %w", err) + } + + if _, err := tarWriter.Write(data); err != nil { + return nil, fmt.Errorf("failed to write tar content: %w", err) + } + + // Close writers to flush data + if err := tarWriter.Close(); err != nil { + return nil, fmt.Errorf("failed to close tar writer: %w", err) + } + if err := gzipWriter.Close(); err != nil { + return nil, fmt.Errorf("failed to close gzip writer: %w", err) + } + + // Return the buffer contents as []byte + return buf.Bytes(), nil +} + +func ExtractDataFromTGZ(data []byte) (_ []byte, retErr error) { + buf := bytes.NewReader(data) + + gzipReader, err := gzip.NewReader(buf) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer func() { + retErr = gzipReader.Close() + }() + + tarReader := tar.NewReader(gzipReader) + + _, err = tarReader.Next() + if err != nil { + return nil, fmt.Errorf("failed to read tar header: %w", err) + } + + var extractedData bytes.Buffer + //nolint:gosec // TODO: Decision needed + if _, err := io.Copy(&extractedData, tarReader); err != nil { + return nil, fmt.Errorf("failed to extract file content: %w", err) + } + + return extractedData.Bytes(), nil +} diff --git a/pkg/compression/util_test.go b/pkg/compression/util_test.go index 384e09f8..c986dd1e 100644 --- a/pkg/compression/util_test.go +++ b/pkg/compression/util_test.go @@ -13,8 +13,8 @@ import ( "github.com/ulikunitz/xz" contcompression "github.com/containers/image/v5/pkg/compression" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" ) @@ -26,7 +26,7 @@ type MockStorage struct { data *bytes.Buffer } -func (m *MockStorage) Copy(_ *artifactv1.Artifact, reader io.Reader) error { +func (m *MockStorage) Copy(_ *v1alpha1.Snapshot, reader io.Reader) error { m.data = new(bytes.Buffer) _, err := io.Copy(m.data, reader) return err @@ -36,26 +36,26 @@ func (m *MockStorage) GetData() *bytes.Buffer { return m.data } -var _ compression.WriterToStorageFromArtifact = &MockStorage{} +var _ compression.WriterToStorageFromSnapshot = &MockStorage{} func TestAutoCompressAndArchiveFile(t *testing.T) { tests := []struct { name string - setup func(t *testing.T) (string, *MockStorage) - validate func(t *testing.T, storage *MockStorage) + setup func(t *testing.T) string + validate func(t *testing.T, reader io.Reader) }{ { name: "Uncompressed", - setup: func(t *testing.T) (string, *MockStorage) { + setup: func(t *testing.T) string { path := filepath.Join(t.TempDir(), testfile) assert.NoError(t, os.WriteFile(path, testData, 0o644)) - return path, &MockStorage{} + return path }, validate: validateAutoCompressedAsGzip, }, { name: "Precompressed_Gzip", - setup: func(t *testing.T) (string, *MockStorage) { + setup: func(t *testing.T) string { path := filepath.Join(t.TempDir(), testfile) var buf bytes.Buffer compress := gzip.NewWriter(&buf) @@ -63,13 +63,13 @@ func TestAutoCompressAndArchiveFile(t *testing.T) { assert.NoError(t, compress.Close()) assert.NoError(t, err) assert.NoError(t, os.WriteFile(path, buf.Bytes(), 0o644)) - return path, &MockStorage{} + return path }, validate: validateAutoCompressedAsGzip, }, { name: "Precompressed_Nongzip_Xz", - setup: func(t *testing.T) (string, *MockStorage) { + setup: func(t *testing.T) string { path := filepath.Join(t.TempDir(), testfile) var buf bytes.Buffer compress, err := xz.NewWriter(&buf) @@ -78,7 +78,7 @@ func TestAutoCompressAndArchiveFile(t *testing.T) { assert.NoError(t, compress.Close()) assert.NoError(t, err) assert.NoError(t, os.WriteFile(path, buf.Bytes(), 0o644)) - return path, &MockStorage{} + return path }, validate: validateAutoCompressedAsGzip, }, @@ -86,18 +86,17 @@ func TestAutoCompressAndArchiveFile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - path, storage := tt.setup(t) - art := &artifactv1.Artifact{} - err := compression.AutoCompressAsGzipAndArchiveFile(context.Background(), art, storage, path) + path := tt.setup(t) + reader, err := compression.AutoCompressAsGzip(context.Background(), path) assert.NoError(t, err) - tt.validate(t, storage) + tt.validate(t, reader) }) } } -func validateAutoCompressedAsGzip(t *testing.T, storage *MockStorage) { +func validateAutoCompressedAsGzip(t *testing.T, input io.Reader) { t.Helper() - algo, decompress, reader, err := contcompression.DetectCompressionFormat(storage.GetData()) + algo, decompress, reader, err := contcompression.DetectCompressionFormat(input) assert.NoError(t, err) assert.Equal(t, contcompression.Gzip.Name(), algo.Name()) decompressed, err := decompress(reader) 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 deleted file mode 100644 index 127584dd..00000000 --- a/pkg/ocm/artifact.go +++ /dev/null @@ -1,116 +0,0 @@ -package ocm - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/openfluxcd/controller-manager/storage" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/yaml" - "ocm.software/ocm/api/ocm/compdesc" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - ctrl "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" -) - -// 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) { - reader, err := repository.FetchSnapshot(ctx, snapshotResource.GetDigest()) - if err != nil { - return nil, err - } - - // Get component descriptor set - cds := &Descriptors{} - if err := yaml.NewYAMLToJSONDecoder(reader).Decode(cds); err != nil { - return nil, fmt.Errorf("failed to unmarshal component descriptors: %w", err) - } - - return compdesc.NewComponentVersionSet(cds.List...), nil -} - -// GetAndVerifyArtifactForCollectable gets the artifact for the given collectable and verifies it against the given strg. -// If the artifact is not found, an error is returned. -func GetAndVerifyArtifactForCollectable( - ctx context.Context, - reader ctrl.Reader, - strg *storage.Storage, - collectable storage.Collectable, -) (*artifactv1.Artifact, error) { - artifact := strg.NewArtifactFor(collectable.GetKind(), collectable.GetObjectMeta(), "", "") - if err := reader.Get(ctx, types.NamespacedName{Name: artifact.Name, Namespace: artifact.Namespace}, artifact); err != nil { - return nil, fmt.Errorf("failed to get artifact: %w", err) - } - - // Check the digest of the archive and compare it to the one in the artifact - if err := strg.VerifyArtifact(artifact); err != nil { - return nil, fmt.Errorf("failed to verify artifact: %w", err) - } - - return artifact, nil -} - -// ValidateArtifactForCollectable verifies if the artifact for the given collectable is valid. -// This means that the artifact must be present in the cluster the reader is connected to and -// the artifact must be present in the storage. -// Additionally, the digest of the artifact must be different from the file name of the artifact. -// -// This method can be used to determine if an artifact needs an update or not because an artifact that does not -// fulfill these conditions can be considered out of date (not in the cluster, not in the storage, or mismatching digest). -// -// Prerequisite for this method is that the artifact name is based on its original digest. -func ValidateArtifactForCollectable( - ctx context.Context, - reader ctrl.Reader, - strg *storage.Storage, - collectable storage.Collectable, - digest string, -) (bool, error) { - artifact, err := GetAndVerifyArtifactForCollectable(ctx, reader, strg, collectable) - if errors.Is(err, os.ErrNotExist) { - return false, nil - } - if ctrl.IgnoreNotFound(err) != nil { - return false, fmt.Errorf("failed to get artifact: %w", err) - } - if artifact == nil { - return false, nil - } - - existingFile := filepath.Base(strg.LocalPath(artifact)) - - return existingFile != digest, nil -} - -// RemoveArtifactForCollectable removes the artifact for the given collectable from the given storage. -func RemoveArtifactForCollectable( - ctx context.Context, - client ctrl.Client, - strg *storage.Storage, - collectable storage.Collectable, -) error { - artifact, err := GetAndVerifyArtifactForCollectable(ctx, client, strg, collectable) - if ctrl.IgnoreNotFound(err) != nil { - return fmt.Errorf("failed to get artifact: %w", err) - } - - if artifact != nil { - if err := strg.Remove(artifact); err != nil { - if !os.IsNotExist(err) { - return fmt.Errorf("failed to remove artifact: %w", err) - } - } - } - - return nil -} - -func GetComponentSetForArtifact(_ *storage.Storage, _ *artifactv1.Artifact) (*compdesc.ComponentVersionSet, error) { - return nil, nil -} diff --git a/pkg/ocm/ocm_test.go b/pkg/ocm/ocm_test.go index aba47149..6be98fce 100644 --- a/pkg/ocm/ocm_test.go +++ b/pkg/ocm/ocm_test.go @@ -26,7 +26,6 @@ import ( "ocm.software/ocm/api/utils/mime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -382,7 +381,6 @@ consumers: utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(v1alpha1.AddToScheme(scheme)) - utilruntime.Must(artifactv1.AddToScheme(scheme)) BeforeEach(func() { bldr = fake.NewClientBuilder() diff --git a/pkg/ocm/snapshot.go b/pkg/ocm/snapshot.go new file mode 100644 index 00000000..568aff9b --- /dev/null +++ b/pkg/ocm/snapshot.go @@ -0,0 +1,111 @@ +package ocm + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/yaml" + "ocm.software/ocm/api/ocm/compdesc" + + ctrl "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" +) + +// GetComponentSetForSnapshot returns the component descriptor set for the given snapshot. +func GetComponentSetForSnapshot(ctx context.Context, repository snapshot.RepositoryType, snapshotResource *v1alpha1.Snapshot) (*compdesc.ComponentVersionSet, error) { + data, err := repository.FetchSnapshot(ctx, snapshotResource.GetDigest()) + if err != nil { + return nil, err + } + + dataExtracted, err := compression.ExtractDataFromTGZ(data) + if err != nil { + return nil, err + } + + cds := &Descriptors{} + if err := yaml.NewYAMLToJSONDecoder(bytes.NewReader(dataExtracted)).Decode(cds); err != nil { + return nil, fmt.Errorf("failed to unmarshal component descriptors: %w", err) + } + + return compdesc.NewComponentVersionSet(cds.List...), nil +} + +// GetAndVerifySnapshotForOwner gets the snapshot for the given collectable and verifies it. +// If the snapshot is not found, an error is returned. +func GetAndVerifySnapshotForOwner( + ctx context.Context, + reader ctrl.Reader, + registry snapshot.RegistryType, + owner v1alpha1.SnapshotWriter, +) (*v1alpha1.Snapshot, error) { + snapshotRef := owner.GetSnapshotName() + if snapshotRef == "" { + return nil, os.ErrNotExist + } + + snapshotCR := &v1alpha1.Snapshot{} + if err := reader.Get(ctx, types.NamespacedName{Name: snapshotRef, Namespace: owner.GetNamespace()}, snapshotCR); err != nil { + return nil, fmt.Errorf("failed to get snapshot %s: %w", snapshotRef, err) + } + + repository, err := registry.NewRepository(ctx, snapshotCR.Spec.Repository) + if err != nil { + return nil, fmt.Errorf("failed to createry: %w", err) + } + + 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 +} + +// ValidateSnapshotForOwner verifies if the snapshot for the given collectable is valid. +// This means that the snapshot must be present in the cluster the reader is connected to and +// the snapshot must be present in the OCI registry. +// Additionally, the passed digest must be different from the blob digest stored in the snapshot. +// +// This method can be used to determine if an snapshot needs an update or not because an snapshot that does not +// fulfill these conditions can be considered out of date (not in the cluster, not in the OCI registry, or mismatching +// digest). +func ValidateSnapshotForOwner( + ctx context.Context, + reader ctrl.Reader, + registry snapshot.RegistryType, + owner v1alpha1.SnapshotWriter, + digest string, +) (bool, error) { + 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 snapshot: %w", err) + } + if err != nil { + return false, fmt.Errorf("failed to get and verify snapshot: %w", err) + } + + if snapshotCR == nil { + return false, nil + } + + return snapshotCR.Spec.Blob.Digest != digest, nil +} diff --git a/pkg/artifact/object_config.go b/pkg/snapshot/object_config.go similarity index 98% rename from pkg/artifact/object_config.go rename to pkg/snapshot/object_config.go index 8ad01388..97e93458 100644 --- a/pkg/artifact/object_config.go +++ b/pkg/snapshot/object_config.go @@ -1,4 +1,4 @@ -package artifact +package snapshot import ( "bytes" diff --git a/pkg/snapshot/repository.go b/pkg/snapshot/repository.go index e7f80ff9..5b320065 100644 --- a/pkg/snapshot/repository.go +++ b/pkg/snapshot/repository.go @@ -6,39 +6,72 @@ import ( "encoding/json" "fmt" "io" + "net/url" + "strings" + "github.com/google/go-containerregistry/pkg/name" "github.com/mitchellh/hashstructure/v2" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry/remote" "sigs.k8s.io/controller-runtime/pkg/log" ociV1 "github.com/opencontainers/image-spec/specs-go/v1" -) + ocmctx "ocm.software/ocm/api/ocm" -const OCISchemaVersion = 2 + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" +) // A RepositoryType is a type that can push and fetch blobs. type RepositoryType interface { - // PushSnapshot pushes the blob to its repository. It returns the manifest-digest to retrieve the blob. + // TODO: Desc PushSnapshot(ctx context.Context, reference string, blob []byte) (digest.Digest, error) - FetchSnapshot(ctx context.Context, reference string) (io.ReadCloser, error) + // TODO: Desc + FetchSnapshot(ctx context.Context, reference string) ([]byte, error) + // TODO: Desc DeleteSnapshot(ctx context.Context, digest string) error + + // TODO: Desc + ExistsSnapshot(ctx context.Context, manifestDigest string) (bool, error) + + // TODO: Desc + CopySnapshotForResourceAccess(ctx context.Context, access ocmctx.ResourceAccess) (digest.Digest, error) + + // TODO: Desc + FetchManifest(ctx context.Context, digest string) (*ociV1.Manifest, error) + + // TODO: Desc + GetHost() string + + // TODO: Desc + GetName() string } type Repository struct { *remote.Repository } +func (r *Repository) GetHost() string { + return r.Reference.Host() +} + +func (r *Repository) GetName() string { + return r.Reference.Repository +} + func (r *Repository) PushSnapshot(ctx context.Context, tag string, blob []byte) (digest.Digest, error) { logger := log.FromContext(ctx) + // TODO: Check if tarred and gziped? + // Prepare and upload blob blobDescriptor := ociV1.Descriptor{ - MediaType: ociV1.MediaTypeImageLayer, + MediaType: ociV1.MediaTypeImageLayerGzip, Digest: digest.FromBytes(blob), Size: int64(len(blob)), } @@ -70,7 +103,7 @@ func (r *Repository) PushSnapshot(ctx context.Context, tag string, blob []byte) // Prepare and upload manifest manifest := ociV1.Manifest{ - Versioned: specs.Versioned{SchemaVersion: OCISchemaVersion}, + Versioned: specs.Versioned{SchemaVersion: v1alpha1.OCISchemaVersion}, MediaType: ociV1.MediaTypeImageManifest, Config: imageConfigDescriptor, Layers: []ociV1.Descriptor{blobDescriptor}, @@ -107,7 +140,7 @@ func (r *Repository) PushSnapshot(ctx context.Context, tag string, blob []byte) return manifestDigest, nil } -func (r *Repository) FetchSnapshot(ctx context.Context, manifestDigest string) (io.ReadCloser, error) { +func (r *Repository) FetchSnapshot(ctx context.Context, manifestDigest string) ([]byte, error) { // Fetch manifest descriptor to get manifest. manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) if err != nil { @@ -130,7 +163,12 @@ func (r *Repository) FetchSnapshot(ctx context.Context, manifestDigest string) ( return nil, fmt.Errorf("oci: expected 1 layer, got %d", len(manifest.Layers)) } - return r.Fetch(ctx, manifest.Layers[0]) + reader, err := r.Fetch(ctx, manifest.Layers[0]) + if err != nil { + return nil, fmt.Errorf("oci: error fetching layer: %w", err) + } + + return io.ReadAll(reader) } func (r *Repository) DeleteSnapshot(ctx context.Context, manifestDigest string) error { @@ -142,6 +180,104 @@ func (r *Repository) DeleteSnapshot(ctx context.Context, manifestDigest string) return r.Delete(ctx, manifestDescriptor) } +func (r *Repository) ExistsSnapshot(ctx context.Context, manifestDigest string) (bool, error) { + manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) + if err != nil { + return false, fmt.Errorf("oci: error fetching manifest: %w", err) + } + + return r.Exists(ctx, manifestDescriptor) +} + +func (r *Repository) FetchManifest(ctx context.Context, manifestDigest string) (*ociV1.Manifest, error) { + manifestDescriptor, _, err := r.FetchReference(ctx, manifestDigest) + if err != nil { + return nil, fmt.Errorf("oci: error fetching manifest: %w", err) + } + + manifestReader, err := r.Fetch(ctx, manifestDescriptor) + if err != nil { + return nil, fmt.Errorf("oci: error fetching manifest: %w", err) + } + + var manifest ociV1.Manifest + if err := json.NewDecoder(manifestReader).Decode(&manifest); err != nil { + return nil, fmt.Errorf("oci: error parsing manifest: %w", err) + } + + return &manifest, err +} + +func (r *Repository) CopySnapshotForResourceAccess(ctx context.Context, access ocmctx.ResourceAccess) (digest.Digest, error) { + logger := log.FromContext(ctx) + + gloAccess := access.GlobalAccess() + accessSpec, ok := gloAccess.(*ociartifact.AccessSpec) + if !ok { + return "", fmt.Errorf("expected type ociartifact.AccessSpec, but got %T", gloAccess) + } + + var http bool + var refSanitized string + refURL, err := url.Parse(accessSpec.ImageReference) + if err != nil { + return "", fmt.Errorf("oci: error parsing image reference: %w", err) + } + + if refURL.Scheme != "" { + if refURL.Scheme == "http" { + http = true + } + refSanitized = strings.TrimPrefix(accessSpec.ImageReference, refURL.Scheme+"://") + } else { + refSanitized = accessSpec.ImageReference + } + + ref, err := name.ParseReference(refSanitized) + if err != nil { + return "", fmt.Errorf("oci: error parsing image reference: %w", err) + } + + sourceRegistry, err := remote.NewRegistry(ref.Context().RegistryStr()) + if err != nil { + return "", fmt.Errorf("oci: error creating source registry: %w", err) + } + + if http { + sourceRegistry.PlainHTTP = true + } + + sourceRepository, err := sourceRegistry.Repository(ctx, ref.Context().RepositoryStr()) + if err != nil { + return "", fmt.Errorf("oci: error creating source repository: %w", err) + } + + desc, err := oras.Copy(ctx, sourceRepository, ref.Identifier(), r.Repository, ref.Identifier(), oras.CopyOptions{ + CopyGraphOptions: oras.CopyGraphOptions{ + PreCopy: func(_ context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + + return nil + }, + PostCopy: func(_ context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + + return nil + }, + OnCopySkipped: func(_ context.Context, desc ociV1.Descriptor) error { + logger.Info("uploading", "digest", desc.Digest.String(), "mediaType", desc.MediaType) + + return nil + }, + }, + }) + if err != nil { + return "", fmt.Errorf("oci: error copying snapshot: %w", err) + } + + return desc.Digest, nil +} + // CreateRepositoryName creates a name for an OCI repository and returns a hashed string from the passed arguments. The // purpose of this function is to sanitize any passed string to an OCI repository compliant name. func CreateRepositoryName(args ...string) (string, error) { diff --git a/pkg/artifact/resource.go b/pkg/snapshot/resource.go similarity index 73% rename from pkg/artifact/resource.go rename to pkg/snapshot/resource.go index e4829a3a..602e29fa 100644 --- a/pkg/artifact/resource.go +++ b/pkg/snapshot/resource.go @@ -1,20 +1,18 @@ -package artifact +package snapshot import ( + "bytes" "context" "errors" "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/util" @@ -39,36 +37,37 @@ type Content interface { util.RevisionAndDigest } -func NewContentBackedByComponentResourceArtifact( - storage *storage.Storage, +func NewContentBackedByComponentResourceSnapshot( + registry 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 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,18 @@ 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) + data, err := repository.FetchSnapshot(ctx, r.Snapshot.GetDigest()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to fetch snapshot: %w", err) } - return &lockedReadCloser{ - ReadCloser: readCloser, - unlock: unlock, - }, nil + return io.NopCloser(bytes.NewReader(data)), nil } var _ io.ReadCloser = &lockedReadCloser{} @@ -114,6 +109,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 +118,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 +149,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 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, snapshotResource, err := GetComponentResourceSnapshotFromReference(ctx, clnt, registry, ref) if err != nil { return nil, err } - return NewContentBackedByComponentResourceArtifact(strg, component, resource, artifact), nil + return NewContentBackedByComponentResourceSnapshot(registry, component, resource, snapshotResource), nil } type ObjectWithTargetReference interface { GetTarget() *v1alpha1.ConfigurationReference } -func GetComponentResourceArtifactFromReference( +func GetComponentResourceSnapshotFromReference( ctx context.Context, clnt client.Reader, - strg *storage.Storage, + registry RegistryType, ref *v1alpha1.ConfigurationReference, -) (*v1alpha1.Component, *v1alpha1.Resource, *artifactv1.Artifact, error) { +) (*v1alpha1.Component, *v1alpha1.Resource, *v1alpha1.Snapshot, error) { var ( resource client.Object err error @@ -230,15 +216,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 +232,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/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index bbbeb8fa..f239a8c4 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -27,8 +27,8 @@ func generateName(obj v1alpha1.SnapshotWriter) string { return name } -func Create(owner v1alpha1.SnapshotWriter, ociRepository, manifestDigest, blobVersion, blobDigest string, blobSize int64) v1alpha1.Snapshot { - return v1alpha1.Snapshot{ +func Create(owner v1alpha1.SnapshotWriter, ociRepository, manifestDigest string, blob *v1alpha1.BlobInfo) *v1alpha1.Snapshot { + return &v1alpha1.Snapshot{ ObjectMeta: metav1.ObjectMeta{ Name: generateName(owner), Namespace: owner.GetNamespace(), @@ -36,11 +36,7 @@ func Create(owner v1alpha1.SnapshotWriter, ociRepository, manifestDigest, blobVe Spec: v1alpha1.SnapshotSpec{ Repository: ociRepository, Digest: manifestDigest, - Blob: v1alpha1.BlobInfo{ - Digest: blobDigest, - Tag: blobVersion, - Size: blobSize, - }, + Blob: blob, }, Status: v1alpha1.SnapshotStatus{}, } diff --git a/pkg/test/component.go b/pkg/test/component.go new file mode 100644 index 00000000..5b7318b8 --- /dev/null +++ b/pkg/test/component.go @@ -0,0 +1,107 @@ +package test + +import ( + "context" + "fmt" + "time" + + "github.com/fluxcd/pkg/runtime/conditions" + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/onsi/gomega" + + "github.com/fluxcd/pkg/runtime/patch" + "github.com/opencontainers/go-digest" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/compression" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" +) + +type MockComponentOptions struct { + BasePath string + Registry snapshotRegistry.RegistryType + Client client.Client + Recorder record.EventRecorder + Info v1alpha1.ComponentInfo + Repository string +} + +func SetupComponentWithDescriptorList( + ctx context.Context, + name, namespace string, + descriptorListData []byte, + options *MockComponentOptions, +) *v1alpha1.Component { + data, err := compression.CreateTGZForData(descriptorListData) + Expect(err).ToNot(HaveOccurred()) + + component := &v1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.ComponentSpec{ + RepositoryRef: v1alpha1.ObjectKey{Name: options.Repository, Namespace: namespace}, + Component: options.Info.Component, + }, + } + Expect(options.Client.Create(ctx, component)).To(Succeed()) + + patchHelper := patch.NewSerialPatcher(component, options.Client) + + repositoryName, err := snapshotRegistry.CreateRepositoryName(options.Repository, name) + Expect(err).ToNot(HaveOccurred()) + + repository, err := options.Registry.NewRepository(ctx, repositoryName) + Expect(err).ToNot(HaveOccurred()) + + manifestDigest, err := repository.PushSnapshot(ctx, options.Info.Version, data) + Expect(err).ToNot(HaveOccurred()) + + snapshotCR := snapshotRegistry.Create( + component, + repositoryName, + manifestDigest.String(), + &v1alpha1.BlobInfo{ + Digest: digest.FromBytes(data).String(), + Tag: options.Info.Version, + Size: int64(len(data)), + }, + ) + + _, err = controllerutil.CreateOrUpdate(ctx, options.Client, snapshotCR, func() error { + if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { + if err := controllerutil.SetControllerReference(component, snapshotCR, options.Client.Scheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + } + + component.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), + } + + component.Status.Component = options.Info + + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + // Marks snapshot as ready + conditions.MarkTrue(snapshotCR, "Ready", "ready", "message") + Expect(options.Client.Status().Update(ctx, snapshotCR)).To(Succeed()) + + Eventually(func(ctx context.Context) error { + status.MarkReady(options.Recorder, component, "applied mock component") + + return status.UpdateStatus(ctx, patchHelper, component, options.Recorder, time.Hour, nil) + }).WithContext(ctx).Should(Succeed()) + + return component +} diff --git a/pkg/test/helper.go b/pkg/test/helper.go new file mode 100644 index 00000000..866acd6d --- /dev/null +++ b/pkg/test/helper.go @@ -0,0 +1,110 @@ +package test + +import ( + "archive/tar" + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/onsi/ginkgo/v2" + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/onsi/gomega" + + "github.com/mandelsoft/vfs/pkg/memoryfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "ocm.software/ocm/api/utils/tarutils" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" +) + +func VerifySnapshot(ctx context.Context, registry snapshotRegistry.RegistryType, snapshotCR *v1alpha1.Snapshot, files map[string]func(data []byte)) { + GinkgoHelper() + + repository, err := registry.NewRepository(ctx, snapshotCR.Spec.Repository) + Expect(err).ToNot(HaveOccurred()) + + data, err := repository.FetchSnapshot(ctx, snapshotCR.GetDigest()) + Expect(err).ToNot(HaveOccurred()) + + memFs := vfs.New(memoryfs.New()) + Expect(tarutils.UnzipTarToFs(memFs, bytes.NewReader(data))).To(Succeed()) + + for fileName, assert := range files { + data, err := memFs.ReadFile(fileName) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("expected %s to be present and be readable", fileName)) + assert(data) + } +} + +func CreateTGZFromPath(srcDir, tarPath string) (err error) { + // Create the output tar file + tarFile, err := os.Create(tarPath) + if err != nil { + return fmt.Errorf("could not create file: %w", err) + } + defer func() { + err = tarFile.Close() + }() + + // Create a new tar writer + tarWriter := tar.NewWriter(tarFile) + defer func() { + err = tarWriter.Close() + }() + + // Walk through the source directory + return filepath.Walk(srcDir, func(file string, fileInfo os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("could not read file %s: %w", file, err) + } + + if !fileInfo.Mode().IsRegular() { + return nil + } + + if fileInfo.IsDir() { + return nil + } + + // Create tar header + header, err := tar.FileInfoHeader(fileInfo, fileInfo.Name()) + if err != nil { + return fmt.Errorf("could not create tar header: %w", err) + } + + // Use relative path for header.Name to preserve folder structure + relPath, err := filepath.Rel(srcDir, file) + if err != nil { + return fmt.Errorf("could not create relative path: %w", err) + } + header.Name = relPath + + // Write header + err = tarWriter.WriteHeader(header) + if err != nil { + return fmt.Errorf("could not write tar header: %w", err) + } + + // Open the file + f, err := os.Open(file) + if err != nil { + return fmt.Errorf("could not open file %s: %w", file, err) + } + defer func() { + err = f.Close() + }() + + // Copy file data into the tar archive + _, err = io.Copy(tarWriter, f) + if err != nil { + return fmt.Errorf("could not copy file %s: %w", file, err) + } + + return nil + }) +} diff --git a/pkg/test/resource.go b/pkg/test/resource.go new file mode 100644 index 00000000..86e2a57e --- /dev/null +++ b/pkg/test/resource.go @@ -0,0 +1,142 @@ +package test + +import ( + "context" + "fmt" + "io" + "os" + "time" + + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/onsi/gomega" + + "github.com/fluxcd/pkg/runtime/patch" + "github.com/opencontainers/go-digest" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + + "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" + snapshotRegistry "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" + "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" +) + +type MockResourceOptions struct { + // TODO: Check removal as this should be the basePath of the removed artifact + BasePath string + + // option one to create a resource: directly pass the Data + Data io.Reader + // option two to create a resource: pass the path to the Data + DataPath string + + ComponentRef v1alpha1.ObjectKey + + Registry snapshotRegistry.RegistryType + Clnt client.Client + Recorder record.EventRecorder +} + +func SetupMockResourceWithData( + ctx context.Context, + name, namespace string, + options *MockResourceOptions, +) *v1alpha1.Resource { + res := &v1alpha1.Resource{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: v1alpha1.ResourceSpec{ + Resource: v1alpha1.ResourceID{ + ByReference: v1alpha1.ResourceReference{ + Resource: v1.NewIdentity(name), + }, + }, + ComponentRef: corev1.LocalObjectReference{ + Name: options.ComponentRef.Name, + }, + }, + } + Expect(options.Clnt.Create(ctx, res)).To(Succeed()) + + patchHelper := patch.NewSerialPatcher(res, options.Clnt) + + var data []byte + var err error + + if options.Data != nil { + data, err = io.ReadAll(options.Data) + Expect(err).ToNot(HaveOccurred()) + } + + if options.DataPath != "" { + f, err := os.Stat(options.DataPath) + Expect(err).ToNot(HaveOccurred()) + + // If the file is a directory, it must be tarred + if f.IsDir() { + tmpFile, err := os.CreateTemp("", "") + defer func() { + Expect(tmpFile.Close()).To(Succeed()) + }() + Expect(err).ToNot(HaveOccurred()) + + err = CreateTGZFromPath(options.DataPath, tmpFile.Name()) + Expect(err).ToNot(HaveOccurred()) + + data, err = os.ReadFile(tmpFile.Name()) + Expect(err).ToNot(HaveOccurred()) + } else { + data, err = os.ReadFile(options.DataPath) + Expect(err).ToNot(HaveOccurred()) + } + } + + // TODO: Check what about version?! + version := "1.0.0" + repositoryName, err := snapshotRegistry.CreateRepositoryName(options.ComponentRef.Name, name) + Expect(err).ToNot(HaveOccurred()) + repository, err := options.Registry.NewRepository(ctx, repositoryName) + Expect(err).ToNot(HaveOccurred()) + + manifestDigest, err := repository.PushSnapshot(ctx, version, data) + Expect(err).ToNot(HaveOccurred()) + snapshotCR := snapshotRegistry.Create( + res, + repositoryName, + manifestDigest.String(), + &v1alpha1.BlobInfo{ + Digest: digest.FromBytes(data).String(), + Tag: version, + Size: int64(len(data)), + }, + ) + + _, err = controllerutil.CreateOrUpdate(ctx, options.Clnt, snapshotCR, func() error { + if snapshotCR.ObjectMeta.CreationTimestamp.IsZero() { + if err := controllerutil.SetControllerReference(res, snapshotCR, options.Clnt.Scheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + } + + res.Status.SnapshotRef = corev1.LocalObjectReference{ + Name: snapshotCR.GetName(), + } + + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func(ctx context.Context) error { + status.MarkReady(options.Recorder, res, "applied mock resource") + + return status.UpdateStatus(ctx, patchHelper, res, options.Recorder, time.Hour, nil) + }).WithContext(ctx).Should(Succeed()) + + return res +} diff --git a/pkg/test/util.go b/pkg/test/util.go deleted file mode 100644 index bb0296aa..00000000 --- a/pkg/test/util.go +++ /dev/null @@ -1,233 +0,0 @@ -package test - -import ( - "context" - "fmt" - "io" - "os" - "path/filepath" - "time" - - //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL - . "github.com/onsi/ginkgo/v2" - //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL - . "github.com/onsi/gomega" - //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL - . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" - - "github.com/fluxcd/pkg/runtime/patch" - "github.com/mandelsoft/vfs/pkg/memoryfs" - "github.com/mandelsoft/vfs/pkg/vfs" - "github.com/openfluxcd/controller-manager/storage" - "k8s.io/client-go/tools/record" - "ocm.software/ocm/api/utils/tarutils" - "sigs.k8s.io/controller-runtime/pkg/client" - - artifactv1 "github.com/openfluxcd/artifact/api/v1alpha1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" - - "github.com/open-component-model/ocm-k8s-toolkit/api/v1alpha1" - "github.com/open-component-model/ocm-k8s-toolkit/pkg/status" -) - -type MockResourceOptions struct { - BasePath string - - // option one to create a resource: directly pass the Data - Data io.Reader - // option two to create a resource: pass the path to the Data - DataPath string - - ComponentRef v1alpha1.ObjectKey - - Strg *storage.Storage - Clnt client.Client - Recorder record.EventRecorder -} - -func SetupMockResourceWithData( - ctx context.Context, - name, namespace string, - options *MockResourceOptions, -) *v1alpha1.Resource { - res := &v1alpha1.Resource{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - Spec: v1alpha1.ResourceSpec{ - Resource: v1alpha1.ResourceID{ - ByReference: v1alpha1.ResourceReference{ - Resource: v1.NewIdentity(name), - }, - }, - ComponentRef: corev1.LocalObjectReference{ - Name: options.ComponentRef.Name, - }, - }, - } - Expect(options.Clnt.Create(ctx, res)).To(Succeed()) - - patchHelper := patch.NewSerialPatcher(res, options.Clnt) - - path := options.BasePath - - err := options.Strg.ReconcileArtifact( - ctx, - res, - name, - path, - fmt.Sprintf("%s.tar.gz", name), - func(artifact *artifactv1.Artifact, _ string) error { - // Archive directory to storage - if options.Data != nil { - if err := options.Strg.Copy(artifact, options.Data); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } - } - if options.DataPath != "" { - abs, err := filepath.Abs(options.DataPath) - if err != nil { - return fmt.Errorf("unable to get absolute path: %w", err) - } - if err := options.Strg.Archive(artifact, abs, nil); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } - } - - res.Status.ArtifactRef = corev1.LocalObjectReference{ - Name: artifact.Name, - } - - return nil - }) - Expect(err).ToNot(HaveOccurred()) - - art := &artifactv1.Artifact{} - art.Name = res.Status.ArtifactRef.Name - art.Namespace = res.Namespace - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - - Eventually(func(ctx context.Context) error { - status.MarkReady(options.Recorder, res, "applied mock resource") - - return status.UpdateStatus(ctx, patchHelper, res, options.Recorder, time.Hour, nil) - }).WithContext(ctx).Should(Succeed()) - - return res -} - -type MockComponentOptions struct { - BasePath string - Strg *storage.Storage - Client client.Client - Recorder record.EventRecorder - Info v1alpha1.ComponentInfo - Repository string -} - -func SetupComponentWithDescriptorList( - ctx context.Context, - name, namespace string, - descriptorListData []byte, - options *MockComponentOptions, -) *v1alpha1.Component { - dir := filepath.Join(options.BasePath, "descriptor") - CreateTGZ(dir, map[string][]byte{ - v1alpha1.OCMComponentDescriptorList: descriptorListData, - }) - component := &v1alpha1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: v1alpha1.ComponentSpec{ - RepositoryRef: v1alpha1.ObjectKey{Name: options.Repository, Namespace: namespace}, - Component: options.Info.Component, - }, - Status: v1alpha1.ComponentStatus{ - ArtifactRef: corev1.LocalObjectReference{ - Name: name, - }, - Component: options.Info, - }, - } - Expect(options.Client.Create(ctx, component)).To(Succeed()) - - patchHelper := patch.NewSerialPatcher(component, options.Client) - - Expect(options.Strg.ReconcileArtifact( - ctx, - component, - name, - options.BasePath, - fmt.Sprintf("%s.tar.gz", name), - func(artifact *artifactv1.Artifact, _ string) error { - if err := options.Strg.Archive(artifact, dir, nil); err != nil { - return fmt.Errorf("unable to archive artifact to storage: %w", err) - } - - component.Status.ArtifactRef = corev1.LocalObjectReference{ - Name: artifact.Name, - } - component.Status.Component = options.Info - - return nil - }), - ).To(Succeed()) - - art := &artifactv1.Artifact{} - art.Name = component.Status.ArtifactRef.Name - art.Namespace = component.Namespace - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - - Eventually(func(ctx context.Context) error { - status.MarkReady(options.Recorder, component, "applied mock component") - - return status.UpdateStatus(ctx, patchHelper, component, options.Recorder, time.Hour, nil) - }).WithContext(ctx).Should(Succeed()) - - return component -} - -func VerifyArtifact(strg *storage.Storage, art *artifactv1.Artifact, files map[string]func(data []byte)) { - GinkgoHelper() - - art = art.DeepCopy() - - Eventually(Object(art), "5s").Should(HaveField("Spec.URL", Not(BeEmpty()))) - - localized := strg.LocalPath(art) - Expect(localized).To(BeAnExistingFile()) - - memFs := vfs.New(memoryfs.New()) - localizedArchiveData, err := os.OpenFile(localized, os.O_RDONLY, 0o600) - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(func() { - Expect(localizedArchiveData.Close()).To(Succeed()) - }) - Expect(tarutils.UnzipTarToFs(memFs, localizedArchiveData)).To(Succeed()) - - for fileName, assert := range files { - data, err := memFs.ReadFile(fileName) - Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("expected %s to be present and be readable", fileName)) - assert(data) - } -} - -func CreateTGZ(tgzPackageDir string, data map[string][]byte) { - GinkgoHelper() - Expect(os.Mkdir(tgzPackageDir, os.ModePerm|os.ModeDir)).To(Succeed()) - for path, data := range data { - path = filepath.Join(tgzPackageDir, path) - writer, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.ModePerm) - Expect(err).ToNot(HaveOccurred()) - defer func() { - Expect(writer.Close()).To(Succeed()) - }() - _, err = writer.Write(data) - Expect(err).ToNot(HaveOccurred()) - } -} diff --git a/pkg/test/zot-registry.go b/pkg/test/zot-registry.go new file mode 100644 index 00000000..471bcabf --- /dev/null +++ b/pkg/test/zot-registry.go @@ -0,0 +1,60 @@ +package test + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "time" + + //nolint:revive,stylecheck // dot import necessary for Ginkgo DSL + . "github.com/onsi/gomega" + + "github.com/open-component-model/ocm-k8s-toolkit/pkg/snapshot" +) + +const ( + timeout = 30 * time.Second +) + +func SetupRegistry(binPath, rootDir, address, port string) (*exec.Cmd, *snapshot.Registry) { + config := []byte(fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},"http":{"address":"%s","port": "%s"}}`, rootDir, address, port)) + configFile := filepath.Join(rootDir, "config.json") + err := os.WriteFile(configFile, config, 0644) + Expect(err).NotTo(HaveOccurred()) + + // Start zot-registry + zotCmd := exec.Command(binPath, "serve", configFile) + err = zotCmd.Start() + Expect(err).NotTo(HaveOccurred(), "Failed to start Zot") + + // Wait for Zot to be ready + Eventually(func() error { + url := fmt.Sprintf("http://%s/v2/", net.JoinHostPort(address, port)) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("could not connect to Zot") + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil + }, timeout).Should(Succeed(), "Zot registry did not start in time") + + registry, err := snapshot.NewRegistry(fmt.Sprintf("%s:%s", address, port)) + Expect(err).NotTo(HaveOccurred()) + registry.PlainHTTP = true + + return zotCmd, registry +} diff --git a/test/utils/utils.go b/test/utils/utils.go index 18462b2d..543ac9a8 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -29,6 +29,7 @@ import ( . "github.com/onsi/ginkgo/v2" //nolint:golint,revive,stylecheck // ginkgo... + // TODO: Replacement required! "github.com/openfluxcd/artifact/test/utils" )