Skip to content

Commit 213681b

Browse files
committed
First step to implement full garbage collector for image layers
Refactored exiting logic on way that layers are first marked to be under removal so if actual removal fails they can be found from disk and cleaned up. Full garbage collector will be implemented as part of containerd migration. Signed-off-by: Olli Janatuinen <olli.janatuinen@gmail.com>
1 parent 71e0057 commit 213681b

6 files changed

Lines changed: 247 additions & 5 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// +build !windows
2+
3+
package image // import "github.com/docker/docker/integration/image"
4+
5+
import (
6+
"context"
7+
"io"
8+
"io/ioutil"
9+
"os"
10+
"strconv"
11+
"syscall"
12+
"testing"
13+
"unsafe"
14+
15+
"github.com/docker/docker/api/types"
16+
"github.com/docker/docker/internal/test/daemon"
17+
"github.com/docker/docker/internal/test/fakecontext"
18+
"gotest.tools/assert"
19+
"gotest.tools/skip"
20+
)
21+
22+
// This is a regression test for #38488
23+
// It ensures that orphan layers can be found and cleaned up
24+
// after unsuccessful image removal
25+
func TestRemoveImageGarbageCollector(t *testing.T) {
26+
// This test uses very platform specific way to prevent
27+
// daemon for remove image layer.
28+
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
29+
skip.If(t, os.Getenv("DOCKER_ENGINE_GOARCH") != "amd64")
30+
31+
// Create daemon with overlay2 graphdriver because vfs uses disk differently
32+
// and this test case would not work with it.
33+
d := daemon.New(t, daemon.WithStorageDriver("overlay2"), daemon.WithImageService)
34+
d.Start(t)
35+
defer d.Stop(t)
36+
37+
ctx := context.Background()
38+
client := d.NewClientT(t)
39+
i := d.ImageService()
40+
41+
img := "test-garbage-collector"
42+
43+
// Build a image with multiple layers
44+
dockerfile := `FROM busybox
45+
RUN echo echo Running... > /run.sh`
46+
source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
47+
defer source.Close()
48+
resp, err := client.ImageBuild(ctx,
49+
source.AsTarReader(t),
50+
types.ImageBuildOptions{
51+
Remove: true,
52+
ForceRemove: true,
53+
Tags: []string{img},
54+
})
55+
assert.NilError(t, err)
56+
_, err = io.Copy(ioutil.Discard, resp.Body)
57+
resp.Body.Close()
58+
assert.NilError(t, err)
59+
image, _, err := client.ImageInspectWithRaw(ctx, img)
60+
assert.NilError(t, err)
61+
62+
// Mark latest image layer to immutable
63+
data := image.GraphDriver.Data
64+
file, _ := os.Open(data["UpperDir"])
65+
attr := 0x00000010
66+
fsflags := uintptr(0x40086602)
67+
argp := uintptr(unsafe.Pointer(&attr))
68+
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), fsflags, argp)
69+
assert.Equal(t, "errno 0", errno.Error())
70+
71+
// Try to remove the image, it should generate error
72+
// but marking layer back to mutable before checking errors (so we don't break CI server)
73+
_, err = client.ImageRemove(ctx, img, types.ImageRemoveOptions{})
74+
attr = 0x00000000
75+
argp = uintptr(unsafe.Pointer(&attr))
76+
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), fsflags, argp)
77+
assert.Equal(t, "errno 0", errno.Error())
78+
assert.ErrorContains(t, err, "permission denied")
79+
80+
// Verify that layer remaining on disk
81+
dir, _ := os.Stat(data["UpperDir"])
82+
assert.Equal(t, "true", strconv.FormatBool(dir.IsDir()))
83+
84+
// Run imageService.Cleanup() and make sure that layer was removed from disk
85+
i.Cleanup()
86+
dir, err = os.Stat(data["UpperDir"])
87+
assert.ErrorContains(t, err, "no such file or directory")
88+
}

internal/test/daemon/daemon.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/docker/docker/api/types"
1717
"github.com/docker/docker/api/types/events"
1818
"github.com/docker/docker/client"
19+
"github.com/docker/docker/daemon/images"
1920
"github.com/docker/docker/internal/test"
2021
"github.com/docker/docker/internal/test/request"
2122
"github.com/docker/docker/opts"
@@ -71,6 +72,7 @@ type Daemon struct {
7172
init bool
7273
dockerdBinary string
7374
log logT
75+
imageService *images.ImageService
7476

7577
// swarm related field
7678
swarmListenAddr string
@@ -700,3 +702,8 @@ func cleanupRaftDir(t testingT, rootPath string) {
700702
t.Logf("error removing %v: %v", walDir, err)
701703
}
702704
}
705+
706+
// ImageService returns the Daemon's ImageService
707+
func (d *Daemon) ImageService() *images.ImageService {
708+
return d.imageService
709+
}

