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) {