Skip to content

Commit 03ab1b2

Browse files
committed
Add config for allowing GC to clean unpacked layers up
This commit adds a flag through Pull API for allowing GC to clean layer contents up after unpacking these contents completed. This patch takes an approach to directly delete GC labels pointing to layers from the manifest blob. This will result in other snapshotters cannot reuse these contents on the next pull. But this patch mainly focuses on CRI use-cases where single snapshotter is usually used throughout the node lifecycle so this shouldn't be a matter. Signed-off-by: Kohei Tokunaga <ktokunaga.mail@gmail.com>
1 parent bf672cc commit 03ab1b2

File tree

4 files changed

+122
-1
lines changed

4 files changed

+122
-1
lines changed

client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ type RemoteContext struct {
312312
// afterwards. Unpacking is required to run an image.
313313
Unpack bool
314314

315+
// DiscardContent is a boolean flag to specify whether to allow GC to clean
316+
// layers up from the content store after successfully unpacking these
317+
// contents to the snapshotter.
318+
DiscardContent bool
319+
315320
// UnpackOpts handles options to the unpack call.
316321
UnpackOpts []UnpackOpt
317322

client_opts.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ func WithPullUnpack(_ *Client, c *RemoteContext) error {
132132
return nil
133133
}
134134

135+
// WithDiscardContent is used to allow GC to clean layers up from
136+
// the content store after successfully unpacking these contents to
137+
// the snapshotter.
138+
func WithDiscardContent(_ *Client, c *RemoteContext) error {
139+
c.DiscardContent = true
140+
return nil
141+
}
142+
135143
// WithUnpackOpts is used to add unpack options to the unpacker.
136144
func WithUnpackOpts(opts []UnpackOpt) RemoteOpt {
137145
return func(_ *Client, c *RemoteContext) error {

client_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,17 @@ import (
2929
"time"
3030

3131
"github.com/containerd/containerd/defaults"
32+
"github.com/containerd/containerd/errdefs"
3233
"github.com/containerd/containerd/images"
34+
"github.com/containerd/containerd/leases"
3335
"github.com/containerd/containerd/log"
3436
"github.com/containerd/containerd/log/logtest"
3537
"github.com/containerd/containerd/namespaces"
3638
"github.com/containerd/containerd/pkg/testutil"
3739
"github.com/containerd/containerd/platforms"
3840
"github.com/containerd/containerd/sys"
41+
"github.com/opencontainers/go-digest"
42+
"github.com/opencontainers/image-spec/identity"
3943
"github.com/sirupsen/logrus"
4044
)
4145

@@ -215,6 +219,80 @@ func TestImagePull(t *testing.T) {
215219
}
216220
}
217221

222+
func TestImagePullWithDiscardContent(t *testing.T) {
223+
client, err := newClient(t, address)
224+
if err != nil {
225+
t.Fatal(err)
226+
}
227+
defer client.Close()
228+
229+
ctx, cancel := testContext(t)
230+
defer cancel()
231+
232+
ls := client.LeasesService()
233+
l, err := ls.Create(ctx, leases.WithRandomID(), leases.WithExpiration(24*time.Hour))
234+
if err != nil {
235+
t.Fatal(err)
236+
}
237+
ctx = leases.WithLease(ctx, l.ID)
238+
img, err := client.Pull(ctx, testImage,
239+
WithPlatformMatcher(platforms.Default()),
240+
WithPullUnpack,
241+
WithDiscardContent,
242+
)
243+
// Synchronously garbage collect contents
244+
if errL := ls.Delete(ctx, l, leases.SynchronousDelete); errL != nil {
245+
t.Fatal(errL)
246+
}
247+
if err != nil {
248+
t.Fatal(err)
249+
}
250+
251+
// Check if all layer contents have been unpacked and aren't preserved
252+
var (
253+
diffIDs []digest.Digest
254+
layers []digest.Digest
255+
)
256+
cs := client.ContentStore()
257+
manifest, err := images.Manifest(ctx, cs, img.Target(), platforms.Default())
258+
if err != nil {
259+
t.Fatal(err)
260+
}
261+
if len(manifest.Layers) == 0 {
262+
t.Fatalf("failed to get children from %v", img.Target())
263+
}
264+
for _, l := range manifest.Layers {
265+
layers = append(layers, l.Digest)
266+
}
267+
config, err := images.Config(ctx, cs, img.Target(), platforms.Default())
268+
if err != nil {
269+
t.Fatal(err)
270+
}
271+
diffIDs, err = images.RootFS(ctx, cs, config)
272+
if err != nil {
273+
t.Fatal(err)
274+
}
275+
if len(layers) != len(diffIDs) {
276+
t.Fatalf("number of layers and diffIDs don't match: %d != %d", len(layers), len(diffIDs))
277+
} else if len(layers) == 0 {
278+
t.Fatalf("there is no layers in the target image(parent: %v)", img.Target())
279+
}
280+
var (
281+
sn = client.SnapshotService("")
282+
chain []digest.Digest
283+
)
284+
for i, dgst := range layers {
285+
chain = append(chain, diffIDs[i])
286+
chainID := identity.ChainID(chain).String()
287+
if _, err := sn.Stat(ctx, chainID); err != nil {
288+
t.Errorf("snapshot %v must exist: %v", chainID, err)
289+
}
290+
if _, err := cs.Info(ctx, dgst); err == nil || !errdefs.IsNotFound(err) {
291+
t.Errorf("content %v must be garbage collected: %v", dgst, err)
292+
}
293+
}
294+
}
295+
218296
func TestImagePullAllPlatforms(t *testing.T) {
219297
client, err := newClient(t, address)
220298
if err != nil {

unpacker.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"encoding/json"
2323
"fmt"
2424
"math/rand"
25+
"strings"
2526
"sync"
2627
"sync/atomic"
2728
"time"
@@ -77,6 +78,7 @@ func (u *unpacker) unpack(
7778
rCtx *RemoteContext,
7879
h images.Handler,
7980
config ocispec.Descriptor,
81+
parentDesc ocispec.Descriptor,
8082
layers []ocispec.Descriptor,
8183
) error {
8284
p, err := content.ReadBlob(ctx, u.c.ContentStore(), config)
@@ -245,6 +247,31 @@ EachLayer:
245247
"chainID": chainID,
246248
}).Debug("image unpacked")
247249

250+
if rCtx.DiscardContent {
251+
// delete references to successfully unpacked layers
252+
layersMap := map[string]struct{}{}
253+
for _, desc := range layers {
254+
layersMap[desc.Digest.String()] = struct{}{}
255+
}
256+
pinfo, err := cs.Info(ctx, parentDesc.Digest)
257+
if err != nil {
258+
return err
259+
}
260+
fields := []string{}
261+
for k, v := range pinfo.Labels {
262+
if strings.HasPrefix(k, "containerd.io/gc.ref.content.") {
263+
if _, ok := layersMap[v]; ok {
264+
// We've already unpacked this layer content
265+
pinfo.Labels[k] = ""
266+
fields = append(fields, "labels."+k)
267+
}
268+
}
269+
}
270+
if _, err := cs.Update(ctx, pinfo, fields...); err != nil {
271+
return err
272+
}
273+
}
274+
248275
return nil
249276
}
250277

@@ -287,6 +314,7 @@ func (u *unpacker) handlerWrapper(
287314
var (
288315
lock sync.Mutex
289316
layers = map[digest.Digest][]ocispec.Descriptor{}
317+
parent = map[digest.Digest]ocispec.Descriptor{}
290318
)
291319
return images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
292320
children, err := f.Handle(ctx, desc)
@@ -312,18 +340,20 @@ func (u *unpacker) handlerWrapper(
312340
lock.Lock()
313341
for _, nl := range nonLayers {
314342
layers[nl.Digest] = manifestLayers
343+
parent[nl.Digest] = desc
315344
}
316345
lock.Unlock()
317346

318347
children = nonLayers
319348
case images.MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
320349
lock.Lock()
321350
l := layers[desc.Digest]
351+
p := parent[desc.Digest]
322352
lock.Unlock()
323353
if len(l) > 0 {
324354
atomic.AddInt32(unpacks, 1)
325355
eg.Go(func() error {
326-
return u.unpack(uctx, rCtx, f, desc, l)
356+
return u.unpack(uctx, rCtx, f, desc, p, l)
327357
})
328358
}
329359
}

0 commit comments

Comments
 (0)