Skip to content

Commit

Permalink
feat: AppendImage/Index with optional name.Reference
Browse files Browse the repository at this point in the history
Adds `f.AppendImage` and `f.AppendIndex`, which append a `v1.Image` or a
`v1.ImageIndex` to the SIF, respectively.

The `OptAppendReference` functional option can be used to provide a
`name.Reference` that will be stored as an annotation against the new
descriptor in the RootIndex, with the key
`org.opencontainers.image.ref.name`, as is convention for associating
references with items stored in an OCI layout.

Fixes #83
Fixes #81
  • Loading branch information
dtrudg committed Sep 27, 2024
1 parent 15041b9 commit 99dd119
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 0 deletions.
Binary file added pkg/sif/testdata/TestAppendImage/Default.golden
Binary file not shown.
Binary file not shown.
Binary file added pkg/sif/testdata/TestAppendIndex/Default.golden
Binary file not shown.
Binary file not shown.
68 changes: 68 additions & 0 deletions pkg/sif/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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))
}
123 changes: 123 additions & 0 deletions pkg/sif/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
})
}
}

0 comments on commit 99dd119

Please sign in to comment.