internal/test/daemon/ops.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,10 @@ func WithEnvironment(e environment.Execution) func(*Daemon) {
7070
}
7171
}
7272
}
73+
74+
// WithStorageDriver sets store driver option
75+
func WithStorageDriver(driver string) func(d *Daemon) {
76+
return func(d *Daemon) {
77+
d.storageDriver = driver
78+
}
79+
}

internal/test/daemon/ops_unix.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// +build !windows
2+
3+
package daemon
4+
5+
import (
6+
"path/filepath"
7+
"runtime"
8+
9+
"github.com/docker/docker/daemon/images"
10+
"github.com/docker/docker/layer"
11+
12+
// register graph drivers
13+
_ "github.com/docker/docker/daemon/graphdriver/register"
14+
"github.com/docker/docker/pkg/idtools"
15+
)
16+
17+
// WithImageService sets imageService options
18+
func WithImageService(d *Daemon) {
19+
layerStores := make(map[string]layer.Store)
20+
os := runtime.GOOS
21+
layerStores[os], _ = layer.NewStoreFromOptions(layer.StoreOptions{
22+
Root: d.Root,
23+
MetadataStorePathTemplate: filepath.Join(d.RootDir(), "image", "%s", "layerdb"),
24+
GraphDriver: d.storageDriver,
25+
GraphDriverOptions: nil,
26+
IDMapping: &idtools.IdentityMapping{},
27+
PluginGetter: nil,
28+
ExperimentalEnabled: false,
29+
OS: os,
30+
})
31+
d.imageService = images.NewImageService(images.ImageServiceConfig{
32+
LayerStores: layerStores,
33+
})
34+
}

layer/filestore.go

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,47 @@ func (fms *fileMetadataStore) GetMountParent(mount string) (ChainID, error) {
305305
return ChainID(dgst), nil
306306
}
307307

