Skip to content

Commit

Permalink
Support artifactType, for images whose config.mediaType is not a conf…
Browse files Browse the repository at this point in the history
…ig (#1541)

* Support artifactType, for images whose config.mediaType is not a config

* lint feedback

* remove empty branches

* avoid nil panics

* unit test

* doc comment
  • Loading branch information
imjasonh committed Jan 27, 2023
1 parent 11843ba commit 759b19f
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 10 deletions.
15 changes: 8 additions & 7 deletions pkg/v1/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ type IndexManifest struct {

// Descriptor holds a reference from the manifest to one of its constituent elements.
type Descriptor struct {
MediaType types.MediaType `json:"mediaType"`
Size int64 `json:"size"`
Digest Hash `json:"digest"`
Data []byte `json:"data,omitempty"`
URLs []string `json:"urls,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Platform *Platform `json:"platform,omitempty"`
MediaType types.MediaType `json:"mediaType"`
Size int64 `json:"size"`
Digest Hash `json:"digest"`
Data []byte `json:"data,omitempty"`
URLs []string `json:"urls,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Platform *Platform `json:"platform,omitempty"`
ArtifactType string `json:"artifactType,omitempty"`
}

// ParseManifest parses the io.Reader's contents into a Manifest.
Expand Down
53 changes: 53 additions & 0 deletions pkg/v1/mutate/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/google/go-containerregistry/pkg/v1/validate"
Expand Down Expand Up @@ -180,3 +181,55 @@ func TestIndexImmutability(t *testing.T) {
}
})
}

// TestAppend_ArtifactType tests that appending an image manifest that has a
// non-standard config.mediaType to an index, results in the image's
// config.mediaType being hoisted into the descriptor inside the index,
// as artifactType.
func TestAppend_ArtifactType(t *testing.T) {
for _, c := range []struct {
desc, configMediaType, wantArtifactType string
}{{
desc: "standard config.mediaType, no artifactType",
configMediaType: string(types.DockerConfigJSON),
wantArtifactType: "",
}, {
desc: "non-standard config.mediaType, want artifactType",
configMediaType: "application/vnd.custom.something",
wantArtifactType: "application/vnd.custom.something",
}} {
t.Run(c.desc, func(t *testing.T) {
img, err := random.Image(1, 1)
if err != nil {
t.Fatalf("random.Image: %v", err)
}
img = mutate.ConfigMediaType(img, types.MediaType(c.configMediaType))
idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{
Add: img,
})
mf, err := idx.IndexManifest()
if err != nil {
t.Fatalf("IndexManifest: %v", err)
}
if got := mf.Manifests[0].ArtifactType; got != c.wantArtifactType {
t.Errorf("manifest artifactType: got %q, want %q", got, c.wantArtifactType)
}

desc, err := partial.Descriptor(img)
if err != nil {
t.Fatalf("partial.Descriptor: %v", err)
}
if got := desc.ArtifactType; got != c.wantArtifactType {
t.Errorf("descriptor artifactType: got %q, want %q", got, c.wantArtifactType)
}

gotAT, err := partial.ArtifactType(img)
if err != nil {
t.Fatalf("partial.ArtifactType: %v", err)
}
if gotAT != c.wantArtifactType {
t.Errorf("partial.ArtifactType: got %q, want %q", gotAT, c.wantArtifactType)
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/v1/mutate/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,8 @@ func MediaType(img v1.Image, mt types.MediaType) v1.Image {
}

// ConfigMediaType modifies the MediaType() of the given image's Config.
//
// If !mt.IsConfig(), this will be the image's artifactType in any indexes it's a part of.
func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image {
return &image{
base: img,
Expand Down
35 changes: 35 additions & 0 deletions pkg/v1/partial/with.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,28 @@ func Descriptor(d Describable) (*v1.Descriptor, error) {
if desc.MediaType, err = d.MediaType(); err != nil {
return nil, err
}
if wat, ok := d.(withArtifactType); ok {
if desc.ArtifactType, err = wat.ArtifactType(); err != nil {
return nil, err
}
} else {
if wrm, ok := d.(WithRawManifest); ok && desc.MediaType.IsImage() {
mf, _ := Manifest(wrm)
// Failing to parse as a manifest should just be ignored.
// The manifest might not be valid, and that's okay.
if mf != nil && !mf.Config.MediaType.IsConfig() {
desc.ArtifactType = string(mf.Config.MediaType)
}
}
}

return &desc, nil
}

type withArtifactType interface {
ArtifactType() (string, error)
}

type withUncompressedSize interface {
UncompressedSize() (int64, error)
}
Expand Down Expand Up @@ -399,3 +417,20 @@ func unwrap(i any) any {
}
return i
}

// ArtifactType returns the artifact type for the given manifest.
//
// If the manifest reports its own artifact type, that's returned, otherwise
// the manifest is parsed and, if successful, its config.mediaType is returned.
func ArtifactType(w WithManifest) (string, error) {
if wat, ok := w.(withArtifactType); ok {
return wat.ArtifactType()
}
mf, _ := w.Manifest()
// Failing to parse as a manifest should just be ignored.
// The manifest might not be valid, and that's okay.
if mf != nil && !mf.Config.MediaType.IsConfig() {
return string(mf.Config.MediaType), nil
}
return "", nil
}
16 changes: 13 additions & 3 deletions pkg/v1/remote/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,15 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
return nil, nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.Ref)
}
}

var artifactType string
mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
// Failing to parse as a manifest should just be ignored.
// The manifest might not be valid, and that's okay.
if mf != nil && !mf.Config.MediaType.IsConfig() {
artifactType = string(mf.Config.MediaType)
}

// Do nothing for tags; I give up.
//
// We'd like to validate that the "Docker-Content-Digest" header matches what is returned by the registry,
Expand All @@ -293,9 +302,10 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType

// Return all this info since we have to calculate it anyway.
desc := v1.Descriptor{
Digest: digest,
Size: size,
MediaType: mediaType,
Digest: digest,
Size: size,
MediaType: mediaType,
ArtifactType: artifactType,
}

return manifest, &desc, nil
Expand Down
9 changes: 9 additions & 0 deletions pkg/v1/remote/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ type remoteImage struct {
descriptor *v1.Descriptor
}

func (r *remoteImage) ArtifactType() (string, error) {
// kind of a hack, but RawManifest does appropriate locking/memoization
// and makes sure r.descriptor is populated.
if _, err := r.RawManifest(); err != nil {
return "", err
}
return r.descriptor.ArtifactType, nil
}

var _ partial.CompressedImageCore = (*remoteImage)(nil)

// Image provides access to a remote image reference.
Expand Down
10 changes: 10 additions & 0 deletions pkg/v1/remote/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,16 @@ func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform)
return nil, err
}
}

if child.MediaType.IsImage() {
mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
// Failing to parse as a manifest should just be ignored.
// The manifest might not be valid, and that's okay.
if mf != nil && !mf.Config.MediaType.IsConfig() {
child.ArtifactType = string(mf.Config.MediaType)
}
}

return &Descriptor{
fetcher: fetcher{
Ref: ref,
Expand Down
9 changes: 9 additions & 0 deletions pkg/v1/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,12 @@ func (m MediaType) IsIndex() bool {
}
return false
}

// IsConfig returns true if the mediaType represents a config, as opposed to something else, like an image.
func (m MediaType) IsConfig() bool {
switch m {
case OCIConfigJSON, DockerConfigJSON:
return true
}
return false
}

0 comments on commit 759b19f

Please sign in to comment.