diff --git a/pkg/sif/testdata/TestReplace/ReplaceImageManifest.golden b/pkg/sif/testdata/TestReplace/ReplaceImageManifest.golden new file mode 100644 index 0000000..a650261 Binary files /dev/null and b/pkg/sif/testdata/TestReplace/ReplaceImageManifest.golden differ diff --git a/pkg/sif/testdata/TestReplace/ReplaceImageManifestList.golden b/pkg/sif/testdata/TestReplace/ReplaceImageManifestList.golden new file mode 100644 index 0000000..0dba8c6 Binary files /dev/null and b/pkg/sif/testdata/TestReplace/ReplaceImageManifestList.golden differ diff --git a/pkg/sif/testdata/TestReplace/ReplaceImageNoMatch.golden b/pkg/sif/testdata/TestReplace/ReplaceImageNoMatch.golden new file mode 100644 index 0000000..2d28e07 Binary files /dev/null and b/pkg/sif/testdata/TestReplace/ReplaceImageNoMatch.golden differ diff --git a/pkg/sif/testdata/TestReplace/ReplaceIndexManifest.golden b/pkg/sif/testdata/TestReplace/ReplaceIndexManifest.golden new file mode 100644 index 0000000..cdec4e3 Binary files /dev/null and b/pkg/sif/testdata/TestReplace/ReplaceIndexManifest.golden differ diff --git a/pkg/sif/testdata/TestReplace/ReplaceIndexManifestList.golden b/pkg/sif/testdata/TestReplace/ReplaceIndexManifestList.golden new file mode 100644 index 0000000..bd70cbf Binary files /dev/null and b/pkg/sif/testdata/TestReplace/ReplaceIndexManifestList.golden differ diff --git a/pkg/sif/testdata/TestReplace/ReplaceIndexNoMatch.golden b/pkg/sif/testdata/TestReplace/ReplaceIndexNoMatch.golden new file mode 100644 index 0000000..8940e8b Binary files /dev/null and b/pkg/sif/testdata/TestReplace/ReplaceIndexNoMatch.golden differ diff --git a/pkg/sif/update.go b/pkg/sif/update.go index aac1007..20fa460 100644 --- a/pkg/sif/update.go +++ b/pkg/sif/update.go @@ -408,17 +408,27 @@ func (f *OCIFileImage) append(add mutate.Appendable, opts ...AppendOpt) error { return err } + ri, err = appendToIndex(ri, add, ao) + if err != nil { + return err + } + + return f.UpdateRootIndex(ri, OptUpdateTempDir(ao.tempDir)) +} + +func appendToIndex(base v1.ImageIndex, add mutate.Appendable, ao appendOpts) (v1.ImageIndex, error) { ia := mutate.IndexAddendum{Add: add} + var err error if ao.ref != nil { - ri, err = removeRefAnnotation(ri, ao.ref) + base, err = removeRefAnnotation(base, ao.ref) if err != nil { - return err + return nil, err } d, err := partial.Descriptor(add) if err != nil { - return err + return nil, err } if d.Annotations != nil { ia.Annotations = maps.Clone(d.Annotations) @@ -427,9 +437,8 @@ func (f *OCIFileImage) append(add mutate.Appendable, opts ...AppendOpt) error { } ia.Annotations[imagespec.AnnotationRefName] = ao.ref.Name() } - ri = mutate.AppendManifests(ri, ia) - return f.UpdateRootIndex(ri, OptUpdateTempDir(ao.tempDir)) + return mutate.AppendManifests(base, ia), nil } // removeRefAnnotation removes an existing "org.opencontainers.image.ref.name" @@ -462,3 +471,42 @@ func (f *OCIFileImage) RemoveManifests(matcher match.Matcher) error { } return f.UpdateRootIndex(mutate.RemoveManifests(ri, matcher)) } + +// ReplaceImage writes img to the SIF, replacing any existing manifest that is +// selected by the matcher. Any blobs in the SIF that are no longer referenced +// are removed from the SIF. +func (f *OCIFileImage) ReplaceImage(img v1.Image, matcher match.Matcher, opts ...AppendOpt) error { + return f.replace(img, matcher, opts...) +} + +// ReplaceIndex writes ii to the SIF, replacing any existing manifest that is +// selected by the matcher. Any blobs in the SIF that are no longer referenced +// are removed from the SIF. +func (f *OCIFileImage) ReplaceIndex(ii v1.ImageIndex, matcher match.Matcher, opts ...AppendOpt) error { + return f.replace(ii, matcher, opts...) +} + +func (f *OCIFileImage) replace(add mutate.Appendable, matcher match.Matcher, opts ...AppendOpt) error { + ao := appendOpts{ + tempDir: os.TempDir(), + } + for _, opt := range opts { + if err := opt(&ao); err != nil { + return err + } + } + + ri, err := f.RootIndex() + if err != nil { + return err + } + + ri = mutate.RemoveManifests(ri, matcher) + + ri, err = appendToIndex(ri, add, ao) + if err != nil { + return err + } + + return f.UpdateRootIndex(ri, OptUpdateTempDir(ao.tempDir)) +} diff --git a/pkg/sif/update_test.go b/pkg/sif/update_test.go index 43bd840..2cb1ca0 100644 --- a/pkg/sif/update_test.go +++ b/pkg/sif/update_test.go @@ -439,3 +439,95 @@ func TestRemoveManifests(t *testing.T) { }) } } + +//nolint:dupl +func TestReplace(t *testing.T) { + r := rand.NewSource(randomSeed) + newImage, err := random.Image(64, 1, random.WithSource(r)) + if err != nil { + t.Fatal(err) + } + newIndex, err := random.Index(64, 1, 1, random.WithSource(r)) + if err != nil { + t.Fatal(err) + } + + replaceImage := func(ofi *sif.OCIFileImage, m match.Matcher) error { return ofi.ReplaceImage(newImage, m) } + replaceIndex := func(ofi *sif.OCIFileImage, m match.Matcher) error { return ofi.ReplaceIndex(newIndex, m) } + tests := []struct { + name string + base string + replacement func(ofi *sif.OCIFileImage, m match.Matcher) error + matcher match.Matcher + }{ + { + name: "ReplaceImageManifest", + base: "hello-world-docker-v2-manifest", + replacement: replaceImage, + matcher: match.Platforms(v1.Platform{OS: "linux", Architecture: "arm64", Variant: "v8"}), + }, + { + name: "ReplaceImageManifestList", + base: "hello-world-docker-v2-manifest-list", + replacement: replaceImage, + matcher: match.Platforms(v1.Platform{OS: "linux", Architecture: "arm64", Variant: "v8"}), + }, + { + name: "ReplaceImageNoMatch", + base: "hello-world-docker-v2-manifest", + replacement: replaceImage, + matcher: match.Platforms(v1.Platform{OS: "linux", Architecture: "m68k"}), + }, + { + name: "ReplaceIndexManifest", + base: "hello-world-docker-v2-manifest", + replacement: replaceIndex, + matcher: match.Platforms(v1.Platform{OS: "linux", Architecture: "arm64", Variant: "v8"}), + }, + { + name: "ReplaceIndexManifestList", + base: "hello-world-docker-v2-manifest-list", + replacement: replaceIndex, + matcher: match.Platforms(v1.Platform{OS: "linux", Architecture: "arm64", Variant: "v8"}), + }, + { + name: "ReplaceIndexNoMatch", + base: "hello-world-docker-v2-manifest", + replacement: replaceIndex, + matcher: match.Platforms(v1.Platform{OS: "linux", Architecture: "m68k"}), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sifPath := corpus.SIF(t, tt.base, sif.OptWriteWithSpareDescriptorCapacity(8)) + fi, err := ssif.LoadContainerFromPath(sifPath) + if err != nil { + t.Fatal(err) + } + + ofi, err := sif.FromFileImage(fi) + if err != nil { + t.Fatal(err) + } + + if err := tt.replacement(ofi, tt.matcher); err != nil { + t.Fatal(err) + } + + if err := fi.UnloadContainer(); err != nil { + t.Fatal(err) + } + + b, err := os.ReadFile(sifPath) + if err != nil { + t.Fatal(err) + } + + g := goldie.New(t, + goldie.WithTestNameForDir(true), + ) + + g.Assert(t, tt.name, b) + }) + } +}