diff --git a/api/v1beta2/ocirepository_types.go b/api/v1beta2/ocirepository_types.go index 83ff7f3f..24ea674c 100644 --- a/api/v1beta2/ocirepository_types.go +++ b/api/v1beta2/ocirepository_types.go @@ -60,6 +60,11 @@ type OCIRepositorySpec struct { // +optional Reference *OCIRepositoryRef `json:"ref,omitempty"` + // LayerSelector specifies which layer should be extracted from the OCI artifact. + // When not specified, the first layer found in the artifact is selected. + // +optional + LayerSelector *OCILayerSelector `json:"layerSelector,omitempty"` + // The provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'. // When not specified, defaults to 'generic'. // +kubebuilder:validation:Enum=generic;aws;azure;gcp @@ -130,6 +135,14 @@ type OCIRepositoryRef struct { Tag string `json:"tag,omitempty"` } +// OCILayerSelector specifies which layer should be extracted from an OCI Artifact +type OCILayerSelector struct { + // MediaType specifies the OCI media type of the layer + // which should be extracted from the OCI Artifact. + // +optional + MediaType string `json:"mediaType,omitempty"` +} + // OCIRepositoryVerification verifies the authenticity of an OCI Artifact type OCIRepositoryVerification struct { // Provider specifies the technology used to sign the OCI Artifact. @@ -192,6 +205,15 @@ func (in *OCIRepository) GetArtifact() *Artifact { return in.Status.Artifact } +// GetLayerMediaType returns the media type layer selector if found in spec. +func (in *OCIRepository) GetLayerMediaType() string { + if in.Spec.LayerSelector == nil { + return "" + } + + return in.Spec.LayerSelector.MediaType +} + // +genclient // +genclient:Namespaced // +kubebuilder:storageversion diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index fc186d4d..25652de7 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -622,6 +622,21 @@ func (in *LocalHelmChartSourceReference) DeepCopy() *LocalHelmChartSourceReferen return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OCILayerSelector) DeepCopyInto(out *OCILayerSelector) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCILayerSelector. +func (in *OCILayerSelector) DeepCopy() *OCILayerSelector { + if in == nil { + return nil + } + out := new(OCILayerSelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OCIRepository) DeepCopyInto(out *OCIRepository) { *out = *in @@ -704,6 +719,11 @@ func (in *OCIRepositorySpec) DeepCopyInto(out *OCIRepositorySpec) { *out = new(OCIRepositoryRef) **out = **in } + if in.LayerSelector != nil { + in, out := &in.LayerSelector, &out.LayerSelector + *out = new(OCILayerSelector) + **out = **in + } if in.SecretRef != nil { in, out := &in.SecretRef, &out.SecretRef *out = new(meta.LocalObjectReference) diff --git a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml index 5e214ccd..39c7fbd2 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml @@ -75,6 +75,16 @@ spec: interval: description: The interval at which to check for image updates. type: string + layerSelector: + description: LayerSelector specifies which layer should be extracted + from the OCI artifact. When not specified, the first layer found + in the artifact is selected. + properties: + mediaType: + description: MediaType specifies the OCI media type of the layer + which should be extracted from the OCI Artifact. + type: string + type: object provider: default: generic description: The provider used for authentication, can be 'aws', 'azure', diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index 2a4993bb..f9965842 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -33,6 +33,7 @@ import ( "github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" + gcrv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -433,7 +434,40 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour return sreconcile.ResultEmpty, e } - blob, err := layers[0].Compressed() + var layer gcrv1.Layer + + switch { + case obj.GetLayerMediaType() != "": + var found bool + for i, l := range layers { + md, err := l.MediaType() + if err != nil { + e := serror.NewGeneric( + fmt.Errorf("failed to determine the media type of layer[%v] from artifact: %w", i, err), + sourcev1.OCILayerOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + if string(md) == obj.GetLayerMediaType() { + layer = layers[i] + found = true + break + } + } + if !found { + e := serror.NewGeneric( + fmt.Errorf("failed to find layer with media type '%s' in artifact: %w", obj.GetLayerMediaType(), err), + sourcev1.OCILayerOperationFailedReason, + ) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) + return sreconcile.ResultEmpty, e + } + default: + layer = layers[0] + } + + blob, err := layer.Compressed() if err != nil { e := serror.NewGeneric( fmt.Errorf("failed to extract the first layer from artifact: %w", err), diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go index b72413b1..b138224d 100644 --- a/controllers/ocirepository_controller_test.go +++ b/controllers/ocirepository_controller_test.go @@ -80,13 +80,15 @@ func TestOCIRepository_Reconcile(t *testing.T) { tag string semver string digest string + mediaType string assertArtifact []artifactFixture }{ { - name: "public tag", - url: podinfoVersions["6.1.6"].url, - tag: podinfoVersions["6.1.6"].tag, - digest: podinfoVersions["6.1.6"].digest.Hex, + name: "public tag", + url: podinfoVersions["6.1.6"].url, + tag: podinfoVersions["6.1.6"].tag, + digest: podinfoVersions["6.1.6"].digest.Hex, + mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", assertArtifact: []artifactFixture{ { expectedPath: "kustomize/deployment.yaml", @@ -142,7 +144,9 @@ func TestOCIRepository_Reconcile(t *testing.T) { if tt.semver != "" { obj.Spec.Reference.SemVer = tt.semver } - + if tt.mediaType != "" { + obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{MediaType: tt.mediaType} + } g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace} diff --git a/docs/api/source.md b/docs/api/source.md index 09f07274..b497c268 100644 --- a/docs/api/source.md +++ b/docs/api/source.md @@ -968,6 +968,21 @@ defaults to the latest tag.
layerSelector
LayerSelector specifies which layer should be extracted from the OCI artifact. +When not specified, the first layer found in the artifact is selected.
+provider
+(Appears on: +OCIRepositorySpec) +
+OCILayerSelector specifies which layer should be extracted from an OCI Artifact
+Field | +Description | +
---|---|
+mediaType + +string + + |
+
+(Optional)
+ MediaType specifies the OCI media type of the layer +which should be extracted from the OCI Artifact. + |
+
@@ -2634,6 +2683,21 @@ defaults to the latest tag.
layerSelector
LayerSelector specifies which layer should be extracted from the OCI artifact. +When not specified, the first layer found in the artifact is selected.
+provider