308+
func (fms *fileMetadataStore) getOrphan() ([]roLayer, error) {
309+
var orphanLayers []roLayer
310+
for _, algorithm := range supportedAlgorithms {
311+
fileInfos, err := ioutil.ReadDir(filepath.Join(fms.root, string(algorithm)))
312+
if err != nil {
313+
if os.IsNotExist(err) {
314+
continue
315+
}
316+
return nil, err
317+
}
318+
319+
for _, fi := range fileInfos {
320+
if fi.IsDir() && strings.Contains(fi.Name(), "-removing") {
321+
nameSplit := strings.Split(fi.Name(), "-")
322+
dgst := digest.NewDigestFromEncoded(algorithm, nameSplit[0])
323+
if err := dgst.Validate(); err != nil {
324+
logrus.Debugf("Ignoring invalid digest %s:%s", algorithm, nameSplit[0])
325+
} else {
326+
chainID := ChainID(dgst)
327+
chainFile := filepath.Join(fms.root, string(algorithm), fi.Name(), "cache-id")
328+
contentBytes, err := ioutil.ReadFile(chainFile)
329+
if err != nil {
330+
logrus.WithError(err).WithField("digest", dgst).Error("cannot get cache ID")
331+
}
332+
cacheID := strings.TrimSpace(string(contentBytes))
333+
if cacheID == "" {
334+
logrus.Errorf("invalid cache id value")
335+
}
336+
337+
l := &roLayer{
338+
chainID: chainID,
339+
cacheID: cacheID,
340+
}
341+
orphanLayers = append(orphanLayers, *l)
342+
}
343+
}
344+
}
345+
}
346+
return orphanLayers, nil
347+
}
348+
308349
func (fms *fileMetadataStore) List() ([]ChainID, []string, error) {
309350
var ids []ChainID
310351
for _, algorithm := range supportedAlgorithms {
@@ -346,8 +387,39 @@ func (fms *fileMetadataStore) List() ([]ChainID, []string, error) {
346387
return ids, mounts, nil
347388
}
348389

349-
func (fms *fileMetadataStore) Remove(layer ChainID) error {
350-
return os.RemoveAll(fms.getLayerDirectory(layer))
390+
// Remove layerdb folder if that is marked for removal
391+
func (fms *fileMetadataStore) Remove(layer ChainID, cache string) error {
392+
dgst := digest.Digest(layer)
393+
files, err := ioutil.ReadDir(filepath.Join(fms.root, string(dgst.Algorithm())))
394+
if err != nil {
395+
return err
396+
}
397+
for _, f := range files {
398+
if !strings.HasSuffix(f.Name(), "-removing") || !strings.HasPrefix(f.Name(), dgst.String()) {
399+
continue
400+
}
401+
402+
// Make sure that we only remove layerdb folder which points to
403+
// requested cacheID
404+
dir := filepath.Join(fms.root, string(dgst.Algorithm()), f.Name())
405+
chainFile := filepath.Join(dir, "cache-id")
406+
contentBytes, err := ioutil.ReadFile(chainFile)
407+
if err != nil {
408+
logrus.WithError(err).WithField("file", chainFile).Error("cannot get cache ID")
409+
continue
410+
}
411+
cacheID := strings.TrimSpace(string(contentBytes))
412+
if cacheID != cache {
413+
continue
414+
}
415+
logrus.Debugf("Removing folder: %s", dir)
416+
err = os.RemoveAll(dir)
417+
if err != nil && !os.IsNotExist(err) {
418+
logrus.WithError(err).WithField("name", f.Name()).Error("cannot remove layer")
419+
continue
420+
}
421+
}
422+
return nil
351423
}
352424

353425
func (fms *fileMetadataStore) RemoveMount(mount string) error {

layer/layer_store.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"io"
77
"io/ioutil"
8+
"os"
9+
"path/filepath"
810
"sync"
911

1012
"github.com/docker/distribution"
@@ -415,11 +417,24 @@ func (ls *layerStore) Map() map[ChainID]Layer {
415417
}
416418

417419
func (ls *layerStore) deleteLayer(layer *roLayer, metadata *Metadata) error {
420+
// Rename layer digest folder first so we detect orphan layer(s)
421+
// if ls.driver.Remove fails
422+
dir := ls.store.getLayerDirectory(layer.chainID)
423+
for {
424+
dgst := digest.Digest(layer.chainID)
425+
tmpID := fmt.Sprintf("%s-%s-removing", dgst.Hex(), stringid.GenerateRandomID())
426+
dir := filepath.Join(ls.store.root, string(dgst.Algorithm()), tmpID)
427+
err := os.Rename(ls.store.getLayerDirectory(layer.chainID), dir)
428+
if os.IsExist(err) {
429+
continue
430+
}
431+
break
432+
}
418433
err := ls.driver.Remove(layer.cacheID)
419434
if err != nil {
420435
return err
421436
}
422-
err = ls.store.Remove(layer.chainID)
437+
err = os.RemoveAll(dir)
423438
if err != nil {
424439
return err
425440
}
@@ -452,12 +467,14 @@ func (ls *layerStore) releaseLayer(l *roLayer) ([]Metadata, error) {
452467
if l.hasReferences() {
453468
panic("cannot delete referenced layer")
454469
}
470+
// Remove layer from layer map first so it is not considered to exist
471+
// when if ls.deleteLayer fails.
472+
delete(ls.layerMap, l.chainID)
473+
455474
var metadata Metadata
456475
if err := ls.deleteLayer(l, &metadata); err != nil {
457476
return nil, err
458477
}
459-
460-
delete(ls.layerMap, l.chainID)
461478
removed = append(removed, metadata)
462479

463480
if l.parent == nil {
@@ -743,6 +760,23 @@ func (ls *layerStore) assembleTarTo(graphID string, metadata io.ReadCloser, size
743760
}
744761

745762
func (ls *layerStore) Cleanup() error {
763+
orphanLayers, err := ls.store.getOrphan()
764+
if err != nil {
765+
logrus.Errorf("Cannot get orphan layers: %v", err)
766+
}
767+
logrus.Debugf("found %v orphan layers", len(orphanLayers))
768+
for _, orphan := range orphanLayers {
769+
logrus.Debugf("removing orphan layer, chain ID: %v , cache ID: %v", orphan.chainID, orphan.cacheID)
770+
err = ls.driver.Remove(orphan.cacheID)
771+
if err != nil && !os.IsNotExist(err) {
772+
logrus.WithError(err).WithField("cache-id", orphan.cacheID).Error("cannot remove orphan layer")
773+
continue
774+
}
775+
err = ls.store.Remove(orphan.chainID, orphan.cacheID)
776+
if err != nil {
777+
logrus.WithError(err).WithField("chain-id", orphan.chainID).Error("cannot remove orphan layer metadata")
778+
}
779+
}
746780
return ls.driver.Cleanup()
747781
}
748782

0 commit comments

Comments
 (0)