From 7a1618ced359a3ac921d8a05903d62f544ff17d0 Mon Sep 17 00:00:00 2001 From: Sargun Dhillon Date: Mon, 30 Oct 2017 13:18:14 -0700 Subject: [PATCH] Add quota support to VFS graphdriver This patch adds the capability for the VFS graphdriver to use XFS project quotas. It reuses the existing quota management code that was created by overlay2 on XFS. It doesn't rely on a filesystem whitelist, but instead the quota-capability detection code. Signed-off-by: Sargun Dhillon --- .../graphdriver/graphtest/graphtest_unix.go | 28 ++++++++++--- daemon/graphdriver/quota/errors.go | 19 +++++++++ daemon/graphdriver/quota/projectquota.go | 5 --- daemon/graphdriver/vfs/driver.go | 42 ++++++++++++++++++- daemon/graphdriver/vfs/quota_linux.go | 27 ++++++++++++ daemon/graphdriver/vfs/quota_unsupported.go | 20 +++++++++ daemon/graphdriver/vfs/vfs_test.go | 4 ++ daemon/graphdriver/zfs/zfs_test.go | 2 +- 8 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 daemon/graphdriver/quota/errors.go create mode 100644 daemon/graphdriver/vfs/quota_linux.go create mode 100644 daemon/graphdriver/vfs/quota_unsupported.go diff --git a/daemon/graphdriver/graphtest/graphtest_unix.go b/daemon/graphdriver/graphtest/graphtest_unix.go index 6b352ba69a..c25d4826fc 100644 --- a/daemon/graphdriver/graphtest/graphtest_unix.go +++ b/daemon/graphdriver/graphtest/graphtest_unix.go @@ -13,6 +13,7 @@ import ( "unsafe" "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/daemon/graphdriver/quota" "github.com/docker/docker/pkg/stringid" "github.com/docker/go-units" "github.com/stretchr/testify/assert" @@ -310,7 +311,7 @@ func writeRandomFile(path string, size uint64) error { } // DriverTestSetQuota Create a driver and test setting quota. -func DriverTestSetQuota(t *testing.T, drivername string) { +func DriverTestSetQuota(t *testing.T, drivername string, required bool) { driver := GetDriver(t, drivername) defer PutDriver(t) @@ -318,19 +319,34 @@ func DriverTestSetQuota(t *testing.T, drivername string) { createOpts := &graphdriver.CreateOpts{} createOpts.StorageOpt = make(map[string]string, 1) createOpts.StorageOpt["size"] = "50M" - if err := driver.Create("zfsTest", "Base", createOpts); err != nil { + layerName := drivername + "Test" + if err := driver.CreateReadWrite(layerName, "Base", createOpts); err == quota.ErrQuotaNotSupported && !required { + t.Skipf("Quota not supported on underlying filesystem: %v", err) + } else if err != nil { t.Fatal(err) } - mountPath, err := driver.Get("zfsTest", "") + mountPath, err := driver.Get(layerName, "") if err != nil { t.Fatal(err) } quota := uint64(50 * units.MiB) - err = writeRandomFile(path.Join(mountPath.Path(), "file"), quota*2) - if pathError, ok := err.(*os.PathError); ok && pathError.Err != unix.EDQUOT { - t.Fatalf("expect write() to fail with %v, got %v", unix.EDQUOT, err) + // Try to write a file smaller than quota, and ensure it works + err = writeRandomFile(path.Join(mountPath.Path(), "smallfile"), quota/2) + if err != nil { + t.Fatal(err) + } + defer os.Remove(path.Join(mountPath.Path(), "smallfile")) + + // Try to write a file bigger than quota. We've already filled up half the quota, so hitting the limit should be easy + err = writeRandomFile(path.Join(mountPath.Path(), "bigfile"), quota) + if err == nil { + t.Fatalf("expected write to fail(), instead had success") + } + if pathError, ok := err.(*os.PathError); ok && pathError.Err != unix.EDQUOT && pathError.Err != unix.ENOSPC { + os.Remove(path.Join(mountPath.Path(), "bigfile")) + t.Fatalf("expect write() to fail with %v or %v, got %v", unix.EDQUOT, unix.ENOSPC, pathError.Err) } } diff --git a/daemon/graphdriver/quota/errors.go b/daemon/graphdriver/quota/errors.go new file mode 100644 index 0000000000..1741f2f5db --- /dev/null +++ b/daemon/graphdriver/quota/errors.go @@ -0,0 +1,19 @@ +package quota + +import "github.com/docker/docker/api/errdefs" + +var ( + _ errdefs.ErrNotImplemented = (*errQuotaNotSupported)(nil) +) + +// ErrQuotaNotSupported indicates if were found the FS didn't have projects quotas available +var ErrQuotaNotSupported = errQuotaNotSupported{} + +type errQuotaNotSupported struct { +} + +func (e errQuotaNotSupported) NotImplemented() {} + +func (e errQuotaNotSupported) Error() string { + return "Filesystem does not support, or has not enabled quotas" +} diff --git a/daemon/graphdriver/quota/projectquota.go b/daemon/graphdriver/quota/projectquota.go index 84e391aa89..9709588b6b 100644 --- a/daemon/graphdriver/quota/projectquota.go +++ b/daemon/graphdriver/quota/projectquota.go @@ -58,15 +58,10 @@ import ( "path/filepath" "unsafe" - "errors" - "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) -// ErrQuotaNotSupported indicates if were found the FS does not have projects quotas available -var ErrQuotaNotSupported = errors.New("Filesystem does not support or has not enabled quotas") - // Quota limit params - currently we only control blocks hard limit type Quota struct { Size uint64 diff --git a/daemon/graphdriver/vfs/driver.go b/daemon/graphdriver/vfs/driver.go index 0482dccb87..610476fd88 100644 --- a/daemon/graphdriver/vfs/driver.go +++ b/daemon/graphdriver/vfs/driver.go @@ -6,10 +6,12 @@ import ( "path/filepath" "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/daemon/graphdriver/quota" "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/system" + units "github.com/docker/go-units" "github.com/opencontainers/selinux/go-selinux/label" ) @@ -33,6 +35,11 @@ func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap if err := idtools.MkdirAllAndChown(home, 0700, rootIDs); err != nil { return nil, err } + + if err := setupDriverQuota(d); err != nil { + return nil, err + } + return graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps), nil } @@ -41,6 +48,7 @@ func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap // In order to support layering, files are copied from the parent layer into the new layer. There is no copy-on-write support. // Driver must be wrapped in NaiveDiffDriver to be used as a graphdriver.Driver type Driver struct { + driverQuota home string idMappings *idtools.IDMappings } @@ -67,15 +75,38 @@ func (d *Driver) Cleanup() error { // CreateReadWrite creates a layer that is writable for use as a container // file system. func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error { - return d.Create(id, parent, opts) + var err error + var size int64 + + if opts != nil { + for key, val := range opts.StorageOpt { + switch key { + case "size": + if !d.quotaSupported() { + return quota.ErrQuotaNotSupported + } + if size, err = units.RAMInBytes(val); err != nil { + return err + } + default: + return fmt.Errorf("Storage opt %s not supported", key) + } + } + } + + return d.create(id, parent, uint64(size)) } // Create prepares the filesystem for the VFS driver and copies the directory for the given id under the parent. func (d *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 vfs") + return fmt.Errorf("--storage-opt is not supported for vfs on read-only layers") } + return d.create(id, parent, 0) +} + +func (d *Driver) create(id, parent string, size uint64) error { dir := d.dir(id) rootIDs := d.idMappings.RootPair() if err := idtools.MkdirAllAndChown(filepath.Dir(dir), 0700, rootIDs); err != nil { @@ -84,6 +115,13 @@ func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error { if err := idtools.MkdirAndChown(dir, 0755, rootIDs); err != nil { return err } + + if size != 0 { + if err := d.setupQuota(dir, size); err != nil { + return err + } + } + labelOpts := []string{"level:s0"} if _, mountLabel, err := label.InitLabels(labelOpts); err == nil { label.SetFileLabel(dir, mountLabel) diff --git a/daemon/graphdriver/vfs/quota_linux.go b/daemon/graphdriver/vfs/quota_linux.go new file mode 100644 index 0000000000..032c15b9ef --- /dev/null +++ b/daemon/graphdriver/vfs/quota_linux.go @@ -0,0 +1,27 @@ +// +build linux + +package vfs + +import "github.com/docker/docker/daemon/graphdriver/quota" + +type driverQuota struct { + quotaCtl *quota.Control +} + +func setupDriverQuota(driver *Driver) error { + if quotaCtl, err := quota.NewControl(driver.home); err == nil { + driver.quotaCtl = quotaCtl + } else if err != quota.ErrQuotaNotSupported { + return err + } + + return nil +} + +func (d *Driver) setupQuota(dir string, size uint64) error { + return d.quotaCtl.SetQuota(dir, quota.Quota{Size: size}) +} + +func (d *Driver) quotaSupported() bool { + return d.quotaCtl != nil +} diff --git a/daemon/graphdriver/vfs/quota_unsupported.go b/daemon/graphdriver/vfs/quota_unsupported.go new file mode 100644 index 0000000000..9cca53d372 --- /dev/null +++ b/daemon/graphdriver/vfs/quota_unsupported.go @@ -0,0 +1,20 @@ +// +build !linux + +package vfs + +import "github.com/docker/docker/daemon/graphdriver/quota" + +type driverQuota struct { +} + +func setupDriverQuota(driver *Driver) error { + return nil +} + +func (d *Driver) setupQuota(dir string, size uint64) error { + return quota.ErrQuotaNotSupported +} + +func (d *Driver) quotaSupported() bool { + return false +} diff --git a/daemon/graphdriver/vfs/vfs_test.go b/daemon/graphdriver/vfs/vfs_test.go index 9ecf21dbaa..16dc1357f1 100644 --- a/daemon/graphdriver/vfs/vfs_test.go +++ b/daemon/graphdriver/vfs/vfs_test.go @@ -32,6 +32,10 @@ func TestVfsCreateSnap(t *testing.T) { graphtest.DriverTestCreateSnap(t, "vfs") } +func TestVfsSetQuota(t *testing.T) { + graphtest.DriverTestSetQuota(t, "vfs", false) +} + func TestVfsTeardown(t *testing.T) { graphtest.PutDriver(t) } diff --git a/daemon/graphdriver/zfs/zfs_test.go b/daemon/graphdriver/zfs/zfs_test.go index 3e22928438..2eb85c4d83 100644 --- a/daemon/graphdriver/zfs/zfs_test.go +++ b/daemon/graphdriver/zfs/zfs_test.go @@ -27,7 +27,7 @@ func TestZfsCreateSnap(t *testing.T) { } func TestZfsSetQuota(t *testing.T) { - graphtest.DriverTestSetQuota(t, "zfs") + graphtest.DriverTestSetQuota(t, "zfs", true) } func TestZfsTeardown(t *testing.T) {