Skip to content

Commit 759b19f

Browse files
authored
Support artifactType, for images whose config.mediaType is not a config (#1541)
* Support artifactType, for images whose config.mediaType is not a config * lint feedback * remove empty branches * avoid nil panics * unit test * doc comment
1 parent 11843ba commit 759b19f

8 files changed

Lines changed: 139 additions & 10 deletions

File tree

pkg/v1/manifest.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ type IndexManifest struct {
4040

4141
// Descriptor holds a reference from the manifest to one of its constituent elements.
4242
type Descriptor struct {
43-
MediaType types.MediaType `json:"mediaType"`
44-
Size int64 `json:"size"`
45-
Digest Hash `json:"digest"`
46-
Data []byte `json:"data,omitempty"`
47-
URLs []string `json:"urls,omitempty"`
48-
Annotations map[string]string `json:"annotations,omitempty"`
49-
Platform *Platform `json:"platform,omitempty"`
43+
MediaType types.MediaType `json:"mediaType"`
44+
Size int64 `json:"size"`
45+
Digest Hash `json:"digest"`
46+
Data []byte `json:"data,omitempty"`
47+
URLs []string `json:"urls,omitempty"`
48+
Annotations map[string]string `json:"annotations,omitempty"`
49+
Platform *Platform `json:"platform,omitempty"`
50+
ArtifactType string `json:"artifactType,omitempty"`
5051
}
5152

5253
// ParseManifest parses the io.Reader's contents into a Manifest.

pkg/v1/mutate/index_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
v1 "github.com/google/go-containerregistry/pkg/v1"
2424
"github.com/google/go-containerregistry/pkg/v1/empty"
2525
"github.com/google/go-containerregistry/pkg/v1/mutate"
26+
"github.com/google/go-containerregistry/pkg/v1/partial"
2627
"github.com/google/go-containerregistry/pkg/v1/random"
2728
"github.com/google/go-containerregistry/pkg/v1/types"
2829
"github.com/google/go-containerregistry/pkg/v1/validate"
@@ -180,3 +181,55 @@ func TestIndexImmutability(t *testing.T) {
180181
}
181182
})
182183
}
184+
185+
// TestAppend_ArtifactType tests that appending an image manifest that has a
186+
// non-standard config.mediaType to an index, results in the image's
187+
// config.mediaType being hoisted into the descriptor inside the index,
188+
// as artifactType.
189+
func TestAppend_ArtifactType(t *testing.T) {
190+
for _, c := range []struct {
191+
desc, configMediaType, wantArtifactType string
192+
}{{
193+
desc: "standard config.mediaType, no artifactType",
194+
configMediaType: string(types.DockerConfigJSON),
195+
wantArtifactType: "",
196+
}, {
197+
desc: "non-standard config.mediaType, want artifactType",
198+
configMediaType: "application/vnd.custom.something",
199+
wantArtifactType: "application/vnd.custom.something",
200+
}} {
201+
t.Run(c.desc, func(t *testing.T) {
202+
img, err := random.Image(1, 1)
203+
if err != nil {
204+
t.Fatalf("random.Image: %v", err)
205+
}
206+
img = mutate.ConfigMediaType(img, types.MediaType(c.configMediaType))
207+
idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{
208+
Add: img,
209+
})
210+
mf, err := idx.IndexManifest()
211+
if err != nil {
212+
t.Fatalf("IndexManifest: %v", err)
213+
}
214+
if got := mf.Manifests[0].ArtifactType; got != c.wantArtifactType {
215+
t.Errorf("manifest artifactType: got %q, want %q", got, c.wantArtifactType)
216+
}
217+
218+
desc, err := partial.Descriptor(img)
219+
if err != nil {
220+
t.Fatalf("partial.Descriptor: %v", err)
221+
}
222+
if got := desc.ArtifactType; got != c.wantArtifactType {
223+
t.Errorf("descriptor artifactType: got %q, want %q", got, c.wantArtifactType)
224+
}
225+
226+
gotAT, err := partial.ArtifactType(img)
227+
if err != nil {
228+
t.Fatalf("partial.ArtifactType: %v", err)
229+
}
230+
if gotAT != c.wantArtifactType {
231+
t.Errorf("partial.ArtifactType: got %q, want %q", gotAT, c.wantArtifactType)
232+
}
233+
})
234+
}
235+
}

pkg/v1/mutate/mutate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,8 @@ func MediaType(img v1.Image, mt types.MediaType) v1.Image {
500500
}
501501

