Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: RemoveBlob / RemoveDescriptors #90

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added pkg/sif/testdata/TestRemoveBlob/Valid.golden
Binary file not shown.
Binary file not shown.
Binary file added pkg/sif/testdata/TestRemoveManifests/Valid.golden
Binary file not shown.
Binary file added pkg/sif/testdata/TestUpdate/RemoveOKNoTemp.golden
Binary file not shown.
80 changes: 57 additions & 23 deletions pkg/sif/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package sif

import (
"bytes"
"errors"
"io"
"maps"
"os"
Expand All @@ -24,7 +25,10 @@ import (

// updateOpts accumulates update options.
type updateOpts struct {
// tempDir is os.TempDir or user supplied value
tempDir string
// cacheDir created inside tempDir
cacheDir string
}

// UpdateOpt are used to specify options to apply when updating a SIF.
Expand Down Expand Up @@ -57,6 +61,11 @@ func (f *OCIFileImage) UpdateRootIndex(ii v1.ImageIndex, opts ...UpdateOpt) erro
return err
}
}
defer func() {
if uo.cacheDir != "" {
os.RemoveAll(uo.cacheDir)
}
}()

// If the existing OCI.RootIndex in the SIF matches ii, then there is nothing to do.
sifRootIndex, err := f.RootIndex()
Expand Down Expand Up @@ -84,12 +93,7 @@ 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
}
defer os.RemoveAll(blobCache)
cachedBlobs, keepBlobs, err := cacheIndexBlobs(ii, sifBlobs, blobCache)
cachedBlobs, keepBlobs, err := uo.cacheIndexBlobs(ii, sifBlobs)
if err != nil {
return err
}
Expand All @@ -110,7 +114,7 @@ func (f *OCIFileImage) UpdateRootIndex(ii v1.ImageIndex, opts ...UpdateOpt) erro

