Skip to content

Commit 2010eef

Browse files
authored
Merge pull request fluxcd#871 from fluxcd/oci-mediatype
[RFC-0003] Select layer by OCI media type
2 parents 02be5de + e5cb32b commit 2010eef

File tree

7 files changed

+290
-6
lines changed

7 files changed

+290
-6
lines changed

api/v1beta2/ocirepository_types.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ type OCIRepositorySpec struct {
6060
// +optional
6161
Reference *OCIRepositoryRef `json:"ref,omitempty"`
6262

63+
// LayerSelector specifies which layer should be extracted from the OCI artifact.
64+
// When not specified, the first layer found in the artifact is selected.
65+
// +optional
66+
LayerSelector *OCILayerSelector `json:"layerSelector,omitempty"`
67+
6368
// The provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'.
6469
// When not specified, defaults to 'generic'.
6570
// +kubebuilder:validation:Enum=generic;aws;azure;gcp
@@ -130,6 +135,15 @@ type OCIRepositoryRef struct {
130135
Tag string `json:"tag,omitempty"`
131136
}
132137

138+
// OCILayerSelector specifies which layer should be extracted from an OCI Artifact
139+
type OCILayerSelector struct {
140+
// MediaType specifies the OCI media type of the layer
141+
// which should be extracted from the OCI Artifact. The
142+
// first layer matching this type is selected.
143+
// +optional
144+
MediaType string `json:"mediaType,omitempty"`
145+
}
146+
133147
// OCIRepositoryVerification verifies the authenticity of an OCI Artifact
134148
type OCIRepositoryVerification struct {
135149
// Provider specifies the technology used to sign the OCI Artifact.
@@ -192,6 +206,15 @@ func (in *OCIRepository) GetArtifact() *Artifact {
192206
return in.Status.Artifact
193207
}
194208

209+
// GetLayerMediaType returns the media type layer selector if found in spec.
210+
func (in *OCIRepository) GetLayerMediaType() string {
211+
if in.Spec.LayerSelector == nil {
212+
return ""
213+
}
214+
215+
return in.Spec.LayerSelector.MediaType
216+
}
217+
195218
// +genclient
196219
// +genclient:Namespaced
197220
// +kubebuilder:storageversion

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ spec:
7575
interval:
7676
description: The interval at which to check for image updates.
7777
type: string
78+
layerSelector:
79+
description: LayerSelector specifies which layer should be extracted
80+
from the OCI artifact. When not specified, the first layer found
81+
in the artifact is selected.
82+
properties:
83+
mediaType:
84+
description: MediaType specifies the OCI media type of the layer
85+
which should be extracted from the OCI Artifact. The first layer
86+
matching this type is selected.
87+
type: string
88+
type: object
7889
provider:
7990
default: generic
8091
description: The provider used for authentication, can be 'aws', 'azure',

controllers/ocirepository_controller.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/google/go-containerregistry/pkg/authn/k8schain"
3434
"github.com/google/go-containerregistry/pkg/crane"
3535
"github.com/google/go-containerregistry/pkg/name"
36+
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
3637
"github.com/google/go-containerregistry/pkg/v1/remote"
3738
corev1 "k8s.io/api/core/v1"
3839
"k8s.io/apimachinery/pkg/runtime"
@@ -433,7 +434,40 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
433434
return sreconcile.ResultEmpty, e
434435
}
435436

436-
blob, err := layers[0].Compressed()
437+
var layer gcrv1.Layer
438+
439+
switch {
440+
case obj.GetLayerMediaType() != "":
441+
var found bool
442+
for i, l := range layers {
443+
md, err := l.MediaType()
444+
if err != nil {
445+
e := serror.NewGeneric(
446+
fmt.Errorf("failed to determine the media type of layer[%v] from artifact: %w", i, err),
447+
sourcev1.OCILayerOperationFailedReason,
448+
)
449+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
450+
return sreconcile.ResultEmpty, e
451+
}
452+
if string(md) == obj.GetLayerMediaType() {
453+
layer = layers[i]
454+
found = true
455+
break
456+
}
457+
}
458+
if !found {
459+
e := serror.NewGeneric(
460+
fmt.Errorf("failed to find layer with media type '%s' in artifact", obj.GetLayerMediaType()),
461+
sourcev1.OCILayerOperationFailedReason,
462+
)
463+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
464+
return sreconcile.ResultEmpty, e
465+
}
466+
default:
467+
layer = layers[0]
468+
}
469+
470+
blob, err := layer.Compressed()
437471
if err != nil {
438472
e := serror.NewGeneric(
439473
fmt.Errorf("failed to extract the first layer from artifact: %w", err),

controllers/ocirepository_controller_test.go

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,15 @@ func TestOCIRepository_Reconcile(t *testing.T) {
8080
tag string
8181
semver string
8282
digest string
83+
mediaType string
8384
assertArtifact []artifactFixture
8485
}{
8586
{
86-
name: "public tag",
87-
url: podinfoVersions["6.1.6"].url,
88-
tag: podinfoVersions["6.1.6"].tag,
89-
digest: podinfoVersions["6.1.6"].digest.Hex,
87+
name: "public tag",
88+
url: podinfoVersions["6.1.6"].url,
89+
tag: podinfoVersions["6.1.6"].tag,
90+
digest: podinfoVersions["6.1.6"].digest.Hex,
91+
mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
9092
assertArtifact: []artifactFixture{
9193
{
9294
expectedPath: "kustomize/deployment.yaml",
@@ -142,7 +144,9 @@ func TestOCIRepository_Reconcile(t *testing.T) {
142144
if tt.semver != "" {
143145
obj.Spec.Reference.SemVer = tt.semver
144146
}
145-
147+
if tt.mediaType != "" {
148+
obj.Spec.LayerSelector = &sourcev1.OCILayerSelector{MediaType: tt.mediaType}
149+
}
146150
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
147151

148152
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
@@ -244,6 +248,109 @@ func TestOCIRepository_Reconcile(t *testing.T) {
244248
}
245249
}
246250

251+
func TestOCIRepository_Reconcile_MediaType(t *testing.T) {
252+
g := NewWithT(t)
253+
254+
// Registry server with public images
255+
tmpDir := t.TempDir()
256+
regServer, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
257+
if err != nil {
258+
g.Expect(err).ToNot(HaveOccurred())
259+
}
260+
261+
podinfoVersions, err := pushMultiplePodinfoImages(regServer.registryHost, "6.1.4", "6.1.5", "6.1.6")
262+
263+
tests := []struct {
264+
name string
265+
url string
266+
tag string
267+
mediaType string
268+
wantErr bool
269+
}{
270+
{
271+
name: "Works with no media type",
272+
url: podinfoVersions["6.1.4"].url,
273+
tag: podinfoVersions["6.1.4"].tag,
274+
},
275+
{
276+
name: "Works with Flux CLI media type",
277+
url: podinfoVersions["6.1.5"].url,
278+
tag: podinfoVersions["6.1.5"].tag,
279+
mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
280+
},
281+
{
282+
name: "Fails with unknown media type",
283+
url: podinfoVersions["6.1.6"].url,
284+
tag: podinfoVersions["6.1.6"].tag,
285+
mediaType: "application/invalid.tar.gzip",
286+
wantErr: true,
287+
},
288+
}
289+
290+
for _, tt := range tests {
291+
t.Run(tt.name, func(t *testing.T) {
292+
293+
g := NewWithT(t)
294+
295+
ns, err := testEnv.CreateNamespace(ctx, "ocirepository-mediatype-test")
296+
g.Expect(err).ToNot(HaveOccurred())
297+
defer func() { g.Expect(testEnv.Delete(ctx, ns)).To(Succeed()) }()
298+
299+
obj := &sourcev1.OCIRepository{
300+
ObjectMeta: metav1.ObjectMeta{
301+
GenerateName: "ocirepository-reconcile",
302+
Namespace: ns.Name,
303+
},
304+
Spec: sourcev1.OCIRepositorySpec{
305+
URL: tt.url,
306+
Interval: metav1.Duration{Duration: 60 * time.Minute},
307+
Reference: &sourcev1.OCIRepositoryRef{
308+
Tag: tt.tag,
309+
},
310+
LayerSelector: &sourcev1.OCILayerSelector{
311+
MediaType: tt.mediaType,
312+
},
313+
},
314+
}
315+
316+
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
317+
318+
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
319+
320+
// Wait for the finalizer to be set
321+
g.Eventually(func() bool {
322+
if err := testEnv.Get(ctx, key, obj); err != nil {
323+
return false
324+
}
325+
return len(obj.Finalizers) > 0
326+
}, timeout).Should(BeTrue())
327+
328+
// Wait for the object to be reconciled
329+
g.Eventually(func() bool {
330+
if err := testEnv.Get(ctx, key, obj); err != nil {
331+
return false
332+
}
333+
readyCondition := conditions.Get(obj, meta.ReadyCondition)
334+
return readyCondition != nil
335+
}, timeout).Should(BeTrue())
336+
337+
g.Expect(conditions.IsReady(obj)).To(BeIdenticalTo(!tt.wantErr))
338+
if tt.wantErr {
339+
g.Expect(conditions.Get(obj, meta.ReadyCondition).Message).Should(ContainSubstring("failed to find layer with media type"))
340+
}
341+
342+
// Wait for the object to be deleted
343+
g.Expect(testEnv.Delete(ctx, obj)).To(Succeed())
344+
g.Eventually(func() bool {
345+
if err := testEnv.Get(ctx, key, obj); err != nil {
346+
return apierrors.IsNotFound(err)
347+
}
348+
return false
349+
}, timeout).Should(BeTrue())
350+
})
351+
}
352+
}
353+
247354
func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
248355
type secretOptions struct {
249356
username string

docs/api/source.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,21 @@ defaults to the latest tag.</p>
968968
</tr>
969969
<tr>
970970
<td>
971+
<code>layerSelector</code><br>
972+
<em>
973+
<a href="#source.toolkit.fluxcd.io/v1beta2.OCILayerSelector">
974+
OCILayerSelector
975+
</a>
976+
</em>
977+
</td>
978+
<td>
979+
<em>(Optional)</em>
980+
<p>LayerSelector specifies which layer should be extracted from the OCI artifact.
981+
When not specified, the first layer found in the artifact is selected.</p>
982+
</td>
983+
</tr>
984+
<tr>
985+
<td>
971986
<code>provider</code><br>
972987
<em>
973988
string
@@ -2529,6 +2544,41 @@ string
25292544
</table>
25302545
</div>
25312546
</div>
2547+
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCILayerSelector">OCILayerSelector
2548+
</h3>
2549+
<p>
2550+
(<em>Appears on:</em>
2551+
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec</a>)
2552+
</p>
2553+
<p>OCILayerSelector specifies which layer should be extracted from an OCI Artifact</p>
2554+
<div class="md-typeset__scrollwrap">
2555+
<div class="md-typeset__table">
2556+
<table>
2557+
<thead>
2558+
<tr>
2559+
<th>Field</th>
2560+
<th>Description</th>
2561+
</tr>
2562+
</thead>
2563+
<tbody>
2564+
<tr>
2565+
<td>
2566+
<code>mediaType</code><br>
2567+
<em>
2568+
string
2569+
</em>
2570+
</td>
2571+
<td>
2572+
<em>(Optional)</em>
2573+
<p>MediaType specifies the OCI media type of the layer
2574+
which should be extracted from the OCI Artifact. The
2575+
first layer matching this type is selected.</p>
2576+
</td>
2577+
</tr>
2578+
</tbody>
2579+
</table>
2580+
</div>
2581+
</div>
25322582
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCIRepositoryRef">OCIRepositoryRef
25332583
</h3>
25342584
<p>
@@ -2634,6 +2684,21 @@ defaults to the latest tag.</p>
26342684
</tr>
26352685
<tr>
26362686
<td>
2687+
<code>layerSelector</code><br>
2688+
<em>
2689+
<a href="#source.toolkit.fluxcd.io/v1beta2.OCILayerSelector">
2690+
OCILayerSelector
2691+
</a>
2692+
</em>
2693+
</td>
2694+
<td>
2695+
<em>(Optional)</em>
2696+
<p>LayerSelector specifies which layer should be extracted from the OCI artifact.
2697+
When not specified, the first layer found in the artifact is selected.</p>
2698+
</td>
2699+
</tr>
2700+
<tr>
2701+
<td>
26372702
<code>provider</code><br>
26382703
<em>
26392704
string

0 commit comments

Comments
 (0)