diff --git a/contrib/check-config.sh b/contrib/check-config.sh index 88f045b556..b3ddf67e45 100755 --- a/contrib/check-config.sh +++ b/contrib/check-config.sh @@ -368,14 +368,6 @@ EXITCODE=0 STORAGE=1 echo '- Storage Drivers:' -echo " - \"$(wrap_color 'aufs' blue)\":" -check_flags AUFS_FS | sed 's/^/ /' -if ! is_set AUFS_FS && grep -q aufs /proc/filesystems; then - echo " $(wrap_color '(note that some kernels include AUFS patches but not the AUFS_FS flag)' bold black)" -fi -[ "$EXITCODE" = 0 ] && STORAGE=0 -EXITCODE=0 - echo " - \"$(wrap_color 'btrfs' blue)\":" check_flags BTRFS_FS | sed 's/^/ /' check_flags BTRFS_FS_POSIX_ACL | sed 's/^/ /' diff --git a/daemon/daemon_linux.go b/daemon/daemon_linux.go index a8dabb4149..afaeabd4f3 100644 --- a/daemon/daemon_linux.go +++ b/daemon/daemon_linux.go @@ -116,7 +116,7 @@ func getCleanPatterns(id string) (regexps []*regexp.Regexp) { id = "[0-9a-f]{64}" patterns = append(patterns, "containers/"+id+"/mounts/shm", "containers/"+id+"/shm") } - patterns = append(patterns, "overlay2/"+id+"/merged$", "aufs/mnt/"+id+"$", "overlay/"+id+"/merged$", "zfs/graph/"+id+"$") + patterns = append(patterns, "overlay2/"+id+"/merged$", "overlay/"+id+"/merged$", "zfs/graph/"+id+"$") for _, p := range patterns { r, err := regexp.Compile(p) if err == nil { diff --git a/daemon/daemon_linux_test.go b/daemon/daemon_linux_test.go index af51a49ee2..cd5f3804d6 100644 --- a/daemon/daemon_linux_test.go +++ b/daemon/daemon_linux_test.go @@ -145,22 +145,6 @@ func TestCleanupMountsByID(t *testing.T) { d := &Daemon{ root: "/var/lib/docker/", } - - t.Run("aufs", func(t *testing.T) { - expected := "/var/lib/docker/aufs/mnt/03ca4b49e71f1e49a41108829f4d5c70ac95934526e2af8984a1f65f1de0715d" - var unmounted int - unmount := func(target string) error { - if target == expected { - unmounted++ - } - return nil - } - - err := d.cleanupMountsFromReaderByID(strings.NewReader(mountsFixture), "03ca4b49e71f1e49a41108829f4d5c70ac95934526e2af8984a1f65f1de0715d", unmount) - assert.NilError(t, err) - assert.Equal(t, unmounted, 1, "Expected to unmount the root (and that only)") - }) - t.Run("overlay2", func(t *testing.T) { expected := "/var/lib/docker/overlay2/3a4b807fcb98c208573f368c5654a6568545a7f92404a07d0045eb5c85acaf67/merged" var unmounted int diff --git a/daemon/graphdriver/aufs/aufs.go b/daemon/graphdriver/aufs/aufs.go deleted file mode 100644 index efd70da7f8..0000000000 --- a/daemon/graphdriver/aufs/aufs.go +++ /dev/null @@ -1,649 +0,0 @@ -//go:build linux -// +build linux - -/* - -aufs driver directory structure - - . - ├── layers // Metadata of layers - │ ├── 1 - │ ├── 2 - │ └── 3 - ├── diff // Content of the layer - │ ├── 1 // Contains layers that need to be mounted for the id - │ ├── 2 - │ └── 3 - └── mnt // Mount points for the rw layers to be mounted - ├── 1 - ├── 2 - └── 3 - -*/ - -package aufs // import "github.com/docker/docker/daemon/graphdriver/aufs" - -import ( - "bufio" - "context" - "fmt" - "io" - "os" - "os/exec" - "path" - "path/filepath" - "strconv" - "strings" - "sync" - - "github.com/containerd/containerd/pkg/userns" - "github.com/docker/docker/daemon/graphdriver" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/chrootarchive" - "github.com/docker/docker/pkg/containerfs" - "github.com/docker/docker/pkg/directory" - "github.com/docker/docker/pkg/idtools" - "github.com/moby/locker" - "github.com/moby/sys/mount" - "github.com/opencontainers/selinux/go-selinux/label" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/vbatts/tar-split/tar/storage" - "golang.org/x/sys/unix" -) - -var ( - // ErrAufsNotSupported is returned if aufs is not supported by the host. - ErrAufsNotSupported = fmt.Errorf("AUFS was not found in /proc/filesystems") - // ErrAufsNested means aufs cannot be used bc we are in a user namespace - ErrAufsNested = fmt.Errorf("AUFS cannot be used in non-init user namespace") - backingFs = "" - - enableDirpermLock sync.Once - enableDirperm bool - - logger = logrus.WithField("storage-driver", "aufs") -) - -func init() { - graphdriver.Register("aufs", Init) -} - -// Driver contains information about the filesystem mounted. -type Driver struct { - root string - idMap idtools.IdentityMapping - ctr *graphdriver.RefCounter - pathCacheLock sync.Mutex - pathCache map[string]string - naiveDiff graphdriver.DiffDriver - locker *locker.Locker - mntL sync.Mutex -} - -// Init returns a new AUFS driver. -// An error is returned if AUFS is not supported. -func Init(root string, options []string, idMap idtools.IdentityMapping) (graphdriver.Driver, error) { - // Try to load the aufs kernel module - if err := supportsAufs(); err != nil { - logger.Error(err) - return nil, graphdriver.ErrNotSupported - } - - // Perform feature detection on /var/lib/docker/aufs if it's an existing directory. - // This covers situations where /var/lib/docker/aufs is a mount, and on a different - // filesystem than /var/lib/docker. - // If the path does not exist, fall back to using /var/lib/docker for feature detection. - testdir := root - if _, err := os.Stat(testdir); os.IsNotExist(err) { - testdir = filepath.Dir(testdir) - } - - fsMagic, err := graphdriver.GetFSMagic(testdir) - if err != nil { - return nil, err - } - if fsName, ok := graphdriver.FsNames[fsMagic]; ok { - backingFs = fsName - } - - switch fsMagic { - case graphdriver.FsMagicAufs, graphdriver.FsMagicBtrfs, graphdriver.FsMagicEcryptfs: - logger.Errorf("AUFS is not supported over %s", backingFs) - return nil, graphdriver.ErrIncompatibleFS - } - - paths := []string{ - "mnt", - "diff", - "layers", - } - - a := &Driver{ - root: root, - idMap: idMap, - pathCache: make(map[string]string), - ctr: graphdriver.NewRefCounter(graphdriver.NewFsChecker(graphdriver.FsMagicAufs)), - locker: locker.New(), - } - - currentID := idtools.CurrentIdentity() - dirID := idtools.Identity{ - UID: currentID.UID, - GID: a.idMap.RootPair().GID, - } - - // Create the root aufs driver dir - if err := idtools.MkdirAllAndChown(root, 0710, dirID); err != nil { - return nil, err - } - - // Populate the dir structure - for _, p := range paths { - if err := idtools.MkdirAllAndChown(path.Join(root, p), 0710, dirID); err != nil { - return nil, err - } - } - - for _, path := range []string{"mnt", "diff"} { - p := filepath.Join(root, path) - entries, err := os.ReadDir(p) - if err != nil { - logger.WithError(err).WithField("dir", p).Error("error reading dir entries") - continue - } - for _, entry := range entries { - if !entry.IsDir() { - continue - } - if strings.HasSuffix(entry.Name(), "-removing") { - logger.WithField("dir", entry.Name()).Debug("Cleaning up stale layer dir") - if err := containerfs.EnsureRemoveAll(filepath.Join(p, entry.Name())); err != nil { - logger.WithField("dir", entry.Name()).WithError(err).Error("Error removing stale layer dir") - } - } - } - } - - a.naiveDiff = graphdriver.NewNaiveDiffDriver(a, a.idMap) - return a, nil -} - -// Return a nil error if the kernel supports aufs -// We cannot modprobe because inside dind modprobe fails -// to run -func supportsAufs() error { - // We can try to modprobe aufs first before looking at - // proc/filesystems for when aufs is supported - exec.Command("modprobe", "aufs").Run() - - if userns.RunningInUserNS() { - return ErrAufsNested - } - - f, err := os.Open("/proc/filesystems") - if err != nil { - return err - } - defer f.Close() - - s := bufio.NewScanner(f) - for s.Scan() { - if strings.Contains(s.Text(), "aufs") { - return nil - } - } - return ErrAufsNotSupported -} - -func (a *Driver) rootPath() string { - return a.root -} - -func (*Driver) String() string { - return "aufs" -} - -// Status returns current information about the filesystem such as root directory, number of directories mounted, etc. -func (a *Driver) Status() [][2]string { - ids, _ := loadIds(path.Join(a.rootPath(), "layers")) - return [][2]string{ - {"Root Dir", a.rootPath()}, - {"Backing Filesystem", backingFs}, - {"Dirs", strconv.Itoa(len(ids))}, - {"Dirperm1 Supported", strconv.FormatBool(useDirperm())}, - } -} - -// GetMetadata not implemented -func (a *Driver) GetMetadata(id string) (map[string]string, error) { - return nil, nil -} - -// Exists returns true if the given id is registered with -// this driver -func (a *Driver) Exists(id string) bool { - if _, err := os.Lstat(path.Join(a.rootPath(), "layers", id)); err != nil { - return false - } - return true -} - -// CreateReadWrite creates a layer that is writable for use as a container -// file system. -func (a *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error { - return a.Create(id, parent, opts) -} - -// Create three folders for each id -// mnt, layers, and diff -func (a *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error { - if opts != nil && len(opts.StorageOpt) != 0 { - return fmt.Errorf("--storage-opt is not supported for aufs") - } - - if err := a.createDirsFor(id); err != nil { - return err - } - // Write the layers metadata - f, err := os.Create(path.Join(a.rootPath(), "layers", id)) - if err != nil { - return err - } - defer f.Close() - - if parent != "" { - ids, err := getParentIDs(a.rootPath(), parent) - if err != nil { - return err - } - - if _, err := fmt.Fprintln(f, parent); err != nil { - return err - } - for _, i := range ids { - if _, err := fmt.Fprintln(f, i); err != nil { - return err - } - } - } - - return nil -} - -// createDirsFor creates two directories for the given id. -// mnt and diff -func (a *Driver) createDirsFor(id string) error { - paths := []string{ - "mnt", - "diff", - } - - // Directory permission is 0755. - // The path of directories are /mnt/ - // and /diff/ - for _, p := range paths { - if err := idtools.MkdirAllAndChown(path.Join(a.rootPath(), p, id), 0755, a.idMap.RootPair()); err != nil { - return err - } - } - return nil -} - -// Remove will unmount and remove the given id. -func (a *Driver) Remove(id string) error { - a.locker.Lock(id) - defer a.locker.Unlock(id) - a.pathCacheLock.Lock() - mountpoint, exists := a.pathCache[id] - a.pathCacheLock.Unlock() - if !exists { - mountpoint = a.getMountpoint(id) - } - - if err := a.unmount(mountpoint); err != nil { - logger.WithError(err).WithField("method", "Remove()").Warn() - return err - } - - // Remove the layers file for the id - if err := os.Remove(path.Join(a.rootPath(), "layers", id)); err != nil && !os.IsNotExist(err) { - return errors.Wrapf(err, "error removing layers dir for %s", id) - } - - if err := atomicRemove(a.getDiffPath(id)); err != nil { - return errors.Wrapf(err, "could not remove diff path for id %s", id) - } - - // Atomically remove each directory in turn by first moving it out of the - // way (so that docker doesn't find it anymore) before doing removal of - // the whole tree. - if err := atomicRemove(mountpoint); err != nil { - if errors.Is(err, unix.EBUSY) { - logger.WithField("dir", mountpoint).WithError(err).Warn("error performing atomic remove due to EBUSY") - } - return errors.Wrapf(err, "could not remove mountpoint for id %s", id) - } - - a.pathCacheLock.Lock() - delete(a.pathCache, id) - a.pathCacheLock.Unlock() - return nil -} - -func atomicRemove(source string) error { - target := source + "-removing" - - err := os.Rename(source, target) - switch { - case err == nil, os.IsNotExist(err): - case os.IsExist(err): - // Got error saying the target dir already exists, maybe the source doesn't exist due to a previous (failed) remove - if _, e := os.Stat(source); !os.IsNotExist(e) { - return errors.Wrapf(err, "target rename dir %q exists but should not, this needs to be manually cleaned up", target) - } - default: - return errors.Wrapf(err, "error preparing atomic delete") - } - - return containerfs.EnsureRemoveAll(target) -} - -// Get returns the rootfs path for the id. -// This will mount the dir at its given path -func (a *Driver) Get(id, mountLabel string) (string, error) { - a.locker.Lock(id) - defer a.locker.Unlock(id) - parents, err := a.getParentLayerPaths(id) - if err != nil && !os.IsNotExist(err) { - return "", err - } - - a.pathCacheLock.Lock() - m, exists := a.pathCache[id] - a.pathCacheLock.Unlock() - - if !exists { - m = a.getDiffPath(id) - if len(parents) > 0 { - m = a.getMountpoint(id) - } - } - if count := a.ctr.Increment(m); count > 1 { - return m, nil - } - - // If a dir does not have a parent ( no layers )do not try to mount - // just return the diff path to the data - if len(parents) > 0 { - if err := a.mount(id, m, mountLabel, parents); err != nil { - return "", err - } - } - - a.pathCacheLock.Lock() - a.pathCache[id] = m - a.pathCacheLock.Unlock() - return m, nil -} - -// Put unmounts and updates list of active mounts. -func (a *Driver) Put(id string) error { - a.locker.Lock(id) - defer a.locker.Unlock(id) - a.pathCacheLock.Lock() - m, exists := a.pathCache[id] - if !exists { - m = a.getMountpoint(id) - a.pathCache[id] = m - } - a.pathCacheLock.Unlock() - if count := a.ctr.Decrement(m); count > 0 { - return nil - } - - err := a.unmount(m) - if err != nil { - logger.WithError(err).WithField("method", "Put()").Warn() - } - return err -} - -// isParent returns if the passed in parent is the direct parent of the passed in layer -func (a *Driver) isParent(id, parent string) bool { - parents, _ := getParentIDs(a.rootPath(), id) - if parent == "" && len(parents) > 0 { - return false - } - return !(len(parents) > 0 && parent != parents[0]) -} - -// Diff produces an archive of the changes between the specified -// layer and its parent layer which may be "". -func (a *Driver) Diff(id, parent string) (io.ReadCloser, error) { - if !a.isParent(id, parent) { - return a.naiveDiff.Diff(id, parent) - } - - // AUFS doesn't need the parent layer to produce a diff. - return archive.TarWithOptions(path.Join(a.rootPath(), "diff", id), &archive.TarOptions{ - Compression: archive.Uncompressed, - ExcludePatterns: []string{archive.WhiteoutMetaPrefix + "*", "!" + archive.WhiteoutOpaqueDir}, - IDMap: a.idMap, - }) -} - -type fileGetNilCloser struct { - storage.FileGetter -} - -func (f fileGetNilCloser) Close() error { - return nil -} - -// DiffGetter returns a FileGetCloser that can read files from the directory that -// contains files for the layer differences. Used for direct access for tar-split. -func (a *Driver) DiffGetter(id string) (graphdriver.FileGetCloser, error) { - p := path.Join(a.rootPath(), "diff", id) - return fileGetNilCloser{storage.NewPathFileGetter(p)}, nil -} - -func (a *Driver) applyDiff(id string, diff io.Reader) error { - return chrootarchive.UntarUncompressed(diff, path.Join(a.rootPath(), "diff", id), &archive.TarOptions{ - IDMap: a.idMap, - }) -} - -// DiffSize calculates the changes between the specified id -// and its parent and returns the size in bytes of the changes -// relative to its base filesystem directory. -func (a *Driver) DiffSize(id, parent string) (size int64, err error) { - if !a.isParent(id, parent) { - return a.naiveDiff.DiffSize(id, parent) - } - // AUFS doesn't need the parent layer to calculate the diff size. - return directory.Size(context.TODO(), path.Join(a.rootPath(), "diff", id)) -} - -// ApplyDiff extracts the changeset from the given diff into the -// layer with the specified id and parent, returning the size of the -// new layer in bytes. -func (a *Driver) ApplyDiff(id, parent string, diff io.Reader) (size int64, err error) { - if !a.isParent(id, parent) { - return a.naiveDiff.ApplyDiff(id, parent, diff) - } - - // AUFS doesn't need the parent id to apply the diff if it is the direct parent. - if err = a.applyDiff(id, diff); err != nil { - return - } - - return a.DiffSize(id, parent) -} - -// Changes produces a list of changes between the specified layer -// and its parent layer. If parent is "", then all changes will be ADD changes. -func (a *Driver) Changes(id, parent string) ([]archive.Change, error) { - if !a.isParent(id, parent) { - return a.naiveDiff.Changes(id, parent) - } - - // AUFS doesn't have snapshots, so we need to get changes from all parent - // layers. - layers, err := a.getParentLayerPaths(id) - if err != nil { - return nil, err - } - return archive.Changes(layers, path.Join(a.rootPath(), "diff", id)) -} - -func (a *Driver) getParentLayerPaths(id string) ([]string, error) { - parentIds, err := getParentIDs(a.rootPath(), id) - if err != nil { - return nil, err - } - layers := make([]string, len(parentIds)) - - // Get the diff paths for all the parent ids - for i, p := range parentIds { - layers[i] = path.Join(a.rootPath(), "diff", p) - } - return layers, nil -} - -func (a *Driver) mount(id string, target string, mountLabel string, layers []string) error { - // If the id is mounted or we get an error return - if mounted, err := a.mounted(target); err != nil || mounted { - return err - } - - rw := a.getDiffPath(id) - - if err := a.aufsMount(layers, rw, target, mountLabel); err != nil { - return fmt.Errorf("error creating aufs mount to %s: %v", target, err) - } - return nil -} - -func (a *Driver) unmount(mountPath string) error { - if mounted, err := a.mounted(mountPath); err != nil || !mounted { - return err - } - return Unmount(mountPath) -} - -func (a *Driver) mounted(mountpoint string) (bool, error) { - return graphdriver.Mounted(graphdriver.FsMagicAufs, mountpoint) -} - -// Cleanup aufs and unmount all mountpoints -func (a *Driver) Cleanup() error { - dir := a.mntPath() - files, err := os.ReadDir(dir) - if err != nil { - return errors.Wrap(err, "aufs readdir error") - } - for _, f := range files { - if !f.IsDir() { - continue - } - - m := path.Join(dir, f.Name()) - - if err := a.unmount(m); err != nil { - logger.WithError(err).WithField("method", "Cleanup()").Warn() - } - } - return mount.RecursiveUnmount(a.root) -} - -func (a *Driver) aufsMount(ro []string, rw, target, mountLabel string) (err error) { - defer func() { - if err != nil { - mount.Unmount(target) - } - }() - - // Mount options are clipped to page size(4096 bytes). If there are more - // layers then these are remounted individually using append. - - offset := 54 - if useDirperm() { - offset += len(",dirperm1") - } - b := make([]byte, unix.Getpagesize()-len(mountLabel)-offset) // room for xino & mountLabel - bp := copy(b, fmt.Sprintf("br:%s=rw", rw)) - - index := 0 - for ; index < len(ro); index++ { - layer := fmt.Sprintf(":%s=ro+wh", ro[index]) - if bp+len(layer) > len(b) { - break - } - bp += copy(b[bp:], layer) - } - - opts := "dio,xino=/dev/shm/aufs.xino" - if useDirperm() { - opts += ",dirperm1" - } - data := label.FormatMountLabel(fmt.Sprintf("%s,%s", string(b[:bp]), opts), mountLabel) - a.mntL.Lock() - err = unix.Mount("none", target, "aufs", 0, data) - a.mntL.Unlock() - if err != nil { - err = errors.Wrap(err, "mount target="+target+" data="+data) - return - } - - for index < len(ro) { - bp = 0 - for ; index < len(ro); index++ { - layer := fmt.Sprintf("append:%s=ro+wh,", ro[index]) - if bp+len(layer) > len(b) { - break - } - bp += copy(b[bp:], layer) - } - data := label.FormatMountLabel(string(b[:bp]), mountLabel) - a.mntL.Lock() - err = unix.Mount("none", target, "aufs", unix.MS_REMOUNT, data) - a.mntL.Unlock() - if err != nil { - err = errors.Wrap(err, "mount target="+target+" flags=MS_REMOUNT data="+data) - return - } - } - - return -} - -// useDirperm checks dirperm1 mount option can be used with the current -// version of aufs. -func useDirperm() bool { - enableDirpermLock.Do(func() { - base, err := os.MkdirTemp("", "docker-aufs-base") - if err != nil { - logger.Errorf("error checking dirperm1: %v", err) - return - } - defer os.RemoveAll(base) - - union, err := os.MkdirTemp("", "docker-aufs-union") - if err != nil { - logger.Errorf("error checking dirperm1: %v", err) - return - } - defer os.RemoveAll(union) - - opts := fmt.Sprintf("br:%s,dirperm1,xino=/dev/shm/aufs.xino", base) - if err := unix.Mount("none", union, "aufs", 0, opts); err != nil { - return - } - enableDirperm = true - if err := Unmount(union); err != nil { - logger.Errorf("error checking dirperm1: failed to unmount %v", err) - } - }) - return enableDirperm -} diff --git a/daemon/graphdriver/aufs/aufs_test.go b/daemon/graphdriver/aufs/aufs_test.go deleted file mode 100644 index 61b70739bc..0000000000 --- a/daemon/graphdriver/aufs/aufs_test.go +++ /dev/null @@ -1,799 +0,0 @@ -//go:build linux -// +build linux - -package aufs // import "github.com/docker/docker/daemon/graphdriver/aufs" - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "os" - "path" - "path/filepath" - "strconv" - "sync" - "testing" - - "github.com/docker/docker/daemon/graphdriver" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/idtools" - "github.com/docker/docker/pkg/reexec" - "github.com/docker/docker/pkg/stringid" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -var ( - tmpOuter = path.Join(os.TempDir(), "aufs-tests") - tmp = path.Join(tmpOuter, "aufs") -) - -func init() { - reexec.Init() -} - -func testInit(dir string, t testing.TB) graphdriver.Driver { - d, err := Init(dir, nil, idtools.IdentityMapping{}) - if err != nil { - if err == graphdriver.ErrNotSupported { - t.Skip(err) - } else { - t.Fatal(err) - } - } - return d -} - -func newDriver(t testing.TB) *Driver { - if err := os.MkdirAll(tmp, 0755); err != nil { - t.Fatal(err) - } - - d := testInit(tmp, t) - return d.(*Driver) -} - -func TestNewDriver(t *testing.T) { - if err := os.MkdirAll(tmp, 0755); err != nil { - t.Fatal(err) - } - - d := testInit(tmp, t) - defer os.RemoveAll(tmp) - if d == nil { - t.Fatal("Driver should not be nil") - } -} - -func TestAufsString(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if d.String() != "aufs" { - t.Fatalf("Expected aufs got %s", d.String()) - } -} - -func TestCreateDirStructure(t *testing.T) { - newDriver(t) - defer os.RemoveAll(tmp) - - paths := []string{ - "mnt", - "layers", - "diff", - } - - for _, p := range paths { - if _, err := os.Stat(path.Join(tmp, p)); err != nil { - t.Fatal(err) - } - } -} - -// We should be able to create two drivers with the same dir structure -func TestNewDriverFromExistingDir(t *testing.T) { - if err := os.MkdirAll(tmp, 0755); err != nil { - t.Fatal(err) - } - - testInit(tmp, t) - testInit(tmp, t) - os.RemoveAll(tmp) -} - -func TestCreateNewDir(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } -} - -func TestCreateNewDirStructure(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - - paths := []string{ - "mnt", - "diff", - "layers", - } - - for _, p := range paths { - if _, err := os.Stat(path.Join(tmp, p, "1")); err != nil { - t.Fatal(err) - } - } -} - -func TestRemoveImage(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - - if err := d.Remove("1"); err != nil { - t.Fatal(err) - } - - paths := []string{ - "mnt", - "diff", - "layers", - } - - for _, p := range paths { - if _, err := os.Stat(path.Join(tmp, p, "1")); err == nil { - t.Fatalf("Error should not be nil because dirs with id 1 should be deleted: %s", p) - } - if _, err := os.Stat(path.Join(tmp, p, "1-removing")); err == nil { - t.Fatalf("Error should not be nil because dirs with id 1-removing should be deleted: %s", p) - } - } -} - -func TestGetWithoutParent(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - - diffPath, err := d.Get("1", "") - if err != nil { - t.Fatal(err) - } - expected := path.Join(tmp, "diff", "1") - if diffPath != expected { - t.Fatalf("Expected path %s got %s", expected, diffPath) - } -} - -func TestCleanupWithNoDirs(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - err := d.Cleanup() - assert.Check(t, err) -} - -func TestCleanupWithDir(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - - if err := d.Cleanup(); err != nil { - t.Fatal(err) - } -} - -func TestMountedFalseResponse(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - err := d.Create("1", "", nil) - assert.NilError(t, err) - - response, err := d.mounted(d.getDiffPath("1")) - assert.NilError(t, err) - assert.Check(t, !response) -} - -func TestMountedTrueResponse(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - defer d.Cleanup() - - err := d.Create("1", "", nil) - assert.NilError(t, err) - err = d.Create("2", "1", nil) - assert.NilError(t, err) - - _, err = d.Get("2", "") - assert.NilError(t, err) - - response, err := d.mounted(d.pathCache["2"]) - assert.NilError(t, err) - assert.Check(t, response) -} - -func TestMountWithParent(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - if err := d.Create("2", "1", nil); err != nil { - t.Fatal(err) - } - - defer func() { - if err := d.Cleanup(); err != nil { - t.Fatal(err) - } - }() - - mntPath, err := d.Get("2", "") - if err != nil { - t.Fatal(err) - } - if mntPath == "" { - t.Fatal("mntPath should not be empty") - } - - expected := path.Join(tmp, "mnt", "2") - if mntPath != expected { - t.Fatalf("Expected %s got %s", expected, mntPath) - } -} - -func TestRemoveMountedDir(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - if err := d.Create("2", "1", nil); err != nil { - t.Fatal(err) - } - - defer func() { - if err := d.Cleanup(); err != nil { - t.Fatal(err) - } - }() - - mntPath, err := d.Get("2", "") - if err != nil { - t.Fatal(err) - } - if mntPath == "" { - t.Fatal("mntPath should not be empty") - } - - mounted, err := d.mounted(d.pathCache["2"]) - if err != nil { - t.Fatal(err) - } - - if !mounted { - t.Fatal("Dir id 2 should be mounted") - } - - if err := d.Remove("2"); err != nil { - t.Fatal(err) - } -} - -func TestCreateWithInvalidParent(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "docker", nil); err == nil { - t.Fatal("Error should not be nil with parent does not exist") - } -} - -func TestGetDiff(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.CreateReadWrite("1", "", nil); err != nil { - t.Fatal(err) - } - - diffPath, err := d.Get("1", "") - if err != nil { - t.Fatal(err) - } - - // Add a file to the diff path with a fixed size - size := int64(1024) - - f, err := os.Create(path.Join(diffPath, "test_file")) - if err != nil { - t.Fatal(err) - } - if err := f.Truncate(size); err != nil { - t.Fatal(err) - } - f.Close() - - a, err := d.Diff("1", "") - if err != nil { - t.Fatal(err) - } - if a == nil { - t.Fatal("Archive should not be nil") - } -} - -func TestChanges(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - - if err := d.CreateReadWrite("2", "1", nil); err != nil { - t.Fatal(err) - } - - defer func() { - if err := d.Cleanup(); err != nil { - t.Fatal(err) - } - }() - - mntPoint, err := d.Get("2", "") - if err != nil { - t.Fatal(err) - } - - // Create a file to save in the mountpoint - f, err := os.Create(path.Join(mntPoint, "test.txt")) - if err != nil { - t.Fatal(err) - } - - if _, err := f.WriteString("testline"); err != nil { - t.Fatal(err) - } - if err := f.Close(); err != nil { - t.Fatal(err) - } - - changes, err := d.Changes("2", "") - if err != nil { - t.Fatal(err) - } - if len(changes) != 1 { - t.Fatalf("Dir 2 should have one change from parent got %d", len(changes)) - } - change := changes[0] - - expectedPath := "/test.txt" - if change.Path != expectedPath { - t.Fatalf("Expected path %s got %s", expectedPath, change.Path) - } - - if change.Kind != archive.ChangeAdd { - t.Fatalf("Change kind should be ChangeAdd got %s", change.Kind) - } - - if err := d.CreateReadWrite("3", "2", nil); err != nil { - t.Fatal(err) - } - mntPoint, err = d.Get("3", "") - if err != nil { - t.Fatal(err) - } - - // Create a file to save in the mountpoint - f, err = os.Create(path.Join(mntPoint, "test2.txt")) - if err != nil { - t.Fatal(err) - } - - if _, err := f.WriteString("testline"); err != nil { - t.Fatal(err) - } - if err := f.Close(); err != nil { - t.Fatal(err) - } - - changes, err = d.Changes("3", "2") - if err != nil { - t.Fatal(err) - } - - if len(changes) != 1 { - t.Fatalf("Dir 2 should have one change from parent got %d", len(changes)) - } - change = changes[0] - - expectedPath = "/test2.txt" - if change.Path != expectedPath { - t.Fatalf("Expected path %s got %s", expectedPath, change.Path) - } - - if change.Kind != archive.ChangeAdd { - t.Fatalf("Change kind should be ChangeAdd got %s", change.Kind) - } -} - -func TestDiffSize(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - - if err := d.CreateReadWrite("1", "", nil); err != nil { - t.Fatal(err) - } - - diffPath, err := d.Get("1", "") - if err != nil { - t.Fatal(err) - } - - // Add a file to the diff path with a fixed size - size := int64(1024) - - f, err := os.Create(path.Join(diffPath, "test_file")) - if err != nil { - t.Fatal(err) - } - if err := f.Truncate(size); err != nil { - t.Fatal(err) - } - s, err := f.Stat() - if err != nil { - t.Fatal(err) - } - size = s.Size() - if err := f.Close(); err != nil { - t.Fatal(err) - } - - diffSize, err := d.DiffSize("1", "") - if err != nil { - t.Fatal(err) - } - if diffSize != size { - t.Fatalf("Expected size to be %d got %d", size, diffSize) - } -} - -func TestChildDiffSize(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - defer d.Cleanup() - - if err := d.CreateReadWrite("1", "", nil); err != nil { - t.Fatal(err) - } - - diffPath, err := d.Get("1", "") - if err != nil { - t.Fatal(err) - } - - // Add a file to the diff path with a fixed size - size := int64(1024) - - f, err := os.Create(path.Join(diffPath, "test_file")) - if err != nil { - t.Fatal(err) - } - if err := f.Truncate(size); err != nil { - t.Fatal(err) - } - s, err := f.Stat() - if err != nil { - t.Fatal(err) - } - size = s.Size() - if err := f.Close(); err != nil { - t.Fatal(err) - } - - diffSize, err := d.DiffSize("1", "") - if err != nil { - t.Fatal(err) - } - if diffSize != size { - t.Fatalf("Expected size to be %d got %d", size, diffSize) - } - - if err := d.Create("2", "1", nil); err != nil { - t.Fatal(err) - } - - diffSize, err = d.DiffSize("2", "1") - if err != nil { - t.Fatal(err) - } - // The diff size for the child should be zero - if diffSize != 0 { - t.Fatalf("Expected size to be %d got %d", 0, diffSize) - } -} - -func TestExists(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - defer d.Cleanup() - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - - if d.Exists("none") { - t.Fatal("id none should not exist in the driver") - } - - if !d.Exists("1") { - t.Fatal("id 1 should exist in the driver") - } -} - -func TestStatus(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - defer d.Cleanup() - - if err := d.Create("1", "", nil); err != nil { - t.Fatal(err) - } - - status := d.Status() - assert.Check(t, is.Len(status, 4)) - - rootDir := status[0] - dirs := status[2] - if rootDir[0] != "Root Dir" { - t.Fatalf("Expected Root Dir got %s", rootDir[0]) - } - if rootDir[1] != d.rootPath() { - t.Fatalf("Expected %s got %s", d.rootPath(), rootDir[1]) - } - if dirs[0] != "Dirs" { - t.Fatalf("Expected Dirs got %s", dirs[0]) - } - if dirs[1] != "1" { - t.Fatalf("Expected 1 got %s", dirs[1]) - } -} - -func TestApplyDiff(t *testing.T) { - d := newDriver(t) - defer os.RemoveAll(tmp) - defer d.Cleanup() - - if err := d.CreateReadWrite("1", "", nil); err != nil { - t.Fatal(err) - } - - diffPath, err := d.Get("1", "") - if err != nil { - t.Fatal(err) - } - - // Add a file to the diff path with a fixed size - size := int64(1024) - - f, err := os.Create(path.Join(diffPath, "test_file")) - if err != nil { - t.Fatal(err) - } - if err := f.Truncate(size); err != nil { - t.Fatal(err) - } - f.Close() - - diff, err := d.Diff("1", "") - if err != nil { - t.Fatal(err) - } - - if err := d.Create("2", "", nil); err != nil { - t.Fatal(err) - } - if err := d.Create("3", "2", nil); err != nil { - t.Fatal(err) - } - - if err := d.applyDiff("3", diff); err != nil { - t.Fatal(err) - } - - // Ensure that the file is in the mount point for id 3 - - mountPoint, err := d.Get("3", "") - if err != nil { - t.Fatal(err) - } - if _, err := os.Stat(path.Join(mountPoint, "test_file")); err != nil { - t.Fatal(err) - } -} - -func hash(c string) string { - h := sha256.New() - fmt.Fprint(h, c) - return hex.EncodeToString(h.Sum(nil)) -} - -func testMountMoreThan42Layers(t *testing.T, mountPath string) { - if err := os.MkdirAll(mountPath, 0755); err != nil { - t.Fatal(err) - } - - defer os.RemoveAll(mountPath) - d := testInit(mountPath, t).(*Driver) - defer d.Cleanup() - var last string - var expected int - - for i := 1; i < 127; i++ { - expected++ - var ( - parent = strconv.Itoa(i - 1) - current = strconv.Itoa(i) - ) - - if parent == "0" { - parent = "" - } else { - parent = hash(parent) - } - current = hash(current) - - err := d.CreateReadWrite(current, parent, nil) - assert.NilError(t, err, "current layer %d", i) - - point, err := d.Get(current, "") - assert.NilError(t, err, "current layer %d", i) - - f, err := os.Create(path.Join(point, current)) - assert.NilError(t, err, "current layer %d", i) - f.Close() - - if i%10 == 0 { - err := os.Remove(path.Join(point, parent)) - assert.NilError(t, err, "current layer %d", i) - expected-- - } - last = current - } - - // Perform the actual mount for the top most image - point, err := d.Get(last, "") - assert.NilError(t, err) - files, err := os.ReadDir(point) - assert.NilError(t, err) - assert.Check(t, is.Len(files, expected)) -} - -func TestMountMoreThan42Layers(t *testing.T) { - defer os.RemoveAll(tmpOuter) - testMountMoreThan42Layers(t, tmp) -} - -func TestMountMoreThan42LayersMatchingPathLength(t *testing.T) { - defer os.RemoveAll(tmpOuter) - zeroes := "0" - for { - // This finds a mount path so that when combined into aufs mount options - // 4096 byte boundary would be in between the paths or in permission - // section. For '/tmp' it will use '/tmp/aufs-tests/00000000/aufs' - mountPath := path.Join(tmpOuter, zeroes, "aufs") - pathLength := 77 + len(mountPath) - - if mod := 4095 % pathLength; mod == 0 || mod > pathLength-2 { - t.Logf("Using path: %s", mountPath) - testMountMoreThan42Layers(t, mountPath) - return - } - zeroes += "0" - } -} - -func BenchmarkConcurrentAccess(b *testing.B) { - b.StopTimer() - b.ResetTimer() - - d := newDriver(b) - defer os.RemoveAll(tmp) - defer d.Cleanup() - - numConcurrent := 256 - // create a bunch of ids - ids := make([]string, numConcurrent) - for i := 0; i < numConcurrent; i++ { - ids[i] = stringid.GenerateRandomID() - } - - if err := d.Create(ids[0], "", nil); err != nil { - b.Fatal(err) - } - - if err := d.Create(ids[1], ids[0], nil); err != nil { - b.Fatal(err) - } - - parent := ids[1] - ids = ids[2:] - - chErr := make(chan error, numConcurrent) - var outerGroup sync.WaitGroup - outerGroup.Add(len(ids)) - b.StartTimer() - - // here's the actual bench - for _, id := range ids { - go func(id string) { - defer outerGroup.Done() - if err := d.Create(id, parent, nil); err != nil { - b.Logf("Create %s failed", id) - chErr <- err - return - } - var innerGroup sync.WaitGroup - for i := 0; i < b.N; i++ { - innerGroup.Add(1) - go func() { - d.Get(id, "") - d.Put(id) - innerGroup.Done() - }() - } - innerGroup.Wait() - d.Remove(id) - }(id) - } - - outerGroup.Wait() - b.StopTimer() - close(chErr) - for err := range chErr { - if err != nil { - b.Log(err) - b.Fail() - } - } -} - -func TestInitStaleCleanup(t *testing.T) { - if err := os.MkdirAll(tmp, 0755); err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) - - for _, d := range []string{"diff", "mnt"} { - if err := os.MkdirAll(filepath.Join(tmp, d, "123-removing"), 0755); err != nil { - t.Fatal(err) - } - } - - testInit(tmp, t) - for _, d := range []string{"diff", "mnt"} { - if _, err := os.Stat(filepath.Join(tmp, d, "123-removing")); err == nil { - t.Fatal("cleanup failed") - } - } -} diff --git a/daemon/graphdriver/aufs/dirs.go b/daemon/graphdriver/aufs/dirs.go deleted file mode 100644 index 006c556c07..0000000000 --- a/daemon/graphdriver/aufs/dirs.go +++ /dev/null @@ -1,64 +0,0 @@ -//go:build linux -// +build linux - -package aufs // import "github.com/docker/docker/daemon/graphdriver/aufs" - -import ( - "bufio" - "os" - "path" -) - -// Return all the directories -func loadIds(root string) ([]string, error) { - dirs, err := os.ReadDir(root) - if err != nil { - return nil, err - } - var out []string - for _, d := range dirs { - if !d.IsDir() { - out = append(out, d.Name()) - } - } - return out, nil -} - -// Read the layers file for the current id and return all the -// layers represented by new lines in the file -// -// If there are no lines in the file then the id has no parent -// and an empty slice is returned. -func getParentIDs(root, id string) ([]string, error) { - f, err := os.Open(path.Join(root, "layers", id)) - if err != nil { - return nil, err - } - defer f.Close() - - var out []string - s := bufio.NewScanner(f) - - for s.Scan() { - if t := s.Text(); t != "" { - out = append(out, s.Text()) - } - } - return out, s.Err() -} - -func (a *Driver) getMountpoint(id string) string { - return path.Join(a.mntPath(), id) -} - -func (a *Driver) mntPath() string { - return path.Join(a.rootPath(), "mnt") -} - -func (a *Driver) getDiffPath(id string) string { - return path.Join(a.diffPath(), id) -} - -func (a *Driver) diffPath() string { - return path.Join(a.rootPath(), "diff") -} diff --git a/daemon/graphdriver/aufs/mount.go b/daemon/graphdriver/aufs/mount.go deleted file mode 100644 index 33ee5a6400..0000000000 --- a/daemon/graphdriver/aufs/mount.go +++ /dev/null @@ -1,56 +0,0 @@ -//go:build linux -// +build linux - -package aufs // import "github.com/docker/docker/daemon/graphdriver/aufs" - -import ( - "os/exec" - "syscall" - "time" - - "github.com/moby/sys/mount" - "github.com/pkg/errors" - "golang.org/x/sys/unix" -) - -// Unmount the target specified. -func Unmount(target string) error { - const retries = 5 - - // auplink flush - for i := 0; ; i++ { - out, err := exec.Command("auplink", target, "flush").CombinedOutput() - if err == nil { - break - } - rc := 0 - if exiterr, ok := err.(*exec.ExitError); ok { - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - rc = status.ExitStatus() - } - } - if i >= retries || rc != int(unix.EINVAL) { - logger.WithError(err).WithField("method", "Unmount").Warnf("auplink flush failed: %s", out) - break - } - // auplink failed to find target in /proc/self/mounts because - // kernel can't guarantee continuity while reading from it - // while mounts table is being changed - logger.Debugf("auplink flush error (retrying %d/%d): %s", i+1, retries, out) - } - - // unmount - var err error - for i := 0; i < retries; i++ { - err = mount.Unmount(target) - if err != nil && errors.Is(err, unix.EBUSY) { - logger.Debugf("aufs unmount %s failed with EBUSY (retrying %d/%d)", target, i+1, retries) - time.Sleep(100 * time.Millisecond) - continue // try again - } - break - } - - // either no error occurred, or another error - return err -} diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index 34a2ec4fe7..396eb229d4 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -1,6 +1,7 @@ package graphdriver // import "github.com/docker/docker/daemon/graphdriver" import ( + "fmt" "io" "os" "path/filepath" @@ -192,6 +193,9 @@ type Options struct { func New(name string, pg plugingetter.PluginGetter, config Options) (Driver, error) { if name != "" { logrus.Infof("[graphdriver] trying configured driver: %s", name) + if err := checkRemoved(name); err != nil { + return nil, err + } if isDeprecated(name) { logrus.Warnf("[graphdriver] WARNING: the %s storage-driver is deprecated and will be removed in a future release; visit https://docs.docker.com/go/storage-driver/ for more information", name) } @@ -314,8 +318,17 @@ func isEmptyDir(name string) bool { func isDeprecated(name string) bool { switch name { // NOTE: when deprecating a driver, update daemon.fillDriverInfo() accordingly - case "aufs", "devicemapper", "overlay": + case "devicemapper", "overlay": return true } return false } + +// checkRemoved checks if a storage-driver has been deprecated (and removed) +func checkRemoved(name string) error { + switch name { + case "aufs": + return NotSupportedError(fmt.Sprintf("[graphdriver] ERROR: the %s storage-driver has been deprecated and removed; visit https://docs.docker.com/go/storage-driver/ for more information", name)) + } + return nil +} diff --git a/daemon/graphdriver/driver_linux.go b/daemon/graphdriver/driver_linux.go index c4cfe8e891..f819adfae9 100644 --- a/daemon/graphdriver/driver_linux.go +++ b/daemon/graphdriver/driver_linux.go @@ -50,7 +50,7 @@ const ( var ( // List of drivers that should be used in an order - priority = "overlay2,fuse-overlayfs,btrfs,zfs,aufs,overlay,devicemapper,vfs" + priority = "overlay2,fuse-overlayfs,btrfs,zfs,overlay,devicemapper,vfs" // FsNames maps filesystem id to name of the filesystem. FsNames = map[FsMagic]string{ diff --git a/daemon/graphdriver/fsdiff.go b/daemon/graphdriver/fsdiff.go index 2a201dc2f5..6a7b1312b7 100644 --- a/daemon/graphdriver/fsdiff.go +++ b/daemon/graphdriver/fsdiff.go @@ -21,7 +21,6 @@ var ( // capability of the Diffing methods on the local file system, // which it may or may not support on its own. See the comment // on the exported NewNaiveDiffDriver function below. -// Notably, the AUFS driver doesn't need to be wrapped like this. type NaiveDiffDriver struct { ProtoDriver idMap idtools.IdentityMapping diff --git a/daemon/graphdriver/register/register_aufs.go b/daemon/graphdriver/register/register_aufs.go deleted file mode 100644 index 4c028f72ef..0000000000 --- a/daemon/graphdriver/register/register_aufs.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !exclude_graphdriver_aufs && linux -// +build !exclude_graphdriver_aufs,linux - -package register // import "github.com/docker/docker/daemon/graphdriver/register" - -import ( - // register the aufs graphdriver - _ "github.com/docker/docker/daemon/graphdriver/aufs" -) diff --git a/daemon/info.go b/daemon/info.go index 607f685dd5..80dfb39deb 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -128,7 +128,7 @@ WARNING: The %s storage-driver is deprecated, and will be removed in a future re Refer to the documentation for more information: https://docs.docker.com/go/storage-driver/` switch v.Driver { - case "aufs", "devicemapper", "overlay": + case "devicemapper", "overlay": v.Warnings = append(v.Warnings, fmt.Sprintf(warnMsg, v.Driver)) } diff --git a/project/ISSUE-TRIAGE.md b/project/ISSUE-TRIAGE.md index 5a91d13457..5f36e992f4 100644 --- a/project/ISSUE-TRIAGE.md +++ b/project/ISSUE-TRIAGE.md @@ -70,7 +70,6 @@ have: | area/security/selinux | | area/security/trust | | area/storage | -| area/storage/aufs | | area/storage/btrfs | | area/storage/devicemapper | | area/storage/overlay | diff --git a/project/PACKAGERS.md b/project/PACKAGERS.md index 5b569f75fd..8fecf9e86e 100644 --- a/project/PACKAGERS.md +++ b/project/PACKAGERS.md @@ -94,14 +94,9 @@ To disable devicemapper: export DOCKER_BUILDTAGS='exclude_graphdriver_devicemapper' ``` -To disable aufs: -```bash -export DOCKER_BUILDTAGS='exclude_graphdriver_aufs' -``` - NOTE: if you need to set more than one build tag, space separate them: ```bash -export DOCKER_BUILDTAGS='exclude_graphdriver_aufs exclude_graphdriver_btrfs' +export DOCKER_BUILDTAGS='exclude_graphdriver_devicemapper exclude_graphdriver_btrfs' ``` ## System Dependencies @@ -137,8 +132,6 @@ the client will even run on alternative platforms such as Mac OS X / Darwin. Some of Docker's features are activated by using optional command-line flags or by having support for them in the kernel or userspace. A few examples include: -* AUFS graph driver (requires AUFS patches/support enabled in the kernel, and at - least the "auplink" utility from aufs-tools) * BTRFS graph driver (requires suitable kernel headers: `linux/btrfs.h` and `linux/btrfs_tree.h`, present in 4.12+; and BTRFS support enabled in the kernel) * ZFS graph driver (requires userspace zfs-utils and a corresponding kernel module) * Libseccomp to allow running seccomp profiles with containers