Skip to content

Commit

Permalink
feat: RemoveBlob / RemoveDescriptors
Browse files Browse the repository at this point in the history
Implement `RemoveBlob` to remove a blob from the SIF without updating
the RootIndex.

Implement `RemoveDescriptors` to remove specified descriptors from the
SIF RootIndex, and clean up any orphan blobs.

In order that `RemoveDescriptors` can be run without requiring a temp
dir, the update code is modified to accept `OptUpdateNoTemp`. When this
is set only updates that do not require a tempdir will succeed...
without creating a tempDir.

Fixes #84
  • Loading branch information
dtrudg committed Sep 30, 2024
1 parent 376ac47 commit 4690c12
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 6 deletions.
Binary file added pkg/sif/testdata/TestRemoveBlob/Valid.golden
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added pkg/sif/testdata/TestUpdate/RemoveOKNoTemp.golden
Binary file not shown.
48 changes: 43 additions & 5 deletions pkg/sif/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
// updateOpts accumulates update options.
type updateOpts struct {
tempDir string
noTemp bool
}

// UpdateOpt are used to specify options to apply when updating a SIF.
Expand All @@ -44,6 +45,16 @@ func OptUpdateTempDir(d string) UpdateOpt {
}
}

// OptUpdateNoTemp enforces that no tempDir will be created, and no blobs will
// be cached to disk, during the update operation. Any update that requires
// adding blobs to the SIF will fail with errUpdateRequiresTemp.
func OptUpdateNoTemp(n bool) UpdateOpt {
return func(c *updateOpts) error {
c.noTemp = n
return nil
}
}

