Skip to content

Commit

Permalink
Allow mutate.Annotations to annotate an Image or ImageIndex (#1082)
Browse files Browse the repository at this point in the history
* Allow mutate.Annotations to annotate an Image or ImageIndex

* Correctly spell annotateable

* Support arbitrary RawManifest inputs, add unit tests
  • Loading branch information
imjasonh committed Jul 20, 2021
1 parent a0b9468 commit 2f6fbf7
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 26 deletions.
3 changes: 2 additions & 1 deletion cmd/crane/cmd/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -91,7 +92,7 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
log.Fatalf("mutating config: %v", err)
}

img = mutate.Annotations(img, annotations)
img = mutate.Annotations(img, annotations).(v1.Image)

// If the new ref isn't provided, write over the original image.
// If that ref was provided by digest (e.g., output from
Expand Down
22 changes: 16 additions & 6 deletions pkg/v1/mutate/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ type index struct {
// remove is removed before adds
remove match.Matcher

computed bool
manifest *v1.IndexManifest
mediaType *types.MediaType
imageMap map[v1.Hash]v1.Image
indexMap map[v1.Hash]v1.ImageIndex
layerMap map[v1.Hash]v1.Layer
computed bool
manifest *v1.IndexManifest
annotations map[string]string
mediaType *types.MediaType
imageMap map[v1.Hash]v1.Image
indexMap map[v1.Hash]v1.ImageIndex
layerMap map[v1.Hash]v1.Layer
}

var _ v1.ImageIndex = (*index)(nil)
Expand Down Expand Up @@ -137,6 +138,15 @@ func (i *index) compute() error {
}
}

if i.annotations != nil {
if manifest.Annotations == nil {
manifest.Annotations = map[string]string{}
}
for k, v := range i.annotations {
manifest.Annotations[k] = v
}
}

i.manifest = manifest
i.computed = true
return nil
Expand Down
70 changes: 65 additions & 5 deletions pkg/v1/mutate/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package mutate
import (
"archive/tar"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
Expand All @@ -28,6 +29,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/match"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
)
Expand Down Expand Up @@ -113,12 +115,70 @@ func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
return ConfigFile(base, cf)
}

// Annotations mutates the provided v1.Image to have the provided annotations
func Annotations(base v1.Image, annotations map[string]string) v1.Image {
return &image{
base: base,
annotations: annotations,
// Annotatable represents a manifest that can carry annotations.
type Annotatable interface {
partial.WithRawManifest
}

// Annotations mutates the annotations on an annotatable image or index manifest.
//
// The annotatable input is expected to be a v1.Image or v1.ImageIndex, and
// returns the same type. You can type-assert the result like so:
//
// img := Annotations(empty.Image, map[string]string{
// "foo": "bar",
// }).(v1.Image)
//
// Or for an index:
//
// idx := Annotations(empty.Index, map[string]string{
// "foo": "bar",
// }).(v1.ImageIndex)
//
// If the input Annotatable is not an Image or ImageIndex, the result will
// attempt to lazily annotate the raw manifest.
func Annotations(f Annotatable, anns map[string]string) Annotatable {
if img, ok := f.(v1.Image); ok {
return &image{
base: img,
annotations: anns,
}
}
if idx, ok := f.(v1.ImageIndex); ok {
return &index{
base: idx,
annotations: anns,
}
}
return arbitraryRawManifest{f, anns}
}

type arbitraryRawManifest struct {
a Annotatable
anns map[string]string
}

func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
b, err := a.a.RawManifest()
if err != nil {
return nil, err
}
var m map[string]interface{}
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
if ann, ok := m["annotations"]; ok {
if annm, ok := ann.(map[string]string); ok {
for k, v := range a.anns {
annm[k] = v
}
} else {
return nil, fmt.Errorf(".annotations is not a map: %T", ann)
}
} else {
m["annotations"] = a.anns
}
return json.Marshal(m)
}

// ConfigFile mutates the provided v1.Image to have the provided v1.ConfigFile
Expand Down
50 changes: 36 additions & 14 deletions pkg/v1/mutate/mutate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,21 +266,43 @@ func TestMutateConfig(t *testing.T) {
}
}

func TestAnnotations(t *testing.T) {
source := sourceImage(t)

newAnnotations := map[string]string{
"im.the.first.annotation": "hello world",
}

result := mutate.Annotations(source, newAnnotations)

if configDigestsAreEqual(t, source, result) {
t.Errorf("mutating the manifest annotations MUST mutate the config digest")
}
type arbitrary struct {
}

if err := validate.Image(result); err != nil {
t.Errorf("validate.Image() = %v", err)
func (arbitrary) RawManifest() ([]byte, error) {
return []byte(`{"hello":"world"}`), nil
}
func TestAnnotations(t *testing.T) {
anns := map[string]string{
"foo": "bar",
}

for _, c := range []struct {
desc string
in mutate.Annotatable
want string
}{{
desc: "image",
in: empty.Image,
want: `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":115,"digest":"sha256:5b943e2b943f6c81dbbd4e2eca5121f4fcc39139e3d1219d6d89bd925b77d9fe"},"layers":[],"annotations":{"foo":"bar"}}`,
}, {
desc: "index",
in: empty.Index,
want: `{"schemaVersion":2,"manifests":null,"annotations":{"foo":"bar"}}`,
}, {
desc: "arbitrary",
in: arbitrary{},
want: `{"annotations":{"foo":"bar"},"hello":"world"}`,
}} {
t.Run(c.desc, func(t *testing.T) {
got, err := mutate.Annotations(c.in, anns).RawManifest()
if err != nil {
t.Fatalf("Annotations: %v", err)
}
if d := cmp.Diff(c.want, string(got)); d != "" {
t.Errorf("Diff(-want,+got): %s", d)
}
})
}
}

Expand Down

0 comments on commit 2f6fbf7

Please sign in to comment.