diff --git a/pkg/sif/testdata/TestAppendImage/Default.golden b/pkg/sif/testdata/TestAppendImage/Default.golden new file mode 100644 index 0000000..2d28e07 Binary files /dev/null and b/pkg/sif/testdata/TestAppendImage/Default.golden differ diff --git a/pkg/sif/testdata/TestAppendImage/WithReference.golden b/pkg/sif/testdata/TestAppendImage/WithReference.golden new file mode 100644 index 0000000..a056e1c Binary files /dev/null and b/pkg/sif/testdata/TestAppendImage/WithReference.golden differ diff --git a/pkg/sif/testdata/TestAppendIndex/Default.golden b/pkg/sif/testdata/TestAppendIndex/Default.golden new file mode 100644 index 0000000..d188796 Binary files /dev/null and b/pkg/sif/testdata/TestAppendIndex/Default.golden differ diff --git a/pkg/sif/testdata/TestAppendIndex/WithReference.golden b/pkg/sif/testdata/TestAppendIndex/WithReference.golden new file mode 100644 index 0000000..b32f10e Binary files /dev/null and b/pkg/sif/testdata/TestAppendIndex/WithReference.golden differ diff --git a/pkg/sif/update.go b/pkg/sif/update.go index 39b0d58..8bd5e0f 100644 --- a/pkg/sif/update.go +++ b/pkg/sif/update.go @@ -11,7 +11,9 @@ import ( "path/filepath" "slices" + "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/google/go-containerregistry/pkg/v1/types" "github.com/sylabs/sif/v2/pkg/sif" ) @@ -330,3 +332,69 @@ func selectBlobsExcept(keep []v1.Hash) sif.DescriptorSelectorFunc { return false, nil } } + +// appendOpts accumulates append options. +type appendOpts struct { + tempDir string + ref name.Reference +} + +// AppendOpt are used to specify options to apply when appending to a SIF. +type AppendOpt func(*appendOpts) error + +// OptAppendTempDir sets the directory to use for temporary files. If not set, the +// directory returned by os.TempDir is used. +func OptAppendTempDir(d string) AppendOpt { + return func(c *appendOpts) error { + c.tempDir = d + return nil + } +} + +// OptAppendReference sets the reference to be set for the appended item in the +// RootIndex. The reference is added as an `org.opencontainers.image.ref.name` +// in the RootIndex. +func OptAppendReference(r name.Reference) AppendOpt { + return func(c *appendOpts) error { + c.ref = r + return nil + } +} + +// AppendImage appends an image to the SIF f, updating the RootIndex to +// reference it. +func (f *OCIFileImage) AppendImage(img v1.Image, opts ...AppendOpt) error { + return f.append(img, opts...) +} + +// AppendIndex appends an index to the SIF f, updating the RootIndex to +// reference it. +func (f *OCIFileImage) AppendIndex(ii v1.ImageIndex, opts ...AppendOpt) error { + return f.append(ii, opts...) +} + +func (f *OCIFileImage) append(add mutate.Appendable, 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 + } + + ia := mutate.IndexAddendum{Add: add} + if ao.ref != nil { + ia.Annotations = map[string]string{ + "org.opencontainers.image.ref.name": ao.ref.Name(), + } + } + ri = mutate.AppendManifests(ri, ia) + + return f.UpdateRootIndex(ri, OptUpdateTempDir(ao.tempDir)) +} diff --git a/pkg/sif/update_test.go b/pkg/sif/update_test.go index 60426de..5bdee6b 100644 --- a/pkg/sif/update_test.go +++ b/pkg/sif/update_test.go @@ -9,6 +9,7 @@ import ( "os" "testing" + "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" v1mutate "github.com/google/go-containerregistry/pkg/v1/mutate" @@ -152,3 +153,125 @@ func TestUpdate(t *testing.T) { }) } } + +//nolint:dupl +func TestAppendImage(t *testing.T) { + r := rand.NewSource(randomSeed) + newImage, err := random.Image(64, 1, random.WithSource(r)) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + base string + opts []sif.AppendOpt + }{ + { + name: "Default", + base: "hello-world-docker-v2-manifest", + opts: []sif.AppendOpt{}, + }, + { + name: "WithReference", // Replaces many layers with a single layer + base: "hello-world-docker-v2-manifest", + opts: []sif.AppendOpt{ + sif.OptAppendReference(name.MustParseReference("myimage:v1", name.WithDefaultRegistry(""))), + }, + }, + } + 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 := ofi.AppendImage(newImage, tt.opts...); 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) + }) + } +} + +//nolint:dupl +func TestAppendIndex(t *testing.T) { + r := rand.NewSource(randomSeed) + newIndex, err := random.Index(64, 1, 1, random.WithSource(r)) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + base string + opts []sif.AppendOpt + }{ + { + name: "Default", + base: "hello-world-docker-v2-manifest", + opts: []sif.AppendOpt{}, + }, + { + name: "WithReference", // Replaces many layers with a single layer + base: "hello-world-docker-v2-manifest", + opts: []sif.AppendOpt{ + sif.OptAppendReference(name.MustParseReference("myindex:v1", name.WithDefaultRegistry(""))), + }, + }, + } + 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 := ofi.AppendIndex(newIndex, tt.opts...); 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) + }) + } +}