// UpdateRootIndex modifies the SIF file associated with f so that it holds the
// content of ImageIndex ii. The RootIndex of the SIF is replaced with ii. Any
// blobs in the SIF that are not referenced in ii are removed from the SIF. Any
Expand All @@ -52,7 +63,8 @@ func OptUpdateTempDir(d string) UpdateOpt {
//
// UpdateRootIndex may create one or more temporary files during the update
// process. By default, the directory returned by os.TempDir is used. To
// override this, consider using OptUpdateTmpDir.
// override this, consider using OptUpdateTmpDir. Alternatively, updates which
// do not add blobs to the SIF may use OptUpdateNoTemp.
func (f *OCIFileImage) UpdateRootIndex(ii v1.ImageIndex, opts ...UpdateOpt) error {
uo := updateOpts{
tempDir: os.TempDir(),
Expand Down Expand Up @@ -89,11 +101,14 @@ func (f *OCIFileImage) UpdateRootIndex(ii v1.ImageIndex, opts ...UpdateOpt) erro
// Cache all new blobs referenced by the new ImageIndex and its child
// indices / images, which aren't already in the SIF. cachedblobs are new
// things to add. keepBlobs already exist in the SIF and should be kept.
blobCache, err := os.MkdirTemp(uo.tempDir, "")
if err != nil {
return err
blobCache := ""
if !uo.noTemp {
blobCache, err = os.MkdirTemp(uo.tempDir, "")
if err != nil {
return err
}
defer os.RemoveAll(blobCache)
}
defer os.RemoveAll(blobCache)
cachedBlobs, keepBlobs, err := cacheIndexBlobs(ii, sifBlobs, blobCache)
if err != nil {
return err
Expand Down Expand Up @@ -160,6 +175,8 @@ func sifBlobs(fi *sif.FileImage) ([]v1.Hash, error) {
return sifBlobs, nil
}

var errUpdateRequiresTemp = errors.New("update operation requires a temporary directory to cache blobs")

// cacheIndexBlobs will cache all blobs referenced by ii, except those with
// digests specified in skip. The blobs will be cached to files in cacheDir,
// with filenames equal to their digest. The function returns two lists of blobs
Expand Down Expand Up @@ -302,6 +319,9 @@ func cacheImageBlobs(im v1.Image, skip []v1.Hash, cacheDir string) ([]v1.Hash, [
// writeCacheBlob writes blob content from rc into tmpDir with filename equal to
// specified digest.
func writeCacheBlob(rc io.ReadCloser, digest v1.Hash, cacheDir string) error {
if cacheDir == "" {
return errUpdateRequiresTemp
}
path := filepath.Join(cacheDir, digest.String())
f, err := os.Create(path)
if err != nil {
Expand Down Expand Up @@ -470,3 +490,21 @@ func removeRefAnnotation(ii v1.ImageIndex, ref name.Reference) (v1.ImageIndex, e
ii = mutate.RemoveManifests(ii, m)
return mutate.AppendManifests(ii, ia), nil
}

// RemoveBlob removes a blob from the SIF f, without modifying the rootIndex.
func (f *OCIFileImage) RemoveBlob(hash v1.Hash) error {
return f.sif.DeleteObjects(sif.WithOCIBlobDigest(hash),
sif.OptDeleteZero(true),
sif.OptDeleteCompact(true))
}

// RemoveDescriptors modifies the SIF file associated with f so that its
// RootIndex no longer holds descriptors selected by matcher. Any blobs in the
// SIF that are no longer referenced are removed from the SIF.
func (f *OCIFileImage) RemoveDescriptors(matcher match.Matcher) error {
ri, err := f.RootIndex()
if err != nil {
return err
}
return f.UpdateRootIndex(mutate.RemoveManifests(ri, matcher), OptUpdateNoTemp(true))
}
168 changes: 167 additions & 1 deletion pkg/sif/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
match "github.com/google/go-containerregistry/pkg/v1/match"
v1mutate "github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/types"
Expand All @@ -32,6 +33,7 @@ func TestUpdate(t *testing.T) {
base string
updater func(*testing.T, v1.ImageIndex) v1.ImageIndex
opts []sif.UpdateOpt
wantErr bool
}{
{
name: "AddLayer",
Expand Down Expand Up @@ -111,6 +113,39 @@ func TestUpdate(t *testing.T) {
return v1mutate.AppendManifests(ii, v1mutate.IndexAddendum{Add: addIdx})
},
},
// Can't update to a root index with *new* blobs without a temp directory.
{
name: "AddFailNoTemp",
base: "hello-world-docker-v2-manifest",
updater: func(t *testing.T, ii v1.ImageIndex) v1.ImageIndex {
t.Helper()
addIdx, err := random.Index(64, 1, 1, random.WithSource(r))
if err != nil {
t.Fatal(err)
}
if err != nil {
t.Fatal(err)
}
return v1mutate.AppendManifests(ii, v1mutate.IndexAddendum{Add: addIdx})
},
opts: []sif.UpdateOpt{sif.OptUpdateNoTemp(true)},
wantErr: true,
},
// Can update to a root index without new blobs without a temp directory.
{
name: "RemoveOKNoTemp",
base: "hello-world-docker-v2-manifest",
updater: func(t *testing.T, ii v1.ImageIndex) v1.ImageIndex {
t.Helper()
ih, err := v1.NewHash("sha256:432f982638b3aefab73cc58ab28f5c16e96fdb504e8c134fc58dff4bae8bf338")
if err != nil {
t.Fatal(err)
}
return v1mutate.RemoveManifests(ii, match.Digests(ih))
},
opts: []sif.UpdateOpt{sif.OptUpdateNoTemp(true)},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -132,7 +167,14 @@ func TestUpdate(t *testing.T) {

ii = tt.updater(t, ii)

if err := sif.Update(fi, ii, tt.opts...); err != nil {
err = sif.Update(fi, ii, tt.opts...)
if tt.wantErr {
if err == nil {
t.Errorf("expected error, but got nil")
}
return
}
if err != nil {
t.Fatal(err)
}

Expand Down Expand Up @@ -314,3 +356,127 @@ func TestAppendMultiple(t *testing.T) {
)
g.Assert(t, "image", b)
}

func TestRemoveBlob(t *testing.T) {
validDigest, err := v1.NewHash("sha256:7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01")
if err != nil {
t.Fatal(err)
}

otherDigest, err := v1.NewHash("sha256:e66fc843f1291ede94f0ecb3dbd8d277d4b05a8a4ceba1e211365dae9adb17da")
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
base string
digest v1.Hash
wantErr bool
}{
{
name: "Valid",
base: "hello-world-docker-v2-manifest",
digest: validDigest,
wantErr: false,
},
{
name: "NotFound",
base: "hello-world-docker-v2-manifest",
digest: otherDigest,
wantErr: true,
},
}
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)
}

err = ofi.RemoveBlob(tt.digest)
if tt.wantErr {
if err == nil {
t.Errorf("expected error, but nil returned")
}
return
}
if 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)
})
}
}

func TestRemoveDescriptors(t *testing.T) {
tests := []struct {
name string
matcher match.Matcher
base string
}{
{
name: "Valid",
base: "hello-world-docker-v2-manifest-list",
matcher: match.Platforms(v1.Platform{OS: "linux", Architecture: "ppc64le"}),
},
{
name: "NoMatch",
base: "hello-world-docker-v2-manifest-list",
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 := ofi.RemoveDescriptors(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)
})
}
}

0 comments on commit 4690c12

Please sign in to comment.