Преглед изворни кода

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 <sargun@sargun.me>
Sargun Dhillon пре 7 година
родитељ
комит
7a1618ced3

+ 22 - 6
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)
 	}
 }

+ 19 - 0
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"
+}

+ 0 - 5
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

+ 40 - 2
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)

+ 27 - 0
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
+}

+ 20 - 0
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
+}

+ 4 - 0
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)
 }

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