502502
// ConfigMediaType modifies the MediaType() of the given image's Config.
503+
//
504+
// If !mt.IsConfig(), this will be the image's artifactType in any indexes it's a part of.
503505
func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image {
504506
return &image{
505507
base: img,

pkg/v1/partial/with.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,10 +328,28 @@ func Descriptor(d Describable) (*v1.Descriptor, error) {
328328
if desc.MediaType, err = d.MediaType(); err != nil {
329329
return nil, err
330330
}
331+
if wat, ok := d.(withArtifactType); ok {
332+
if desc.ArtifactType, err = wat.ArtifactType(); err != nil {
333+
return nil, err
334+
}
335+
} else {
336+
if wrm, ok := d.(WithRawManifest); ok && desc.MediaType.IsImage() {
337+
mf, _ := Manifest(wrm)
338+
// Failing to parse as a manifest should just be ignored.
339+
// The manifest might not be valid, and that's okay.
340+
if mf != nil && !mf.Config.MediaType.IsConfig() {
341+
desc.ArtifactType = string(mf.Config.MediaType)
342+
}
343+
}
344+
}
331345

332346
return &desc, nil
333347
}
334348

349+
type withArtifactType interface {
350+
ArtifactType() (string, error)
351+
}
352+
335353
type withUncompressedSize interface {
336354
UncompressedSize() (int64, error)
337355
}
@@ -399,3 +417,20 @@ func unwrap(i any) any {
399417
}
400418
return i
401419
}
420+
421+
// ArtifactType returns the artifact type for the given manifest.
422+
//
423+
// If the manifest reports its own artifact type, that's returned, otherwise
424+
// the manifest is parsed and, if successful, its config.mediaType is returned.
425+
func ArtifactType(w WithManifest) (string, error) {
426+
if wat, ok := w.(withArtifactType); ok {
427+
return wat.ArtifactType()
428+
}
429+
mf, _ := w.Manifest()
430+
// Failing to parse as a manifest should just be ignored.
431+
// The manifest might not be valid, and that's okay.
432+
if mf != nil && !mf.Config.MediaType.IsConfig() {
433+
return string(mf.Config.MediaType), nil
434+
}
435+
return "", nil
436+
}

pkg/v1/remote/descriptor.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,15 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
283283
return nil, nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.Ref)
284284
}
285285
}
286+
287+
var artifactType string
288+
mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
289+
// Failing to parse as a manifest should just be ignored.
290+
// The manifest might not be valid, and that's okay.
291+
if mf != nil && !mf.Config.MediaType.IsConfig() {
292+
artifactType = string(mf.Config.MediaType)
293+
}
294+
286295
// Do nothing for tags; I give up.
287296
//
288297
// We'd like to validate that the "Docker-Content-Digest" header matches what is returned by the registry,
@@ -293,9 +302,10 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
293302

294303
// Return all this info since we have to calculate it anyway.
295304
desc := v1.Descriptor{
296-
Digest: digest,
297-
Size: size,
298-
MediaType: mediaType,
305+
Digest: digest,
306+
Size: size,
307+
MediaType: mediaType,
308+
ArtifactType: artifactType,
299309
}
300310

301311
return manifest, &desc, nil

pkg/v1/remote/image.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ type remoteImage struct {
4646
descriptor *v1.Descriptor
4747
}
4848

49+
func (r *remoteImage) ArtifactType() (string, error) {
50+
// kind of a hack, but RawManifest does appropriate locking/memoization
51+
// and makes sure r.descriptor is populated.
52+
if _, err := r.RawManifest(); err != nil {
53+
return "", err
54+
}
55+
return r.descriptor.ArtifactType, nil
56+
}
57+
4958
var _ partial.CompressedImageCore = (*remoteImage)(nil)
5059

5160
// Image provides access to a remote image reference.

pkg/v1/remote/index.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform)
250250
return nil, err
251251
}
252252
}
253+
254+
if child.MediaType.IsImage() {
255+
mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
256+
// Failing to parse as a manifest should just be ignored.
257+
// The manifest might not be valid, and that's okay.
258+
if mf != nil && !mf.Config.MediaType.IsConfig() {
259+
child.ArtifactType = string(mf.Config.MediaType)
260+
}
261+
}
262+
253263
return &Descriptor{
254264
fetcher: fetcher{
255265
Ref: ref,

pkg/v1/types/types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,12 @@ func (m MediaType) IsIndex() bool {
7171
}
7272
return false
7373
}
74+
75+
// IsConfig returns true if the mediaType represents a config, as opposed to something else, like an image.
76+
func (m MediaType) IsConfig() bool {
77+
switch m {
78+
case OCIConfigJSON, DockerConfigJSON:
79+
return true
80+
}
81+
return false
82+
}

0 commit comments

Comments
 (0)