// Write new (cached) blobs from ii into the SIF.
for _, b := range cachedBlobs {
rc, err := readCacheBlob(b, blobCache)
rc, err := uo.readCacheBlob(b)
if err != nil {
return err
}
Expand Down Expand Up @@ -160,7 +164,7 @@ func sifBlobs(fi *sif.FileImage) ([]v1.Hash, error) {
// with filenames equal to their digest. The function returns two lists of blobs
// - those that were cached (in ii but not skip), and those that were skipped
// (in ii and skip).
func cacheIndexBlobs(ii v1.ImageIndex, skip []v1.Hash, cacheDir string) ([]v1.Hash, []v1.Hash, error) {
func (uo *updateOpts) cacheIndexBlobs(ii v1.ImageIndex, skip []v1.Hash) ([]v1.Hash, []v1.Hash, error) {
index, err := ii.IndexManifest()
if err != nil {
return nil, nil, err
Expand All @@ -178,7 +182,7 @@ func cacheIndexBlobs(ii v1.ImageIndex, skip []v1.Hash, cacheDir string) ([]v1.Ha
return nil, nil, err
}
// Cache children of this ImageIndex
childCached, childSkipped, err := cacheIndexBlobs(childIndex, skip, cacheDir)
childCached, childSkipped, err := uo.cacheIndexBlobs(childIndex, skip)
if err != nil {
return nil, nil, err
}
Expand All @@ -194,7 +198,7 @@ func cacheIndexBlobs(ii v1.ImageIndex, skip []v1.Hash, cacheDir string) ([]v1.Ha
return nil, nil, err
}
rc := io.NopCloser(bytes.NewReader(rm))
if err := writeCacheBlob(rc, desc.Digest, cacheDir); err != nil {
if err := uo.writeCacheBlob(rc, desc.Digest); err != nil {
return nil, nil, err
}
cached = append(cached, desc.Digest)
Expand All @@ -204,7 +208,7 @@ func cacheIndexBlobs(ii v1.ImageIndex, skip []v1.Hash, cacheDir string) ([]v1.Ha
if err != nil {
return nil, nil, err
}
childCached, childSkipped, err := cacheImageBlobs(childImage, skip, cacheDir)
childCached, childSkipped, err := uo.cacheImageBlobs(childImage, skip)
if err != nil {
return nil, nil, err
}
Expand All @@ -223,7 +227,7 @@ func cacheIndexBlobs(ii v1.ImageIndex, skip []v1.Hash, cacheDir string) ([]v1.Ha
// with filenames equal to their digest. The function returns lists of blobs
// that were cached (in ii but not skip), and those that were skipped (in ii and
// skipDigests).
func cacheImageBlobs(im v1.Image, skip []v1.Hash, cacheDir string) ([]v1.Hash, []v1.Hash, error) {
func (uo *updateOpts) cacheImageBlobs(im v1.Image, skip []v1.Hash) ([]v1.Hash, []v1.Hash, error) {
cached := []v1.Hash{}
skipped := []v1.Hash{}

Expand All @@ -247,7 +251,7 @@ func cacheImageBlobs(im v1.Image, skip []v1.Hash, cacheDir string) ([]v1.Hash, [
if err != nil {
return nil, nil, err
}
if err := writeCacheBlob(rc, ld, cacheDir); err != nil {
if err := uo.writeCacheBlob(rc, ld); err != nil {
return nil, nil, err
}
cached = append(cached, ld)
Expand All @@ -266,7 +270,7 @@ func cacheImageBlobs(im v1.Image, skip []v1.Hash, cacheDir string) ([]v1.Hash, [
return nil, nil, err
}
rc := io.NopCloser(bytes.NewReader(c))
if err := writeCacheBlob(rc, mf.Config.Digest, cacheDir); err != nil {
if err := uo.writeCacheBlob(rc, mf.Config.Digest); err != nil {
return nil, nil, err
}
cached = append(cached, mf.Config.Digest)
Expand All @@ -286,18 +290,25 @@ func cacheImageBlobs(im v1.Image, skip []v1.Hash, cacheDir string) ([]v1.Hash, [
return nil, nil, err
}
rc := io.NopCloser(bytes.NewReader(rm))
if err := writeCacheBlob(rc, id, cacheDir); err != nil {
if err := uo.writeCacheBlob(rc, id); err != nil {
return nil, nil, err
}
cached = append(cached, id)

return cached, skipped, nil
}

// 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 {
path := filepath.Join(cacheDir, digest.String())
// writeCacheBlob writes blob content from rc into a cache directory with
// filename equal to specified digest.
func (uo *updateOpts) writeCacheBlob(rc io.ReadCloser, digest v1.Hash) error {
if uo.cacheDir == "" {
var err error
if uo.cacheDir, err = os.MkdirTemp(uo.tempDir, ""); err != nil {
return err
}
}

path := filepath.Join(uo.cacheDir, digest.String())
f, err := os.Create(path)
if err != nil {
return err
Expand All @@ -315,10 +326,15 @@ func writeCacheBlob(rc io.ReadCloser, digest v1.Hash, cacheDir string) error {
return nil
}

// readCacheBlob returns a ReadCloser that will read blob content from cacheDir
// with filename equal to specified digest.
func readCacheBlob(digest v1.Hash, cacheDir string) (io.ReadCloser, error) {
path := filepath.Join(cacheDir, digest.String())
var errNoCacheDir = errors.New("cacheDir not set")

// readCacheBlob returns a ReadCloser that will read blob content from the cache
// directory with filename equal to specified digest.
func (uo *updateOpts) readCacheBlob(digest v1.Hash) (io.ReadCloser, error) {
if uo.cacheDir == "" {
return nil, errNoCacheDir
}
path := filepath.Join(uo.cacheDir, digest.String())
f, err := os.Open(path)
if err != nil {
return nil, err
Expand Down Expand Up @@ -428,3 +444,21 @@ func removeRefAnnotation(ii v1.ImageIndex, ref name.Reference) (v1.ImageIndex, e
},
)
}

// 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))
}

// RemoveManifests modifies the SIF file associated with f so that its RootIndex
// no longer holds manifests selected by matcher. Any blobs in the SIF that are
// no longer referenced are removed from the SIF.
func (f *OCIFileImage) RemoveManifests(matcher match.Matcher) error {
ri, err := f.RootIndex()
if err != nil {
return err
}
return f.UpdateRootIndex(mutate.RemoveManifests(ri, matcher))
}
125 changes: 125 additions & 0 deletions 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 Down Expand Up @@ -314,3 +315,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 TestRemoveManifests(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.RemoveManifests(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)
})
}
}