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>
This commit is contained in:
parent
bf672cccee
commit
03ab1b2cac
@ -312,6 +312,11 @@ type RemoteContext struct {
|
|||||||
// afterwards. Unpacking is required to run an image.
|
// afterwards. Unpacking is required to run an image.
|
||||||
Unpack bool
|
Unpack bool
|
||||||
|
|
||||||
|
// DiscardContent is a boolean flag to specify whether to allow GC to clean
|
||||||
|
// layers up from the content store after successfully unpacking these
|
||||||
|
// contents to the snapshotter.
|
||||||
|
DiscardContent bool
|
||||||
|
|
||||||
// UnpackOpts handles options to the unpack call.
|
// UnpackOpts handles options to the unpack call.
|
||||||
UnpackOpts []UnpackOpt
|
UnpackOpts []UnpackOpt
|
||||||
|
|
||||||
|
@ -132,6 +132,14 @@ func WithPullUnpack(_ *Client, c *RemoteContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDiscardContent is used to allow GC to clean layers up from
|
||||||
|
// the content store after successfully unpacking these contents to
|
||||||
|
// the snapshotter.
|
||||||
|
func WithDiscardContent(_ *Client, c *RemoteContext) error {
|
||||||
|
c.DiscardContent = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// WithUnpackOpts is used to add unpack options to the unpacker.
|
// WithUnpackOpts is used to add unpack options to the unpacker.
|
||||||
func WithUnpackOpts(opts []UnpackOpt) RemoteOpt {
|
func WithUnpackOpts(opts []UnpackOpt) RemoteOpt {
|
||||||
return func(_ *Client, c *RemoteContext) error {
|
return func(_ *Client, c *RemoteContext) error {
|
||||||
|
@ -29,13 +29,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containerd/containerd/defaults"
|
"github.com/containerd/containerd/defaults"
|
||||||
|
"github.com/containerd/containerd/errdefs"
|
||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
|
"github.com/containerd/containerd/leases"
|
||||||
"github.com/containerd/containerd/log"
|
"github.com/containerd/containerd/log"
|
||||||
"github.com/containerd/containerd/log/logtest"
|
"github.com/containerd/containerd/log/logtest"
|
||||||
"github.com/containerd/containerd/namespaces"
|
"github.com/containerd/containerd/namespaces"
|
||||||
"github.com/containerd/containerd/pkg/testutil"
|
"github.com/containerd/containerd/pkg/testutil"
|
||||||
"github.com/containerd/containerd/platforms"
|
"github.com/containerd/containerd/platforms"
|
||||||
"github.com/containerd/containerd/sys"
|
"github.com/containerd/containerd/sys"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
"github.com/opencontainers/image-spec/identity"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -215,6 +219,80 @@ func TestImagePull(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImagePullWithDiscardContent(t *testing.T) {
|
||||||
|
client, err := newClient(t, address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
ctx, cancel := testContext(t)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ls := client.LeasesService()
|
||||||
|
l, err := ls.Create(ctx, leases.WithRandomID(), leases.WithExpiration(24*time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx = leases.WithLease(ctx, l.ID)
|
||||||
|
img, err := client.Pull(ctx, testImage,
|
||||||
|
WithPlatformMatcher(platforms.Default()),
|
||||||
|
WithPullUnpack,
|
||||||
|
WithDiscardContent,
|
||||||
|
)
|
||||||
|
// Synchronously garbage collect contents
|
||||||
|
if errL := ls.Delete(ctx, l, leases.SynchronousDelete); errL != nil {
|
||||||
|
t.Fatal(errL)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all layer contents have been unpacked and aren't preserved
|
||||||
|
var (
|
||||||
|
diffIDs []digest.Digest
|
||||||
|
layers []digest.Digest
|
||||||
|
)
|
||||||
|
cs := client.ContentStore()
|
||||||
|
manifest, err := images.Manifest(ctx, cs, img.Target(), platforms.Default())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(manifest.Layers) == 0 {
|
||||||
|
t.Fatalf("failed to get children from %v", img.Target())
|
||||||
|
}
|
||||||
|
for _, l := range manifest.Layers {
|
||||||
|
layers = append(layers, l.Digest)
|
||||||
|
}
|
||||||
|
config, err := images.Config(ctx, cs, img.Target(), platforms.Default())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
diffIDs, err = images.RootFS(ctx, cs, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(layers) != len(diffIDs) {
|
||||||
|
t.Fatalf("number of layers and diffIDs don't match: %d != %d", len(layers), len(diffIDs))
|
||||||
|
} else if len(layers) == 0 {
|
||||||
|
t.Fatalf("there is no layers in the target image(parent: %v)", img.Target())
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
sn = client.SnapshotService("")
|
||||||
|
chain []digest.Digest
|
||||||
|
)
|
||||||
|
for i, dgst := range layers {
|
||||||
|
chain = append(chain, diffIDs[i])
|
||||||
|
chainID := identity.ChainID(chain).String()
|
||||||
|
if _, err := sn.Stat(ctx, chainID); err != nil {
|
||||||
|
t.Errorf("snapshot %v must exist: %v", chainID, err)
|
||||||
|
}
|
||||||
|
if _, err := cs.Info(ctx, dgst); err == nil || !errdefs.IsNotFound(err) {
|
||||||
|
t.Errorf("content %v must be garbage collected: %v", dgst, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestImagePullAllPlatforms(t *testing.T) {
|
func TestImagePullAllPlatforms(t *testing.T) {
|
||||||
client, err := newClient(t, address)
|
client, err := newClient(t, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
32
unpacker.go
32
unpacker.go
@ -22,6 +22,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@ -77,6 +78,7 @@ func (u *unpacker) unpack(
|
|||||||
rCtx *RemoteContext,
|
rCtx *RemoteContext,
|
||||||
h images.Handler,
|
h images.Handler,
|
||||||
config ocispec.Descriptor,
|
config ocispec.Descriptor,
|
||||||
|
parentDesc ocispec.Descriptor,
|
||||||
layers []ocispec.Descriptor,
|
layers []ocispec.Descriptor,
|
||||||
) error {
|
) error {
|
||||||
p, err := content.ReadBlob(ctx, u.c.ContentStore(), config)
|
p, err := content.ReadBlob(ctx, u.c.ContentStore(), config)
|
||||||
@ -245,6 +247,31 @@ EachLayer:
|
|||||||
"chainID": chainID,
|
"chainID": chainID,
|
||||||
}).Debug("image unpacked")
|
}).Debug("image unpacked")
|
||||||
|
|
||||||
|
if rCtx.DiscardContent {
|
||||||
|
// delete references to successfully unpacked layers
|
||||||
|
layersMap := map[string]struct{}{}
|
||||||
|
for _, desc := range layers {
|
||||||
|
layersMap[desc.Digest.String()] = struct{}{}
|
||||||
|
}
|
||||||
|
pinfo, err := cs.Info(ctx, parentDesc.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := []string{}
|
||||||
|
for k, v := range pinfo.Labels {
|
||||||
|
if strings.HasPrefix(k, "containerd.io/gc.ref.content.") {
|
||||||
|
if _, ok := layersMap[v]; ok {
|
||||||
|
// We've already unpacked this layer content
|
||||||
|
pinfo.Labels[k] = ""
|
||||||
|
fields = append(fields, "labels."+k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := cs.Update(ctx, pinfo, fields...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,6 +314,7 @@ func (u *unpacker) handlerWrapper(
|
|||||||
var (
|
var (
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
layers = map[digest.Digest][]ocispec.Descriptor{}
|
layers = map[digest.Digest][]ocispec.Descriptor{}
|
||||||
|
parent = map[digest.Digest]ocispec.Descriptor{}
|
||||||
)
|
)
|
||||||
return images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
return images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||||
children, err := f.Handle(ctx, desc)
|
children, err := f.Handle(ctx, desc)
|
||||||
@ -312,6 +340,7 @@ func (u *unpacker) handlerWrapper(
|
|||||||
lock.Lock()
|
lock.Lock()
|
||||||
for _, nl := range nonLayers {
|
for _, nl := range nonLayers {
|
||||||
layers[nl.Digest] = manifestLayers
|
layers[nl.Digest] = manifestLayers
|
||||||
|
parent[nl.Digest] = desc
|
||||||
}
|
}
|
||||||
lock.Unlock()
|
lock.Unlock()
|
||||||
|
|
||||||
@ -319,11 +348,12 @@ func (u *unpacker) handlerWrapper(
|
|||||||
case images.MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
|
case images.MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
l := layers[desc.Digest]
|
l := layers[desc.Digest]
|
||||||
|
p := parent[desc.Digest]
|
||||||
lock.Unlock()
|
lock.Unlock()
|
||||||
if len(l) > 0 {
|
if len(l) > 0 {
|
||||||
atomic.AddInt32(unpacks, 1)
|
atomic.AddInt32(unpacks, 1)
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
return u.unpack(uctx, rCtx, f, desc, l)
|
return u.unpack(uctx, rCtx, f, desc, p, l)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user