From f16e34d01f9dcc7704b06ace32d4b0783f60b4e4 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Fri, 11 Nov 2022 14:45:40 +0100 Subject: [PATCH] Add Store.GarbageCollect() method This looks in the container store for existing data dirs with ids not in the container files and removes them. It also adds an (optional) driver method to list available layers, then uses this and compares it to the layers json file and removes layers that are not references. Losing track of containers and layers can potentially happen in the case of some kind of unclean shutdown, but mainly it happens at reboot when using transient storage mode. Such users are recommended to run a garbage collect at boot. Signed-off-by: Alexander Larsson --- containers.go | 36 ++++++++++++++++++++++++++++++++++++ drivers/aufs/aufs.go | 5 +++++ drivers/btrfs/btrfs.go | 5 +++++ drivers/devmapper/driver.go | 5 +++++ drivers/driver.go | 3 +++ drivers/overlay/overlay.go | 35 +++++++++++++++++++++++++++++++++++ drivers/vfs/driver.go | 35 +++++++++++++++++++++++++++++++++++ drivers/windows/windows.go | 5 +++++ drivers/zfs/zfs.go | 5 +++++ layers.go | 30 ++++++++++++++++++++++++++++++ store.go | 22 ++++++++++++++++++++++ utils.go | 13 +++++++++++++ 12 files changed, 199 insertions(+) diff --git a/containers.go b/containers.go index b5a08eb2e3..512585d58e 100644 --- a/containers.go +++ b/containers.go @@ -134,6 +134,9 @@ type rwContainerStore interface { // Containers returns a slice enumerating the known containers. Containers() ([]Container, error) + + // Clean up unreferenced datadirs + GarbageCollect() error } type containerStore struct { @@ -317,6 +320,39 @@ func (r *containerStore) Containers() ([]Container, error) { return containers, nil } +// This looks for datadirs in the store directory that are not referenced +// by the json file and removes it. These can happen in the case of unclean +// shutdowns or regular restarts in transient store mode. +func (r *containerStore) GarbageCollect() error { + entries, err := os.ReadDir(r.dir) + if err != nil { + // Unexpected, don't try any GC + return err + } + + for _, entry := range entries { + id := entry.Name() + // Does it look like a datadir directory? + if !entry.IsDir() || !nameLooksLikeID(id) { + continue + } + + // Should the id be there? + if r.byid[id] != nil { + continue + } + + // Otherwise remove datadir + moreErr := os.RemoveAll(filepath.Join(r.dir, id)) + // Propagate first error + if moreErr != nil && err == nil { + err = moreErr + } + } + + return err +} + func (r *containerStore) datadir(id string) string { return filepath.Join(r.dir, id) } diff --git a/drivers/aufs/aufs.go b/drivers/aufs/aufs.go index e384750237..10341d41ac 100644 --- a/drivers/aufs/aufs.go +++ b/drivers/aufs/aufs.go @@ -251,6 +251,11 @@ func (a *Driver) Exists(id string) bool { return true } +// List layers (not including additional image stores) +func (a *Driver) ListLayers() ([]string, error) { + return nil, graphdriver.ErrNotSupported +} + // AdditionalImageStores returns additional image stores supported by the driver func (a *Driver) AdditionalImageStores() []string { return nil diff --git a/drivers/btrfs/btrfs.go b/drivers/btrfs/btrfs.go index 1d6d466628..e8b16b858e 100644 --- a/drivers/btrfs/btrfs.go +++ b/drivers/btrfs/btrfs.go @@ -676,6 +676,11 @@ func (d *Driver) Exists(id string) bool { return err == nil } +// List layers (not including additional image stores) +func (d *Driver) ListLayers() ([]string, error) { + return nil, graphdriver.ErrNotSupported +} + // AdditionalImageStores returns additional image stores supported by the driver func (d *Driver) AdditionalImageStores() []string { return nil diff --git a/drivers/devmapper/driver.go b/drivers/devmapper/driver.go index 27a58a9eab..8b3ee51df7 100644 --- a/drivers/devmapper/driver.go +++ b/drivers/devmapper/driver.go @@ -267,6 +267,11 @@ func (d *Driver) Exists(id string) bool { return d.DeviceSet.HasDevice(id) } +// List layers (not including additional image stores) +func (d *Driver) ListLayers() ([]string, error) { + return nil, graphdriver.ErrNotSupported +} + // AdditionalImageStores returns additional image stores supported by the driver func (d *Driver) AdditionalImageStores() []string { return nil diff --git a/drivers/driver.go b/drivers/driver.go index e47d50a71c..1c1f017432 100644 --- a/drivers/driver.go +++ b/drivers/driver.go @@ -109,6 +109,9 @@ type ProtoDriver interface { // Exists returns whether a filesystem layer with the specified // ID exists on this driver. Exists(id string) bool + // Returns a list of layer ids that exist on this driver (does not include + // additional storage layers). Not supported by all backends. + ListLayers() ([]string, error) // Status returns a set of key-value pairs which give low // level diagnostic status about this driver. Status() [][2]string diff --git a/drivers/overlay/overlay.go b/drivers/overlay/overlay.go index d08e82dff0..49da0679a2 100644 --- a/drivers/overlay/overlay.go +++ b/drivers/overlay/overlay.go @@ -17,6 +17,7 @@ import ( "strings" "sync" "syscall" + "unicode" graphdriver "github.com/containers/storage/drivers" "github.com/containers/storage/drivers/overlayutils" @@ -1697,6 +1698,40 @@ func (d *Driver) Exists(id string) bool { return err == nil } +func nameLooksLikeID(name string) bool { + if len(name) != 64 { + return false + } + for _, c := range name { + if !unicode.Is(unicode.ASCII_Hex_Digit, c) { + return false + } + } + return true +} + +// List layers (not including additional image stores) +func (d *Driver) ListLayers() ([]string, error) { + entries, err := os.ReadDir(d.home) + if err != nil { + return nil, err + } + + layers := make([]string, 0) + + for _, entry := range entries { + id := entry.Name() + // Does it look like a datadir directory? + if !entry.IsDir() || !nameLooksLikeID(id) { + continue + } + + layers = append(layers, id) + } + + return layers, err +} + // isParent returns if the passed in parent is the direct parent of the passed in layer func (d *Driver) isParent(id, parent string) bool { lowers, err := d.getLowerDirs(id) diff --git a/drivers/vfs/driver.go b/drivers/vfs/driver.go index 50b4b28178..9deaa7c3a8 100644 --- a/drivers/vfs/driver.go +++ b/drivers/vfs/driver.go @@ -8,6 +8,7 @@ import ( "runtime" "strconv" "strings" + "unicode" graphdriver "github.com/containers/storage/drivers" "github.com/containers/storage/pkg/archive" @@ -265,6 +266,40 @@ func (d *Driver) Exists(id string) bool { return err == nil } +func nameLooksLikeID(name string) bool { + if len(name) != 64 { + return false + } + for _, c := range name { + if !unicode.Is(unicode.ASCII_Hex_Digit, c) { + return false + } + } + return true +} + +// List layers (not including additional image stores) +func (d *Driver) ListLayers() ([]string, error) { + entries, err := os.ReadDir(d.homes[0]) + if err != nil { + return nil, err + } + + layers := make([]string, 0) + + for _, entry := range entries { + id := entry.Name() + // Does it look like a datadir directory? + if !entry.IsDir() || !nameLooksLikeID(id) { + continue + } + + layers = append(layers, id) + } + + return layers, err +} + // AdditionalImageStores returns additional image stores supported by the driver func (d *Driver) AdditionalImageStores() []string { if len(d.homes) > 1 { diff --git a/drivers/windows/windows.go b/drivers/windows/windows.go index 4cd9fa7244..66aa460cf8 100644 --- a/drivers/windows/windows.go +++ b/drivers/windows/windows.go @@ -185,6 +185,11 @@ func (d *Driver) Exists(id string) bool { return result } +// List layers (not including additional image stores) +func (d *Driver) ListLayers() ([]string, error) { + return nil, graphdriver.ErrNotSupported +} + // CreateFromTemplate creates a layer with the same contents and parent as another layer. func (d *Driver) CreateFromTemplate(id, template string, templateIDMappings *idtools.IDMappings, parent string, parentIDMappings *idtools.IDMappings, opts *graphdriver.CreateOpts, readWrite bool) error { return graphdriver.NaiveCreateFromTemplate(d, id, template, templateIDMappings, parent, parentIDMappings, opts, readWrite) diff --git a/drivers/zfs/zfs.go b/drivers/zfs/zfs.go index d146782404..66da588b09 100644 --- a/drivers/zfs/zfs.go +++ b/drivers/zfs/zfs.go @@ -506,6 +506,11 @@ func (d *Driver) Exists(id string) bool { return d.filesystemsCache[d.zfsPath(id)] } +// List layers (not including additional image stores) +func (d *Driver) ListLayers() ([]string, error) { + return nil, graphdriver.ErrNotSupported +} + // AdditionalImageStores returns additional image stores supported by the driver func (d *Driver) AdditionalImageStores() []string { return nil diff --git a/layers.go b/layers.go index 945581acc8..3b580e2009 100644 --- a/layers.go +++ b/layers.go @@ -295,6 +295,9 @@ type rwLayerStore interface { // store. // This API is experimental and can be changed without bumping the major version number. PutAdditionalLayer(id string, parentLayer *Layer, names []string, aLayer drivers.AdditionalLayer) (layer *Layer, err error) + + // Clean up unreferenced layers + GarbageCollect() error } type layerStore struct { @@ -512,6 +515,33 @@ func (r *layerStore) Layers() ([]Layer, error) { return layers, nil } +func (r *layerStore) GarbageCollect() error { + layers, err := r.driver.ListLayers() + + if err != nil { + if errors.Is(err, drivers.ErrNotSupported) { + return nil + } + return err + } + + for _, id := range layers { + // Is the id still referenced + if r.byid[id] != nil { + continue + } + + // Remove layer and any related data of unreferenced id + if err := r.driver.Remove(id); err != nil { + return err + } + + os.Remove(r.tspath(id)) + os.RemoveAll(r.datadir(id)) + } + return nil +} + func (r *layerStore) mountspath() string { return filepath.Join(r.rundir, "mountpoints.json") } diff --git a/store.go b/store.go index 25a2f63a6b..f38206fa7f 100644 --- a/store.go +++ b/store.go @@ -503,6 +503,11 @@ type Store interface { // Releasing AdditionalLayer handler is caller's responsibility. // This API is experimental and can be changed without bumping the major version number. LookupAdditionalLayer(d digest.Digest, imageref string) (AdditionalLayer, error) + + // Tries to clean up remainders of previous containers or layers that are not + // references in the json files. These can happen in the case of unclean + // shutdowns or regular restarts in transient store mode. + GarbageCollect() error } // AdditionalLayer reprents a layer that is contained in the additional layer store @@ -3385,3 +3390,20 @@ func (s *store) Free() { } } } + +// Tries to clean up old unreferenced container leftovers. returns the first error +// but continues as far as it can +func (s *store) GarbageCollect() error { + firstErr := s.writeToContainerStore(func(rcstore rwContainerStore) error { + return rcstore.GarbageCollect() + }) + + moreErr := s.writeToLayerStore(func(rlstore rwLayerStore) error { + return rlstore.GarbageCollect() + }) + if firstErr == nil { + firstErr = moreErr + } + + return firstErr +} diff --git a/utils.go b/utils.go index ae9600e687..7f9e92b930 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,7 @@ package storage import ( "fmt" + "unicode" "github.com/containers/storage/types" ) @@ -72,3 +73,15 @@ func applyNameOperation(oldNames []string, opParameters []string, op updateNameO } return dedupeNames(result), nil } + +func nameLooksLikeID(name string) bool { + if len(name) != 64 { + return false + } + for _, c := range name { + if !unicode.Is(unicode.ASCII_Hex_Digit, c) { + return false + } + } + return true +}