From 11dc0a3bc7ec1b248810f87632c981b33552afcb Mon Sep 17 00:00:00 2001
From: Stefan Prodan
Date: Mon, 22 Aug 2022 15:51:42 +0300
Subject: [PATCH] Select layer by OCI media type Allow specifying the media
type of the layer which should be extracted from the OCI artifact.
Signed-off-by: Stefan Prodan
---
api/v1beta2/ocirepository_types.go | 22 +++++++
api/v1beta2/zz_generated.deepcopy.go | 20 ++++++
...rce.toolkit.fluxcd.io_ocirepositories.yaml | 10 +++
controllers/ocirepository_controller.go | 36 ++++++++++-
controllers/ocirepository_controller_test.go | 14 ++--
docs/api/source.md | 64 +++++++++++++++++++
6 files changed, 160 insertions(+), 6 deletions(-)
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
+
+
+OCILayerSelector
+
+
+ |
+
+(Optional)
+ LayerSelector specifies which layer should be extracted from the OCI artifact.
+When not specified, the first layer found in the artifact is selected.
+ |
+
+
+
provider
string
@@ -2529,6 +2544,40 @@ string
+
+
+(Appears on:
+OCIRepositorySpec)
+
+OCILayerSelector specifies which layer should be extracted from an OCI Artifact
+
@@ -2634,6 +2683,21 @@ defaults to the latest tag.
|
+layerSelector
+
+
+OCILayerSelector
+
+
+ |
+
+(Optional)
+ LayerSelector specifies which layer should be extracted from the OCI artifact.
+When not specified, the first layer found in the artifact is selected.
+ |
+
+
+
provider
string
|