Browse code

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 authored on 2017/10/31 05:18:14
Showing 8 changed files
... ...
@@ -13,6 +13,7 @@ import (
13 13
 	"unsafe"
14 14
 
15 15
 	"github.com/docker/docker/daemon/graphdriver"
16
+	"github.com/docker/docker/daemon/graphdriver/quota"
16 17
 	"github.com/docker/docker/pkg/stringid"
17 18
 	"github.com/docker/go-units"
18 19
 	"github.com/stretchr/testify/assert"
... ...
@@ -310,7 +311,7 @@ func writeRandomFile(path string, size uint64) error {
310 310
 }
311 311
 
312 312
 // DriverTestSetQuota Create a driver and test setting quota.
313
-func DriverTestSetQuota(t *testing.T, drivername string) {
313
+func DriverTestSetQuota(t *testing.T, drivername string, required bool) {
314 314
 	driver := GetDriver(t, drivername)
315 315
 	defer PutDriver(t)
316 316
 
... ...
@@ -318,19 +319,34 @@ func DriverTestSetQuota(t *testing.T, drivername string) {
318 318
 	createOpts := &graphdriver.CreateOpts{}
319 319
 	createOpts.StorageOpt = make(map[string]string, 1)
320 320
 	createOpts.StorageOpt["size"] = "50M"
321
-	if err := driver.Create("zfsTest", "Base", createOpts); err != nil {
321
+	layerName := drivername + "Test"
322
+	if err := driver.CreateReadWrite(layerName, "Base", createOpts); err == quota.ErrQuotaNotSupported && !required {
323
+		t.Skipf("Quota not supported on underlying filesystem: %v", err)
324
+	} else if err != nil {
322 325
 		t.Fatal(err)
323 326
 	}
324 327
 
325
-	mountPath, err := driver.Get("zfsTest", "")
328
+	mountPath, err := driver.Get(layerName, "")
326 329
 	if err != nil {
327 330
 		t.Fatal(err)
328 331
 	}
329 332
 
330 333
 	quota := uint64(50 * units.MiB)
331 334
 
332
-	err = writeRandomFile(path.Join(mountPath.Path(), "file"), quota*2)
333
-	if pathError, ok := err.(*os.PathError); ok && pathError.Err != unix.EDQUOT {
334
-		t.Fatalf("expect write() to fail with %v, got %v", unix.EDQUOT, err)
335
+	// Try to write a file smaller than quota, and ensure it works
336
+	err = writeRandomFile(path.Join(mountPath.Path(), "smallfile"), quota/2)
337
+	if err != nil {
338
+		t.Fatal(err)
339
+	}
340
+	defer os.Remove(path.Join(mountPath.Path(), "smallfile"))
341
+
342
+	// Try to write a file bigger than quota. We've already filled up half the quota, so hitting the limit should be easy
343
+	err = writeRandomFile(path.Join(mountPath.Path(), "bigfile"), quota)
344
+	if err == nil {
345
+		t.Fatalf("expected write to fail(), instead had success")
346
+	}
347
+	if pathError, ok := err.(*os.PathError); ok && pathError.Err != unix.EDQUOT && pathError.Err != unix.ENOSPC {
348
+		os.Remove(path.Join(mountPath.Path(), "bigfile"))
349
+		t.Fatalf("expect write() to fail with %v or %v, got %v", unix.EDQUOT, unix.ENOSPC, pathError.Err)
335 350
 	}
336 351
 }
337 352
new file mode 100644
... ...
@@ -0,0 +1,19 @@
0
+package quota
1
+
2
+import "github.com/docker/docker/api/errdefs"
3
+
4
+var (
5
+	_ errdefs.ErrNotImplemented = (*errQuotaNotSupported)(nil)
6
+)
7
+
8
+// ErrQuotaNotSupported indicates if were found the FS didn't have projects quotas available
9
+var ErrQuotaNotSupported = errQuotaNotSupported{}
10
+
11
+type errQuotaNotSupported struct {
12
+}
13
+
14
+func (e errQuotaNotSupported) NotImplemented() {}
15
+
16
+func (e errQuotaNotSupported) Error() string {
17
+	return "Filesystem does not support, or has not enabled quotas"
18
+}
... ...
@@ -58,15 +58,10 @@ import (
58 58
 	"path/filepath"
59 59
 	"unsafe"
60 60
 
61
-	"errors"
62
-
63 61
 	"github.com/sirupsen/logrus"
64 62
 	"golang.org/x/sys/unix"
65 63
 )
66 64
 
67
-// ErrQuotaNotSupported indicates if were found the FS does not have projects quotas available
68
-var ErrQuotaNotSupported = errors.New("Filesystem does not support or has not enabled quotas")
69
-
70 65
 // Quota limit params - currently we only control blocks hard limit
71 66
 type Quota struct {
72 67
 	Size uint64
... ...
@@ -6,10 +6,12 @@ import (
6 6
 	"path/filepath"
7 7
 
8 8
 	"github.com/docker/docker/daemon/graphdriver"
9
+	"github.com/docker/docker/daemon/graphdriver/quota"
9 10
 	"github.com/docker/docker/pkg/chrootarchive"
10 11
 	"github.com/docker/docker/pkg/containerfs"
11 12
 	"github.com/docker/docker/pkg/idtools"
12 13
 	"github.com/docker/docker/pkg/system"
14
+	units "github.com/docker/go-units"
13 15
 	"github.com/opencontainers/selinux/go-selinux/label"
14 16
 )
15 17
 
... ...
@@ -33,6 +35,11 @@ func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap
33 33
 	if err := idtools.MkdirAllAndChown(home, 0700, rootIDs); err != nil {
34 34
 		return nil, err
35 35
 	}
36
+
37
+	if err := setupDriverQuota(d); err != nil {
38
+		return nil, err
39
+	}
40
+
36 41
 	return graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps), nil
37 42
 }
38 43
 
... ...
@@ -41,6 +48,7 @@ func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap
41 41
 // In order to support layering, files are copied from the parent layer into the new layer. There is no copy-on-write support.
42 42
 // Driver must be wrapped in NaiveDiffDriver to be used as a graphdriver.Driver
43 43
 type Driver struct {
44
+	driverQuota
44 45
 	home       string
45 46
 	idMappings *idtools.IDMappings
46 47
 }
... ...
@@ -67,15 +75,38 @@ func (d *Driver) Cleanup() error {
67 67
 // CreateReadWrite creates a layer that is writable for use as a container
68 68
 // file system.
69 69
 func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error {
70
-	return d.Create(id, parent, opts)
70
+	var err error
71
+	var size int64
72
+
73
+	if opts != nil {
74
+		for key, val := range opts.StorageOpt {
75
+			switch key {
76
+			case "size":
77
+				if !d.quotaSupported() {
78
+					return quota.ErrQuotaNotSupported
79
+				}
80
+				if size, err = units.RAMInBytes(val); err != nil {
81
+					return err
82
+				}
83
+			default:
84
+				return fmt.Errorf("Storage opt %s not supported", key)
85
+			}
86
+		}
87
+	}
88
+
89
+	return d.create(id, parent, uint64(size))
71 90
 }
72 91
 
73 92
 // Create prepares the filesystem for the VFS driver and copies the directory for the given id under the parent.
74 93
 func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error {
75 94
 	if opts != nil && len(opts.StorageOpt) != 0 {
76
-		return fmt.Errorf("--storage-opt is not supported for vfs")
95
+		return fmt.Errorf("--storage-opt is not supported for vfs on read-only layers")
77 96
 	}
78 97
 
98
+	return d.create(id, parent, 0)
99
+}
100
+
101
+func (d *Driver) create(id, parent string, size uint64) error {
79 102
 	dir := d.dir(id)
80 103
 	rootIDs := d.idMappings.RootPair()
81 104
 	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 {
84 84
 	if err := idtools.MkdirAndChown(dir, 0755, rootIDs); err != nil {
85 85
 		return err
86 86
 	}
87
+
88
+	if size != 0 {
89
+		if err := d.setupQuota(dir, size); err != nil {
90
+			return err
91
+		}
92
+	}
93
+
87 94
 	labelOpts := []string{"level:s0"}
88 95
 	if _, mountLabel, err := label.InitLabels(labelOpts); err == nil {
89 96
 		label.SetFileLabel(dir, mountLabel)
90 97
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+// +build linux
1
+
2
+package vfs
3
+
4
+import "github.com/docker/docker/daemon/graphdriver/quota"
5
+
6
+type driverQuota struct {
7
+	quotaCtl *quota.Control
8
+}
9
+
10
+func setupDriverQuota(driver *Driver) error {
11
+	if quotaCtl, err := quota.NewControl(driver.home); err == nil {
12
+		driver.quotaCtl = quotaCtl
13
+	} else if err != quota.ErrQuotaNotSupported {
14
+		return err
15
+	}
16
+
17
+	return nil
18
+}
19
+
20
+func (d *Driver) setupQuota(dir string, size uint64) error {
21
+	return d.quotaCtl.SetQuota(dir, quota.Quota{Size: size})
22
+}
23
+
24
+func (d *Driver) quotaSupported() bool {
25
+	return d.quotaCtl != nil
26
+}
0 27
new file mode 100644
... ...
@@ -0,0 +1,20 @@
0
+// +build !linux
1
+
2
+package vfs
3
+
4
+import "github.com/docker/docker/daemon/graphdriver/quota"
5
+
6
+type driverQuota struct {
7
+}
8
+
9
+func setupDriverQuota(driver *Driver) error {
10
+	return nil
11
+}
12
+
13
+func (d *Driver) setupQuota(dir string, size uint64) error {
14
+	return quota.ErrQuotaNotSupported
15
+}
16
+
17
+func (d *Driver) quotaSupported() bool {
18
+	return false
19
+}
... ...
@@ -32,6 +32,10 @@ func TestVfsCreateSnap(t *testing.T) {
32 32
 	graphtest.DriverTestCreateSnap(t, "vfs")
33 33
 }
34 34
 
35
+func TestVfsSetQuota(t *testing.T) {
36
+	graphtest.DriverTestSetQuota(t, "vfs", false)
37
+}
38
+
35 39
 func TestVfsTeardown(t *testing.T) {
36 40
 	graphtest.PutDriver(t)
37 41
 }
... ...
@@ -27,7 +27,7 @@ func TestZfsCreateSnap(t *testing.T) {
27 27
 }
28 28
 
29 29
 func TestZfsSetQuota(t *testing.T) {
30
-	graphtest.DriverTestSetQuota(t, "zfs")
30
+	graphtest.DriverTestSetQuota(t, "zfs", true)
31 31
 }
32 32
 
33 33
 func TestZfsTeardown(t *testing.T) {