From 11dc0a3bc7ec1b248810f87632c981b33552afcb Mon Sep 17 00:00:00 2001
From: Stefan Prodan
Date: Mon, 22 Aug 2022 15:51:42 +0300
Subject: [PATCH 1/3] 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 83ff7f3ff..24ea674c4 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 fc186d4df..25652de71 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 5e214ccd8..39c7fbd2e 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 2a4993bbb..f9965842d 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 b72413b1f..b138224df 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 09f072743..b497c2688 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
From 49dc30922dd6356ba454583033a6a5b176eed799 Mon Sep 17 00:00:00 2001
From: Stefan Prodan
Date: Wed, 24 Aug 2022 12:27:30 +0300
Subject: [PATCH 2/3] Add tests for OCI layer selector
Signed-off-by: Stefan Prodan
---
controllers/ocirepository_controller.go | 2 +-
controllers/ocirepository_controller_test.go | 103 +++++++++++++++++++
2 files changed, 104 insertions(+), 1 deletion(-)
diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go
index f9965842d..58646313f 100644
--- a/controllers/ocirepository_controller.go
+++ b/controllers/ocirepository_controller.go
@@ -457,7 +457,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
}
if !found {
e := serror.NewGeneric(
- fmt.Errorf("failed to find layer with media type '%s' in artifact: %w", obj.GetLayerMediaType(), err),
+ fmt.Errorf("failed to find layer with media type '%s' in artifact", obj.GetLayerMediaType()),
sourcev1.OCILayerOperationFailedReason,
)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go
index b138224df..a0835100f 100644
--- a/controllers/ocirepository_controller_test.go
+++ b/controllers/ocirepository_controller_test.go
@@ -248,6 +248,109 @@ func TestOCIRepository_Reconcile(t *testing.T) {
}
}
+func TestOCIRepository_Reconcile_MediaType(t *testing.T) {
+ g := NewWithT(t)
+
+ // Registry server with public images
+ tmpDir := t.TempDir()
+ regServer, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
+ if err != nil {
+ g.Expect(err).ToNot(HaveOccurred())
+ }
+
+ podinfoVersions, err := pushMultiplePodinfoImages(regServer.registryHost, "6.1.4", "6.1.5", "6.1.6")
+
+ tests := []struct {
+ name string
+ url string
+ tag string
+ mediaType string
+ wantErr bool
+ }{
+ {
+ name: "Works with no media type",
+ url: podinfoVersions["6.1.4"].url,
+ tag: podinfoVersions["6.1.4"].tag,
+ },
+ {
+ name: "Works with Flux CLI media type",
+ url: podinfoVersions["6.1.5"].url,
+ tag: podinfoVersions["6.1.5"].tag,
+ mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ },
+ {
+ name: "Fails with unknown media type",
+ url: podinfoVersions["6.1.6"].url,
+ tag: podinfoVersions["6.1.6"].tag,
+ mediaType: "application/invalid.tar.gzip",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ g := NewWithT(t)
+
+ ns, err := testEnv.CreateNamespace(ctx, "ocirepository-mediatype-test")
+ g.Expect(err).ToNot(HaveOccurred())
+ defer func() { g.Expect(testEnv.Delete(ctx, ns)).To(Succeed()) }()
+
+ obj := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ GenerateName: "ocirepository-reconcile",
+ Namespace: ns.Name,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ URL: tt.url,
+ Interval: metav1.Duration{Duration: 60 * time.Minute},
+ Reference: &sourcev1.OCIRepositoryRef{
+ Tag: tt.tag,
+ },
+ LayerSelector: &sourcev1.OCILayerSelector{
+ MediaType: tt.mediaType,
+ },
+ },
+ }
+
+ g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
+
+ key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
+
+ // Wait for the finalizer to be set
+ g.Eventually(func() bool {
+ if err := testEnv.Get(ctx, key, obj); err != nil {
+ return false
+ }
+ return len(obj.Finalizers) > 0
+ }, timeout).Should(BeTrue())
+
+ // Wait for the object to be reconciled
+ g.Eventually(func() bool {
+ if err := testEnv.Get(ctx, key, obj); err != nil {
+ return false
+ }
+ readyCondition := conditions.Get(obj, meta.ReadyCondition)
+ return readyCondition != nil
+ }, timeout).Should(BeTrue())
+
+ g.Expect(conditions.IsReady(obj)).To(BeIdenticalTo(!tt.wantErr))
+ if tt.wantErr {
+ g.Expect(conditions.Get(obj, meta.ReadyCondition).Message).Should(ContainSubstring("failed to find layer with media type"))
+ }
+
+ // Wait for the object to be deleted
+ g.Expect(testEnv.Delete(ctx, obj)).To(Succeed())
+ g.Eventually(func() bool {
+ if err := testEnv.Get(ctx, key, obj); err != nil {
+ return apierrors.IsNotFound(err)
+ }
+ return false
+ }, timeout).Should(BeTrue())
+ })
+ }
+}
+
func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
type secretOptions struct {
username string
From e5cb32b0f248014f5512879dd93851a101c72e61 Mon Sep 17 00:00:00 2001
From: Stefan Prodan
Date: Wed, 24 Aug 2022 12:46:04 +0300
Subject: [PATCH 3/3] Add OCI layer selector to API docs
Signed-off-by: Stefan Prodan
---
api/v1beta2/ocirepository_types.go | 3 ++-
...rce.toolkit.fluxcd.io_ocirepositories.yaml | 3 ++-
docs/api/source.md | 3 ++-
docs/spec/v1beta2/ocirepositories.md | 24 +++++++++++++++++++
4 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/api/v1beta2/ocirepository_types.go b/api/v1beta2/ocirepository_types.go
index 24ea674c4..5c89a4ac0 100644
--- a/api/v1beta2/ocirepository_types.go
+++ b/api/v1beta2/ocirepository_types.go
@@ -138,7 +138,8 @@ type OCIRepositoryRef struct {
// 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.
+ // which should be extracted from the OCI Artifact. The
+ // first layer matching this type is selected.
// +optional
MediaType string `json:"mediaType,omitempty"`
}
diff --git a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
index 39c7fbd2e..d5308a130 100644
--- a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
+++ b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
@@ -82,7 +82,8 @@ spec:
properties:
mediaType:
description: MediaType specifies the OCI media type of the layer
- which should be extracted from the OCI Artifact.
+ which should be extracted from the OCI Artifact. The first layer
+ matching this type is selected.
type: string
type: object
provider:
diff --git a/docs/api/source.md b/docs/api/source.md
index b497c2688..ec0b1daf7 100644
--- a/docs/api/source.md
+++ b/docs/api/source.md
@@ -2571,7 +2571,8 @@ string
(Optional)
MediaType specifies the OCI media type of the layer
-which should be extracted from the OCI Artifact.
+which should be extracted from the OCI Artifact. The
+first layer matching this type is selected.
|
|
diff --git a/docs/spec/v1beta2/ocirepositories.md b/docs/spec/v1beta2/ocirepositories.md
index d540d8131..6bb67650b 100644
--- a/docs/spec/v1beta2/ocirepositories.md
+++ b/docs/spec/v1beta2/ocirepositories.md
@@ -368,6 +368,30 @@ spec:
This field takes precedence over all other fields.
+### Layer selector
+
+`spec.layerSelector` is an optional field to specify which layer should be extracted from the OCI Artifact.
+If not specified, the controller will extract the first layer found in the artifact.
+
+To extract a layer matching a specific
+[OCI media type](https://github.com/opencontainers/image-spec/blob/v1.0.2/media-types.md):
+
+```yaml
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+ name:
+spec:
+ layerSelector:
+ mediaType: "application/deployment.content.v1.tar+gzip"
+```
+
+If the layer selector matches more than one layer, the first layer matching the specified media type will be used.
+Note that the selected OCI layer must be
+[compressed](https://github.com/opencontainers/image-spec/blob/v1.0.2/layer.md#gzip-media-types)
+in the `tar+gzip` format.
+
### Ignore
`.spec.ignore` is an optional field to specify rules in [the `.gitignore`