diff --git a/pkg/v1/layout/write.go b/pkg/v1/layout/write.go index d0b623c6f..2f0a162cd 100644 --- a/pkg/v1/layout/write.go +++ b/pkg/v1/layout/write.go @@ -23,6 +23,9 @@ import ( "path/filepath" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/google/go-containerregistry/pkg/v1/types" "golang.org/x/sync/errgroup" ) @@ -127,6 +130,85 @@ func (l Path) AppendDescriptor(desc v1.Descriptor) error { return l.WriteFile("index.json", rawIndex, os.ModePerm) } +// ReplaceImage writes a v1.Image to the Path and updates +// the index.json to reference it, replacing any existing one that matches matcher, if found. +func (l Path) ReplaceImage(img v1.Image, matcher match.Matcher, options ...Option) error { + if err := l.WriteImage(img); err != nil { + return err + } + + return l.replaceDescriptor(img, matcher, options...) +} + +// ReplaceIndex writes a v1.ImageIndex to the Path and updates +// the index.json to reference it, replacing any existing one that matches matcher, if found. +func (l Path) ReplaceIndex(ii v1.ImageIndex, matcher match.Matcher, options ...Option) error { + if err := l.WriteIndex(ii); err != nil { + return err + } + + return l.replaceDescriptor(ii, matcher, options...) +} + +// replaceDescriptor adds a descriptor to the index.json of the Path, replacing +// any one matching matcher, if found. +func (l Path) replaceDescriptor(append mutate.Appendable, matcher match.Matcher, options ...Option) error { + ii, err := l.ImageIndex() + if err != nil { + return err + } + + desc, err := partial.Descriptor(append) + if err != nil { + return err + } + + for _, opt := range options { + if err := opt(desc); err != nil { + return err + } + } + + add := mutate.IndexAddendum{ + Add: append, + Descriptor: *desc, + } + ii = mutate.AppendManifests(mutate.RemoveManifests(ii, matcher), add) + + index, err := ii.IndexManifest() + if err != nil { + return err + } + + rawIndex, err := json.MarshalIndent(index, "", " ") + if err != nil { + return err + } + + return l.WriteFile("index.json", rawIndex, os.ModePerm) +} + +// RemoveDescriptors removes any descriptors that match the match.Matcher from the index.json of the Path. +func (l Path) RemoveDescriptors(matcher match.Matcher) error { + ii, err := l.ImageIndex() + if err != nil { + return err + } + ii = mutate.RemoveManifests(ii, matcher) + + index, err := ii.IndexManifest() + if err != nil { + return err + } + + rawIndex, err := json.MarshalIndent(index, "", " ") + if err != nil { + return err + } + + return l.WriteFile("index.json", rawIndex, os.ModePerm) +} + // WriteFile write a file with arbitrary data at an arbitrary location in a v1 // layout. Used mostly internally to write files like "oci-layout" and // "index.json", also can be used to write other arbitrary files. Do *not* use diff --git a/pkg/v1/layout/write_test.go b/pkg/v1/layout/write_test.go index fb92c7f45..57eb9b291 100644 --- a/pkg/v1/layout/write_test.go +++ b/pkg/v1/layout/write_test.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-cmp/cmp" 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/random" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/go-containerregistry/pkg/v1/validate" @@ -235,3 +236,214 @@ func TestDeduplicatedWrites(t *testing.T) { t.Fatal(err) } } + +func TestRemoveDescriptor(t *testing.T) { + // need to set up a basic path + tmp, err := ioutil.TempDir("", "remove-descriptor-test") + if err != nil { + t.Fatal(err) + } + + defer os.RemoveAll(tmp) + + var ii v1.ImageIndex + ii = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // add two images + image1, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendImage(image1); err != nil { + t.Fatal(err) + } + image2, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendImage(image2); err != nil { + t.Fatal(err) + } + + // remove one of the images by descriptor and ensure it is correct + digest1, err := image1.Digest() + if err != nil { + t.Fatal(err) + } + digest2, err := image2.Digest() + if err != nil { + t.Fatal(err) + } + if err := l.RemoveDescriptors(match.Digests(digest1)); err != nil { + t.Fatal(err) + } + // ensure we only have one + ii, err = l.ImageIndex() + if err != nil { + t.Fatal(err) + } + manifest, err := ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != 1 { + t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 1) + } + if manifest.Manifests[0].Digest != digest2 { + t.Fatal("removed wrong digest") + } +} + +func TestReplaceIndex(t *testing.T) { + // need to set up a basic path + tmp, err := ioutil.TempDir("", "replace-index-test") + if err != nil { + t.Fatal(err) + } + + defer os.RemoveAll(tmp) + + var ii v1.ImageIndex + ii = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // add two indexes + index1, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendIndex(index1); err != nil { + t.Fatal(err) + } + index2, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendIndex(index2); err != nil { + t.Fatal(err) + } + index3, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + + // remove one of the indexes by descriptor and ensure it is correct + digest1, err := index1.Digest() + if err != nil { + t.Fatal(err) + } + digest3, err := index3.Digest() + if err != nil { + t.Fatal(err) + } + if err := l.ReplaceIndex(index3, match.Digests(digest1)); err != nil { + t.Fatal(err) + } + // ensure we only have one + ii, err = l.ImageIndex() + if err != nil { + t.Fatal(err) + } + manifest, err := ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != 2 { + t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 2) + } + // we should have digest3, and *not* have digest1 + var have3 bool + for _, m := range manifest.Manifests { + if m.Digest == digest1 { + t.Fatal("found digest1 still not replaced", digest1) + } + if m.Digest == digest3 { + have3 = true + } + } + if !have3 { + t.Fatal("could not find digest3", digest3) + } +} + +func TestReplaceImage(t *testing.T) { + // need to set up a basic path + tmp, err := ioutil.TempDir("", "replace-image-test") + if err != nil { + t.Fatal(err) + } + + defer os.RemoveAll(tmp) + + var ii v1.ImageIndex + ii = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // add two images + image1, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendImage(image1); err != nil { + t.Fatal(err) + } + image2, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendImage(image2); err != nil { + t.Fatal(err) + } + image3, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + + // remove one of the images by descriptor and ensure it is correct + digest1, err := image1.Digest() + if err != nil { + t.Fatal(err) + } + digest3, err := image3.Digest() + if err != nil { + t.Fatal(err) + } + if err := l.ReplaceImage(image3, match.Digests(digest1)); err != nil { + t.Fatal(err) + } + // ensure we only have one + ii, err = l.ImageIndex() + if err != nil { + t.Fatal(err) + } + manifest, err := ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != 2 { + t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 2) + } + // we should have digest3, and *not* have digest1 + var have3 bool + for _, m := range manifest.Manifests { + if m.Digest == digest1 { + t.Fatal("found digest1 still not replaced", digest1) + } + if m.Digest == digest3 { + have3 = true + } + } + if !have3 { + t.Fatal("could not find digest3", digest3) + } +} diff --git a/pkg/v1/mutate/index.go b/pkg/v1/mutate/index.go index b8f88c3b8..140d99aa1 100644 --- a/pkg/v1/mutate/index.go +++ b/pkg/v1/mutate/index.go @@ -20,6 +20,7 @@ import ( "github.com/google/go-containerregistry/pkg/logs" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/match" "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/google/go-containerregistry/pkg/v1/types" ) @@ -56,6 +57,8 @@ func computeDescriptor(ia IndexAddendum) (*v1.Descriptor, error) { type index struct { base v1.ImageIndex adds []IndexAddendum + // remove is removed before adds + remove match.Matcher computed bool manifest *v1.IndexManifest @@ -90,6 +93,17 @@ func (i *index) compute() error { } manifest := m.DeepCopy() manifests := manifest.Manifests + + if i.remove != nil { + var cleanedManifests []v1.Descriptor + for _, m := range manifests { + if !i.remove(m) { + cleanedManifests = append(cleanedManifests, m) + } + } + manifests = cleanedManifests + } + for _, add := range i.adds { desc, err := computeDescriptor(add) if err != nil { @@ -105,6 +119,7 @@ func (i *index) compute() error { logs.Warn.Printf("Unexpected index addendum: %T", add.Add) } } + manifest.Manifests = manifests // With OCI media types, this should not be set, see discussion: diff --git a/pkg/v1/mutate/mutate.go b/pkg/v1/mutate/mutate.go index 798f91467..78b768fcc 100644 --- a/pkg/v1/mutate/mutate.go +++ b/pkg/v1/mutate/mutate.go @@ -27,6 +27,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/internal/gzip" + "github.com/google/go-containerregistry/pkg/v1/match" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" ) @@ -92,6 +93,14 @@ func AppendManifests(base v1.ImageIndex, adds ...IndexAddendum) v1.ImageIndex { } } +// RemoveManifests removes any descriptors that match the match.Matcher. +func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) v1.ImageIndex { + return &index{ + base: base, + remove: matcher, + } +} + // Config mutates the provided v1.Image to have the provided v1.Config func Config(base v1.Image, cfg v1.Config) (v1.Image, error) { cf, err := base.ConfigFile() diff --git a/pkg/v1/mutate/mutate_test.go b/pkg/v1/mutate/mutate_test.go index 31bccf435..a19e94232 100644 --- a/pkg/v1/mutate/mutate_test.go +++ b/pkg/v1/mutate/mutate_test.go @@ -30,6 +30,7 @@ import ( "github.com/google/go-cmp/cmp" 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/mutate" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/stream" @@ -485,6 +486,39 @@ func TestCanonical(t *testing.T) { } } +func TestRemoveManifests(t *testing.T) { + // Load up the registry. + count := 3 + for i := 0; i < count; i++ { + ii, err := random.Index(1024, int64(count), int64(count)) + if err != nil { + t.Fatal(err) + } + // test removing the first layer, second layer or the third layer + manifest, err := ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != count { + t.Fatalf("mismatched manifests on setup, had %d, expected %d", len(manifest.Manifests), count) + } + digest := manifest.Manifests[i].Digest + ii = mutate.RemoveManifests(ii, match.Digests(digest)) + manifest, err = ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != (count - 1) { + t.Fatalf("mismatched manifests after removal, had %d, expected %d", len(manifest.Manifests), count-1) + } + for j, m := range manifest.Manifests { + if m.Digest == digest { + t.Fatalf("unexpectedly found removed hash %v at position %d", digest, j) + } + } + } +} + func assertMTime(t *testing.T, layer v1.Layer, expectedTime time.Time) { l, err := layer.Uncompressed()