Skip to content

Commit 404f9b9

Browse files
Kern WalsterKern--
authored andcommitted
Attach SOCI index GC to Image
Before this change, SOCI indexes were given a root garbage collection label when using the containerd content store. This meant that in order to remove SOCI indexes, customers would have to know to delete the content in containerd (e.g. with ctr). This change adds an option to the `Build` API to skip the GC root label and instead allow the caller to manage GC labels. As an example, it updates the CLI's `create` command to set the SOCI index as a content ref for the image. When using the contaienrd content store, the SOCI index will be removed if/when the image gets removed. Signed-off-by: Kern Walster <walster@amazon.com>
1 parent 84ad7cb commit 404f9b9

3 files changed

Lines changed: 91 additions & 25 deletions

File tree

cmd/soci/commands/create.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const (
3333
spanSizeFlag = "span-size"
3434
minLayerSizeFlag = "min-layer-size"
3535
optimizationFlag = "optimizations"
36+
sociIndexGCLabel = "containerd.io/gc.ref.content.soci-index"
3637
)
3738

3839
// CreateCommand creates SOCI index for an image
@@ -130,10 +131,22 @@ var CreateCommand = cli.Command{
130131
}
131132

132133
for _, plat := range ps {
133-
_, err = builder.Build(ctx, srcImg, soci.WithPlatform(plat))
134+
batchCtx, done, err := blobStore.BatchOpen(ctx)
134135
if err != nil {
135136
return err
136137
}
138+
defer done(ctx)
139+
140+
indexWithMetadata, err := builder.Build(batchCtx, srcImg, soci.WithPlatform(plat), soci.WithNoGarbageCollectionLabel())
141+
if err != nil {
142+
return err
143+
}
144+
145+
if srcImg.Labels == nil {
146+
srcImg.Labels = make(map[string]string)
147+
}
148+
srcImg.Labels[sociIndexGCLabel] = indexWithMetadata.Desc.Digest.String()
149+
is.Update(ctx, srcImg, "labels")
137150
}
138151

139152
return nil

integration/create_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"fmt"
2323
"path/filepath"
24+
"strings"
2425
"testing"
2526

2627
"github.com/awslabs/soci-snapshotter/config"
@@ -232,3 +233,34 @@ func TestSociCreateGarbageCollection(t *testing.T) {
232233
t.Fatal(fmt.Errorf("resources unexpectedly garbage collected: %v", err))
233234
}
234235
}
236+
237+
func TestSociImageGCLabel(t *testing.T) {
238+
image := rabbitmqImage
239+
sh, done := newSnapshotterBaseShell(t)
240+
defer done()
241+
242+
extraContainerdConfig := `
243+
[plugins."io.containerd.gc.v1.scheduler"]
244+
deletion_threshold = 1`
245+
246+
rebootContainerd(t, sh, getContainerdConfigToml(t, false, extraContainerdConfig), getSnapshotterConfigToml(t, false, tcpMetricsConfig, GetContentStoreConfigToml(store.WithType(config.ContainerdContentStoreType))))
247+
248+
imgInfo := dockerhub(image)
249+
sh.X("nerdctl", "pull", "-q", imgInfo.ref)
250+
indexDigest := buildIndex(sh, imgInfo, withMinLayerSize(0), withContentStoreType(config.ContainerdContentStoreType))
251+
252+
// This should succeed because the index should still exist
253+
sh.X("ctr", "content", "get", indexDigest)
254+
255+
sh.X("nerdctl", "rmi", imgInfo.ref)
256+
257+
// This should fail because the index should be GC'd
258+
o, err := sh.CombinedOLog("ctr", "content", "get", indexDigest)
259+
if !strings.Contains(string(o), "not found") {
260+
t.Fatal("getting the SOCI index succeeded unexpectedly after GC")
261+
}
262+
if err == nil {
263+
t.Fatal("getting the SOCI index after GC did not return an error")
264+
}
265+
266+
}

soci/soci_index.go

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type Index struct {
111111
// IndexWithMetadata has a soci `Index` and its metadata.
112112
type IndexWithMetadata struct {
113113
Index *Index
114+
Desc ocispec.Descriptor
114115
Platform *ocispec.Platform
115116
ImageDigest digest.Digest
116117
CreatedAt time.Time
@@ -299,6 +300,7 @@ type BuildOption func(*buildConfig) error
299300
// buildConfig represents the config for a single index build operation.
300301
type buildConfig struct {
301302
platform ocispec.Platform
303+
gcRoot bool
302304
}
303305

304306
// WithPlatform sets the platform for a single build operation.
@@ -309,6 +311,22 @@ func WithPlatform(platform ocispec.Platform) BuildOption {
309311
}
310312
}
311313

314+
// WithNoGarbageCollectionLabel prevents the index builder from putting
315+
// a root GC label on the soci index. The builder will set content GC labels
316+
// to prevent the ztocs from being garbage collected.
317+
//
318+
// The caller is responsible for putting appropriate GC labels to prevent the
319+
// index from being garbage collected. The caller is also responsible for
320+
// ensuring that the SOCI index does not get garbage collected after the build finishes,
321+
// but before the GC label is applied. This can be done by calling `contentStore.BatchOpen`
322+
// before calling `Build`.
323+
func WithNoGarbageCollectionLabel() BuildOption {
324+
return func(bc *buildConfig) error {
325+
bc.gcRoot = false
326+
return nil
327+
}
328+
}
329+
312330
// IndexBuilder creates soci indices.
313331
type IndexBuilder struct {
314332
contentStore content.Store
@@ -349,6 +367,17 @@ func NewIndexBuilder(contentStore content.Store, blobStore store.Store, opts ...
349367
// Build builds a soci index for `img` and pushes it with its corresponding zTOCs to the blob store.
350368
// Returns the SOCI index and its metadata.
351369
func (b *IndexBuilder) Build(ctx context.Context, img images.Image, opts ...BuildOption) (*IndexWithMetadata, error) {
370+
buildCfg := buildConfig{
371+
platform: platforms.DefaultSpec(),
372+
gcRoot: true,
373+
}
374+
for _, opt := range opts {
375+
err := opt(&buildCfg)
376+
if err != nil {
377+
return nil, err
378+
}
379+
}
380+
352381
// batch will prevent content from being garbage collected in the middle of the following operations
353382
ctx, done, err := b.blobStore.BatchOpen(ctx)
354383
if err != nil {
@@ -357,13 +386,13 @@ func (b *IndexBuilder) Build(ctx context.Context, img images.Image, opts ...Buil
357386
defer done(ctx)
358387

359388
// Create and push zTOCs to blob store
360-
index, err := b.build(ctx, img, opts...)
389+
index, err := b.build(ctx, img, buildCfg)
361390
if err != nil {
362391
return nil, err
363392
}
364393

365394
// Label zTOCs and push SOCI index
366-
err = b.writeSociIndex(ctx, index)
395+
index.Desc, err = b.writeSociIndex(ctx, index, buildCfg.gcRoot)
367396
if err != nil {
368397
return nil, err
369398
}
@@ -374,17 +403,7 @@ func (b *IndexBuilder) Build(ctx context.Context, img images.Image, opts ...Buil
374403
// build attempts to create a zTOC in each layer and pushes the zTOC to the blob store.
375404
// It then creates the SOCI index and returns it with some metadata.
376405
// This should be done within a Batch and followed by writeSociIndex() to prevent garbage collection.
377-
func (b *IndexBuilder) build(ctx context.Context, img images.Image, opts ...BuildOption) (*IndexWithMetadata, error) {
378-
buildCfg := buildConfig{
379-
platform: platforms.DefaultSpec(),
380-
}
381-
for _, opt := range opts {
382-
err := opt(&buildCfg)
383-
if err != nil {
384-
return nil, err
385-
}
386-
}
387-
406+
func (b *IndexBuilder) build(ctx context.Context, img images.Image, buildCfg buildConfig) (*IndexWithMetadata, error) {
388407
platformMatcher := platforms.OnlyStrict(buildCfg.platform)
389408
// we get manifest descriptor before calling images.Manifest, since after calling
390409
// images.Manifest, images.Children will error out when reading the manifest blob (this happens on containerd side)
@@ -619,10 +638,10 @@ func GetImageManifestDescriptor(ctx context.Context, cs content.Store, imageTarg
619638

620639
// writeSociIndex writes the SociIndex manifest to the blob store.
621640
// This should be done within a Batch to prevent garbage collection.
622-
func (b *IndexBuilder) writeSociIndex(ctx context.Context, indexWithMetadata *IndexWithMetadata) error {
641+
func (b *IndexBuilder) writeSociIndex(ctx context.Context, indexWithMetadata *IndexWithMetadata, gcRoot bool) (ocispec.Descriptor, error) {
623642
manifest, err := MarshalIndex(indexWithMetadata.Index)
624643
if err != nil {
625-
return err
644+
return ocispec.Descriptor{}, err
626645
}
627646

628647
// If we're serializing the SOCI index as an OCI 1.0 Manifest, create an
@@ -631,7 +650,7 @@ func (b *IndexBuilder) writeSociIndex(ctx context.Context, indexWithMetadata *In
631650
if indexWithMetadata.Index.MediaType == ocispec.MediaTypeImageManifest {
632651
err = b.blobStore.Push(ctx, defaultConfigDescriptor, bytes.NewReader(defaultConfigContent))
633652
if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
634-
return fmt.Errorf("error creating OCI 1.0 empty config: %w", err)
653+
return ocispec.Descriptor{}, fmt.Errorf("error creating OCI 1.0 empty config: %w", err)
635654
}
636655
}
637656

@@ -644,18 +663,20 @@ func (b *IndexBuilder) writeSociIndex(ctx context.Context, indexWithMetadata *In
644663

645664
err = b.blobStore.Push(ctx, desc, bytes.NewReader(manifest))
646665
if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
647-
return fmt.Errorf("cannot write SOCI index to local store: %w", err)
666+
return ocispec.Descriptor{}, fmt.Errorf("cannot write SOCI index to local store: %w", err)
648667
}
649668

650669
log.G(ctx).WithField("digest", dgst.String()).Debugf("soci index has been written")
651670

652-
err = store.LabelGCRoot(ctx, b.blobStore, desc)
653-
if err != nil {
654-
return fmt.Errorf("cannot apply garbage collection label to index %s: %w", desc.Digest.String(), err)
671+
if gcRoot {
672+
err = store.LabelGCRoot(ctx, b.blobStore, desc)
673+
if err != nil {
674+
return ocispec.Descriptor{}, fmt.Errorf("cannot apply garbage collection label to index %s: %w", desc.Digest.String(), err)
675+
}
655676
}
656677
err = store.LabelGCRefContent(ctx, b.blobStore, desc, "config", defaultConfigDescriptor.Digest.String())
657678
if err != nil {
658-
return fmt.Errorf("cannot apply garbage collection label to index %s referencing default config: %w", desc.Digest.String(), err)
679+
return ocispec.Descriptor{}, fmt.Errorf("cannot apply garbage collection label to index %s referencing default config: %w", desc.Digest.String(), err)
659680
}
660681

661682
var allErr error
@@ -666,13 +687,13 @@ func (b *IndexBuilder) writeSociIndex(ctx context.Context, indexWithMetadata *In
666687
}
667688
}
668689
if allErr != nil {
669-
return fmt.Errorf("cannot apply one or more garbage collection labels to index %s: %w", desc.Digest.String(), allErr)
690+
return ocispec.Descriptor{}, fmt.Errorf("cannot apply one or more garbage collection labels to index %s: %w", desc.Digest.String(), allErr)
670691
}
671692

672693
refers := indexWithMetadata.Index.Subject
673694

674695
if refers == nil {
675-
return errors.New("cannot write soci index: the Refers field is nil")
696+
return ocispec.Descriptor{}, errors.New("cannot write soci index: the Refers field is nil")
676697
}
677698

678699
// this entry is persisted to be used by cli push
@@ -687,7 +708,7 @@ func (b *IndexBuilder) writeSociIndex(ctx context.Context, indexWithMetadata *In
687708
MediaType: indexWithMetadata.Index.MediaType,
688709
CreatedAt: indexWithMetadata.CreatedAt,
689710
}
690-
return b.config.artifactsDb.WriteArtifactEntry(entry)
711+
return desc, b.config.artifactsDb.WriteArtifactEntry(entry)
691712
}
692713

693714
func (b *IndexBuilder) maybeAddDisableXattrAnnotation(ztocDesc *ocispec.Descriptor, ztoc *ztoc.Ztoc) {

0 commit comments

Comments
 (0)