Browse code

quota: move quota package out of graphdriver

Signed-off-by: Timo Rothenpieler <timo@rothenpieler.org>

Timo Rothenpieler authored on 2020/08/10 05:04:43
Showing 15 changed files
... ...
@@ -13,8 +13,8 @@ import (
13 13
 	"unsafe"
14 14
 
15 15
 	"github.com/docker/docker/daemon/graphdriver"
16
-	"github.com/docker/docker/daemon/graphdriver/quota"
17 16
 	"github.com/docker/docker/pkg/stringid"
17
+	"github.com/docker/docker/quota"
18 18
 	units "github.com/docker/go-units"
19 19
 	"golang.org/x/sys/unix"
20 20
 	"gotest.tools/v3/assert"
... ...
@@ -18,7 +18,6 @@ import (
18 18
 	"github.com/containerd/containerd/sys"
19 19
 	"github.com/docker/docker/daemon/graphdriver"
20 20
 	"github.com/docker/docker/daemon/graphdriver/overlayutils"
21
-	"github.com/docker/docker/daemon/graphdriver/quota"
22 21
 	"github.com/docker/docker/pkg/archive"
23 22
 	"github.com/docker/docker/pkg/chrootarchive"
24 23
 	"github.com/docker/docker/pkg/containerfs"
... ...
@@ -27,6 +26,7 @@ import (
27 27
 	"github.com/docker/docker/pkg/idtools"
28 28
 	"github.com/docker/docker/pkg/parsers"
29 29
 	"github.com/docker/docker/pkg/system"
30
+	"github.com/docker/docker/quota"
30 31
 	units "github.com/docker/go-units"
31 32
 	"github.com/moby/locker"
32 33
 	"github.com/moby/sys/mount"
33 34
deleted file mode 100644
... ...
@@ -1,19 +0,0 @@
1
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
2
-
3
-import "github.com/docker/docker/errdefs"
4
-
5
-var (
6
-	_ errdefs.ErrNotImplemented = (*errQuotaNotSupported)(nil)
7
-)
8
-
9
-// ErrQuotaNotSupported indicates if were found the FS didn't have projects quotas available
10
-var ErrQuotaNotSupported = errQuotaNotSupported{}
11
-
12
-type errQuotaNotSupported struct {
13
-}
14
-
15
-func (e errQuotaNotSupported) NotImplemented() {}
16
-
17
-func (e errQuotaNotSupported) Error() string {
18
-	return "Filesystem does not support, or has not enabled quotas"
19
-}
20 1
deleted file mode 100644
... ...
@@ -1,442 +0,0 @@
1
-// +build linux,!exclude_disk_quota,cgo
2
-
3
-//
4
-// projectquota.go - implements XFS project quota controls
5
-// for setting quota limits on a newly created directory.
6
-// It currently supports the legacy XFS specific ioctls.
7
-//
8
-// TODO: use generic quota control ioctl FS_IOC_FS{GET,SET}XATTR
9
-//       for both xfs/ext4 for kernel version >= v4.5
10
-//
11
-
12
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
13
-
14
-/*
15
-#include <stdlib.h>
16
-#include <dirent.h>
17
-#include <linux/fs.h>
18
-#include <linux/quota.h>
19
-#include <linux/dqblk_xfs.h>
20
-
21
-#ifndef FS_XFLAG_PROJINHERIT
22
-struct fsxattr {
23
-	__u32		fsx_xflags;
24
-	__u32		fsx_extsize;
25
-	__u32		fsx_nextents;
26
-	__u32		fsx_projid;
27
-	unsigned char	fsx_pad[12];
28
-};
29
-#define FS_XFLAG_PROJINHERIT	0x00000200
30
-#endif
31
-#ifndef FS_IOC_FSGETXATTR
32
-#define FS_IOC_FSGETXATTR		_IOR ('X', 31, struct fsxattr)
33
-#endif
34
-#ifndef FS_IOC_FSSETXATTR
35
-#define FS_IOC_FSSETXATTR		_IOW ('X', 32, struct fsxattr)
36
-#endif
37
-
38
-#ifndef PRJQUOTA
39
-#define PRJQUOTA	2
40
-#endif
41
-#ifndef XFS_PROJ_QUOTA
42
-#define XFS_PROJ_QUOTA	2
43
-#endif
44
-#ifndef Q_XSETPQLIM
45
-#define Q_XSETPQLIM QCMD(Q_XSETQLIM, PRJQUOTA)
46
-#endif
47
-#ifndef Q_XGETPQUOTA
48
-#define Q_XGETPQUOTA QCMD(Q_XGETQUOTA, PRJQUOTA)
49
-#endif
50
-
51
-const int Q_XGETQSTAT_PRJQUOTA = QCMD(Q_XGETQSTAT, PRJQUOTA);
52
-*/
53
-import "C"
54
-import (
55
-	"io/ioutil"
56
-	"path"
57
-	"path/filepath"
58
-	"sync"
59
-	"unsafe"
60
-
61
-	"github.com/containerd/containerd/sys"
62
-	"github.com/pkg/errors"
63
-	"github.com/sirupsen/logrus"
64
-	"golang.org/x/sys/unix"
65
-)
66
-
67
-type pquotaState struct {
68
-	sync.Mutex
69
-	nextProjectID uint32
70
-}
71
-
72
-var pquotaStateInst *pquotaState
73
-var pquotaStateOnce sync.Once
74
-
75
-// getPquotaState - get global pquota state tracker instance
76
-func getPquotaState() *pquotaState {
77
-	pquotaStateOnce.Do(func() {
78
-		pquotaStateInst = &pquotaState{
79
-			nextProjectID: 1,
80
-		}
81
-	})
82
-	return pquotaStateInst
83
-}
84
-
85
-// registerBasePath - register a new base path and update nextProjectID
86
-func (state *pquotaState) updateMinProjID(minProjectID uint32) {
87
-	state.Lock()
88
-	defer state.Unlock()
89
-	if state.nextProjectID <= minProjectID {
90
-		state.nextProjectID = minProjectID + 1
91
-	}
92
-}
93
-
94
-// NewControl - initialize project quota support.
95
-// Test to make sure that quota can be set on a test dir and find
96
-// the first project id to be used for the next container create.
97
-//
98
-// Returns nil (and error) if project quota is not supported.
99
-//
100
-// First get the project id of the home directory.
101
-// This test will fail if the backing fs is not xfs.
102
-//
103
-// xfs_quota tool can be used to assign a project id to the driver home directory, e.g.:
104
-//    echo 999:/var/lib/docker/overlay2 >> /etc/projects
105
-//    echo docker:999 >> /etc/projid
106
-//    xfs_quota -x -c 'project -s docker' /<xfs mount point>
107
-//
108
-// In that case, the home directory project id will be used as a "start offset"
109
-// and all containers will be assigned larger project ids (e.g. >= 1000).
110
-// This is a way to prevent xfs_quota management from conflicting with docker.
111
-//
112
-// Then try to create a test directory with the next project id and set a quota
113
-// on it. If that works, continue to scan existing containers to map allocated
114
-// project ids.
115
-//
116
-func NewControl(basePath string) (*Control, error) {
117
-	//
118
-	// If we are running in a user namespace quota won't be supported for
119
-	// now since makeBackingFsDev() will try to mknod().
120
-	//
121
-	if sys.RunningInUserNS() {
122
-		return nil, ErrQuotaNotSupported
123
-	}
124
-
125
-	//
126
-	// create backing filesystem device node
127
-	//
128
-	backingFsBlockDev, err := makeBackingFsDev(basePath)
129
-	if err != nil {
130
-		return nil, err
131
-	}
132
-
133
-	// check if we can call quotactl with project quotas
134
-	// as a mechanism to determine (early) if we have support
135
-	hasQuotaSupport, err := hasQuotaSupport(backingFsBlockDev)
136
-	if err != nil {
137
-		return nil, err
138
-	}
139
-	if !hasQuotaSupport {
140
-		return nil, ErrQuotaNotSupported
141
-	}
142
-
143
-	//
144
-	// Get project id of parent dir as minimal id to be used by driver
145
-	//
146
-	baseProjectID, err := getProjectID(basePath)
147
-	if err != nil {
148
-		return nil, err
149
-	}
150
-	minProjectID := baseProjectID + 1
151
-
152
-	//
153
-	// Test if filesystem supports project quotas by trying to set
154
-	// a quota on the first available project id
155
-	//
156
-	quota := Quota{
157
-		Size: 0,
158
-	}
159
-	if err := setProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil {
160
-		return nil, err
161
-	}
162
-
163
-	q := Control{
164
-		backingFsBlockDev: backingFsBlockDev,
165
-		quotas:            make(map[string]uint32),
166
-	}
167
-
168
-	//
169
-	// update minimum project ID
170
-	//
171
-	state := getPquotaState()
172
-	state.updateMinProjID(minProjectID)
173
-
174
-	//
175
-	// get first project id to be used for next container
176
-	//
177
-	err = q.findNextProjectID(basePath, baseProjectID)
178
-	if err != nil {
179
-		return nil, err
180
-	}
181
-
182
-	logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, state.nextProjectID)
183
-	return &q, nil
184
-}
185
-
186
-// SetQuota - assign a unique project id to directory and set the quota limits
187
-// for that project id
188
-func (q *Control) SetQuota(targetPath string, quota Quota) error {
189
-	q.RLock()
190
-	projectID, ok := q.quotas[targetPath]
191
-	q.RUnlock()
192
-	if !ok {
193
-		state := getPquotaState()
194
-		state.Lock()
195
-		projectID = state.nextProjectID
196
-
197
-		//
198
-		// assign project id to new container directory
199
-		//
200
-		err := setProjectID(targetPath, projectID)
201
-		if err != nil {
202
-			state.Unlock()
203
-			return err
204
-		}
205
-
206
-		state.nextProjectID++
207
-		state.Unlock()
208
-
209
-		q.Lock()
210
-		q.quotas[targetPath] = projectID
211
-		q.Unlock()
212
-	}
213
-
214
-	//
215
-	// set the quota limit for the container's project id
216
-	//
217
-	logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
218
-	return setProjectQuota(q.backingFsBlockDev, projectID, quota)
219
-}
220
-
221
-// setProjectQuota - set the quota for project id on xfs block device
222
-func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
223
-	var d C.fs_disk_quota_t
224
-	d.d_version = C.FS_DQUOT_VERSION
225
-	d.d_id = C.__u32(projectID)
226
-	d.d_flags = C.XFS_PROJ_QUOTA
227
-
228
-	d.d_fieldmask = C.FS_DQ_BHARD | C.FS_DQ_BSOFT
229
-	d.d_blk_hardlimit = C.__u64(quota.Size / 512)
230
-	d.d_blk_softlimit = d.d_blk_hardlimit
231
-
232
-	var cs = C.CString(backingFsBlockDev)
233
-	defer C.free(unsafe.Pointer(cs))
234
-
235
-	_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XSETPQLIM,
236
-		uintptr(unsafe.Pointer(cs)), uintptr(d.d_id),
237
-		uintptr(unsafe.Pointer(&d)), 0, 0)
238
-	if errno != 0 {
239
-		return errors.Wrapf(errno, "failed to set quota limit for projid %d on %s",
240
-			projectID, backingFsBlockDev)
241
-	}
242
-
243
-	return nil
244
-}
245
-
246
-// GetQuota - get the quota limits of a directory that was configured with SetQuota
247
-func (q *Control) GetQuota(targetPath string, quota *Quota) error {
248
-	q.RLock()
249
-	projectID, ok := q.quotas[targetPath]
250
-	q.RUnlock()
251
-	if !ok {
252
-		return errors.Errorf("quota not found for path: %s", targetPath)
253
-	}
254
-
255
-	//
256
-	// get the quota limit for the container's project id
257
-	//
258
-	var d C.fs_disk_quota_t
259
-
260
-	var cs = C.CString(q.backingFsBlockDev)
261
-	defer C.free(unsafe.Pointer(cs))
262
-
263
-	_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQUOTA,
264
-		uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)),
265
-		uintptr(unsafe.Pointer(&d)), 0, 0)
266
-	if errno != 0 {
267
-		return errors.Wrapf(errno, "Failed to get quota limit for projid %d on %s",
268
-			projectID, q.backingFsBlockDev)
269
-	}
270
-	quota.Size = uint64(d.d_blk_hardlimit) * 512
271
-
272
-	return nil
273
-}
274
-
275
-// getProjectID - get the project id of path on xfs
276
-func getProjectID(targetPath string) (uint32, error) {
277
-	dir, err := openDir(targetPath)
278
-	if err != nil {
279
-		return 0, err
280
-	}
281
-	defer closeDir(dir)
282
-
283
-	var fsx C.struct_fsxattr
284
-	_, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR,
285
-		uintptr(unsafe.Pointer(&fsx)))
286
-	if errno != 0 {
287
-		return 0, errors.Wrapf(errno, "failed to get projid for %s", targetPath)
288
-	}
289
-
290
-	return uint32(fsx.fsx_projid), nil
291
-}
292
-
293
-// setProjectID - set the project id of path on xfs
294
-func setProjectID(targetPath string, projectID uint32) error {
295
-	dir, err := openDir(targetPath)
296
-	if err != nil {
297
-		return err
298
-	}
299
-	defer closeDir(dir)
300
-
301
-	var fsx C.struct_fsxattr
302
-	_, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR,
303
-		uintptr(unsafe.Pointer(&fsx)))
304
-	if errno != 0 {
305
-		return errors.Wrapf(errno, "failed to get projid for %s", targetPath)
306
-	}
307
-	fsx.fsx_projid = C.__u32(projectID)
308
-	fsx.fsx_xflags |= C.FS_XFLAG_PROJINHERIT
309
-	_, _, errno = unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSSETXATTR,
310
-		uintptr(unsafe.Pointer(&fsx)))
311
-	if errno != 0 {
312
-		return errors.Wrapf(errno, "failed to set projid for %s", targetPath)
313
-	}
314
-
315
-	return nil
316
-}
317
-
318
-// findNextProjectID - find the next project id to be used for containers
319
-// by scanning driver home directory to find used project ids
320
-func (q *Control) findNextProjectID(home string, baseID uint32) error {
321
-	state := getPquotaState()
322
-	state.Lock()
323
-	defer state.Unlock()
324
-
325
-	checkProjID := func(path string) (uint32, error) {
326
-		projid, err := getProjectID(path)
327
-		if err != nil {
328
-			return projid, err
329
-		}
330
-		if projid > 0 {
331
-			q.quotas[path] = projid
332
-		}
333
-		if state.nextProjectID <= projid {
334
-			state.nextProjectID = projid + 1
335
-		}
336
-		return projid, nil
337
-	}
338
-
339
-	files, err := ioutil.ReadDir(home)
340
-	if err != nil {
341
-		return errors.Errorf("read directory failed: %s", home)
342
-	}
343
-	for _, file := range files {
344
-		if !file.IsDir() {
345
-			continue
346
-		}
347
-		path := filepath.Join(home, file.Name())
348
-		projid, err := checkProjID(path)
349
-		if err != nil {
350
-			return err
351
-		}
352
-		if projid > 0 && projid != baseID {
353
-			continue
354
-		}
355
-		subfiles, err := ioutil.ReadDir(path)
356
-		if err != nil {
357
-			return errors.Errorf("read directory failed: %s", path)
358
-		}
359
-		for _, subfile := range subfiles {
360
-			if !subfile.IsDir() {
361
-				continue
362
-			}
363
-			subpath := filepath.Join(path, subfile.Name())
364
-			_, err := checkProjID(subpath)
365
-			if err != nil {
366
-				return err
367
-			}
368
-		}
369
-	}
370
-
371
-	return nil
372
-}
373
-
374
-func free(p *C.char) {
375
-	C.free(unsafe.Pointer(p))
376
-}
377
-
378
-func openDir(path string) (*C.DIR, error) {
379
-	Cpath := C.CString(path)
380
-	defer free(Cpath)
381
-
382
-	dir := C.opendir(Cpath)
383
-	if dir == nil {
384
-		return nil, errors.Errorf("failed to open dir: %s", path)
385
-	}
386
-	return dir, nil
387
-}
388
-
389
-func closeDir(dir *C.DIR) {
390
-	if dir != nil {
391
-		C.closedir(dir)
392
-	}
393
-}
394
-
395
-func getDirFd(dir *C.DIR) uintptr {
396
-	return uintptr(C.dirfd(dir))
397
-}
398
-
399
-// Get the backing block device of the driver home directory
400
-// and create a block device node under the home directory
401
-// to be used by quotactl commands
402
-func makeBackingFsDev(home string) (string, error) {
403
-	var stat unix.Stat_t
404
-	if err := unix.Stat(home, &stat); err != nil {
405
-		return "", err
406
-	}
407
-
408
-	backingFsBlockDev := path.Join(home, "backingFsBlockDev")
409
-	// Re-create just in case someone copied the home directory over to a new device
410
-	unix.Unlink(backingFsBlockDev)
411
-	err := unix.Mknod(backingFsBlockDev, unix.S_IFBLK|0600, int(stat.Dev))
412
-	switch err {
413
-	case nil:
414
-		return backingFsBlockDev, nil
415
-
416
-	case unix.ENOSYS, unix.EPERM:
417
-		return "", ErrQuotaNotSupported
418
-
419
-	default:
420
-		return "", errors.Wrapf(err, "failed to mknod %s", backingFsBlockDev)
421
-	}
422
-}
423
-
424
-func hasQuotaSupport(backingFsBlockDev string) (bool, error) {
425
-	var cs = C.CString(backingFsBlockDev)
426
-	defer free(cs)
427
-	var qstat C.fs_quota_stat_t
428
-
429
-	_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, uintptr(C.Q_XGETQSTAT_PRJQUOTA), uintptr(unsafe.Pointer(cs)), 0, uintptr(unsafe.Pointer(&qstat)), 0, 0)
430
-	if errno == 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ENFD > 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ACCT > 0 {
431
-		return true, nil
432
-	}
433
-
434
-	switch errno {
435
-	// These are the known fatal errors, consider all other errors (ENOTTY, etc.. not supporting quota)
436
-	case unix.EFAULT, unix.ENOENT, unix.ENOTBLK, unix.EPERM:
437
-	default:
438
-		return false, nil
439
-	}
440
-
441
-	return false, errno
442
-}
443 1
deleted file mode 100644
... ...
@@ -1,152 +0,0 @@
1
-// +build linux
2
-
3
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
4
-
5
-import (
6
-	"io"
7
-	"io/ioutil"
8
-	"os"
9
-	"os/exec"
10
-	"path/filepath"
11
-	"testing"
12
-
13
-	"golang.org/x/sys/unix"
14
-	"gotest.tools/v3/assert"
15
-	is "gotest.tools/v3/assert/cmp"
16
-	"gotest.tools/v3/fs"
17
-)
18
-
19
-// 10MB
20
-const testQuotaSize = 10 * 1024 * 1024
21
-const imageSize = 64 * 1024 * 1024
22
-
23
-func TestBlockDev(t *testing.T) {
24
-	mkfs, err := exec.LookPath("mkfs.xfs")
25
-	if err != nil {
26
-		t.Skip("mkfs.xfs not found in PATH")
27
-	}
28
-
29
-	// create a sparse image
30
-	imageFile, err := ioutil.TempFile("", "xfs-image")
31
-	if err != nil {
32
-		t.Fatal(err)
33
-	}
34
-	imageFileName := imageFile.Name()
35
-	defer os.Remove(imageFileName)
36
-	if _, err = imageFile.Seek(imageSize-1, 0); err != nil {
37
-		t.Fatal(err)
38
-	}
39
-	if _, err = imageFile.Write([]byte{0}); err != nil {
40
-		t.Fatal(err)
41
-	}
42
-	if err = imageFile.Close(); err != nil {
43
-		t.Fatal(err)
44
-	}
45
-
46
-	// The reason for disabling these options is sometimes people run with a newer userspace
47
-	// than kernelspace
48
-	out, err := exec.Command(mkfs, "-m", "crc=0,finobt=0", imageFileName).CombinedOutput()
49
-	if len(out) > 0 {
50
-		t.Log(string(out))
51
-	}
52
-	if err != nil {
53
-		t.Fatal(err)
54
-	}
55
-
56
-	t.Run("testBlockDevQuotaDisabled", wrapMountTest(imageFileName, false, testBlockDevQuotaDisabled))
57
-	t.Run("testBlockDevQuotaEnabled", wrapMountTest(imageFileName, true, testBlockDevQuotaEnabled))
58
-	t.Run("testSmallerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testSmallerThanQuota)))
59
-	t.Run("testBiggerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testBiggerThanQuota)))
60
-	t.Run("testRetrieveQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testRetrieveQuota)))
61
-}
62
-
63
-func wrapMountTest(imageFileName string, enableQuota bool, testFunc func(t *testing.T, mountPoint, backingFsDev string)) func(*testing.T) {
64
-	return func(t *testing.T) {
65
-		mountOptions := "loop"
66
-
67
-		if enableQuota {
68
-			mountOptions = mountOptions + ",prjquota"
69
-		}
70
-
71
-		mountPointDir := fs.NewDir(t, "xfs-mountPoint")
72
-		defer mountPointDir.Remove()
73
-		mountPoint := mountPointDir.Path()
74
-
75
-		out, err := exec.Command("mount", "-o", mountOptions, imageFileName, mountPoint).CombinedOutput()
76
-		if err != nil {
77
-			_, err := os.Stat("/proc/fs/xfs")
78
-			if os.IsNotExist(err) {
79
-				t.Skip("no /proc/fs/xfs")
80
-			}
81
-		}
82
-
83
-		assert.NilError(t, err, "mount failed: %s", out)
84
-
85
-		defer func() {
86
-			assert.NilError(t, unix.Unmount(mountPoint, 0))
87
-		}()
88
-
89
-		backingFsDev, err := makeBackingFsDev(mountPoint)
90
-		assert.NilError(t, err)
91
-
92
-		testFunc(t, mountPoint, backingFsDev)
93
-	}
94
-}
95
-
96
-func testBlockDevQuotaDisabled(t *testing.T, mountPoint, backingFsDev string) {
97
-	hasSupport, err := hasQuotaSupport(backingFsDev)
98
-	assert.NilError(t, err)
99
-	assert.Check(t, !hasSupport)
100
-}
101
-
102
-func testBlockDevQuotaEnabled(t *testing.T, mountPoint, backingFsDev string) {
103
-	hasSupport, err := hasQuotaSupport(backingFsDev)
104
-	assert.NilError(t, err)
105
-	assert.Check(t, hasSupport)
106
-}
107
-
108
-func wrapQuotaTest(testFunc func(t *testing.T, ctrl *Control, mountPoint, testDir, testSubDir string)) func(t *testing.T, mountPoint, backingFsDev string) {
109
-	return func(t *testing.T, mountPoint, backingFsDev string) {
110
-		testDir, err := ioutil.TempDir(mountPoint, "per-test")
111
-		assert.NilError(t, err)
112
-		defer os.RemoveAll(testDir)
113
-
114
-		ctrl, err := NewControl(testDir)
115
-		assert.NilError(t, err)
116
-
117
-		testSubDir, err := ioutil.TempDir(testDir, "quota-test")
118
-		assert.NilError(t, err)
119
-		testFunc(t, ctrl, mountPoint, testDir, testSubDir)
120
-	}
121
-
122
-}
123
-
124
-func testSmallerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
125
-	assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
126
-	smallerThanQuotaFile := filepath.Join(testSubDir, "smaller-than-quota")
127
-	assert.NilError(t, ioutil.WriteFile(smallerThanQuotaFile, make([]byte, testQuotaSize/2), 0644))
128
-	assert.NilError(t, os.Remove(smallerThanQuotaFile))
129
-}
130
-
131
-func testBiggerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
132
-	// Make sure the quota is being enforced
133
-	// TODO: When we implement this under EXT4, we need to shed CAP_SYS_RESOURCE, otherwise
134
-	// we're able to violate quota without issue
135
-	assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
136
-
137
-	biggerThanQuotaFile := filepath.Join(testSubDir, "bigger-than-quota")
138
-	err := ioutil.WriteFile(biggerThanQuotaFile, make([]byte, testQuotaSize+1), 0644)
139
-	assert.Assert(t, is.ErrorContains(err, ""))
140
-	if err == io.ErrShortWrite {
141
-		assert.NilError(t, os.Remove(biggerThanQuotaFile))
142
-	}
143
-}
144
-
145
-func testRetrieveQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
146
-	// Validate that we can retrieve quota
147
-	assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
148
-
149
-	var q Quota
150
-	assert.NilError(t, ctrl.GetQuota(testSubDir, &q))
151
-	assert.Check(t, is.Equal(uint64(testQuotaSize), q.Size))
152
-}
153 1
deleted file mode 100644
... ...
@@ -1,18 +0,0 @@
1
-// +build linux,exclude_disk_quota linux,!cgo !linux
2
-
3
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
4
-
5
-func NewControl(basePath string) (*Control, error) {
6
-	return nil, ErrQuotaNotSupported
7
-}
8
-
9
-// SetQuota - assign a unique project id to directory and set the quota limits
10
-// for that project id
11
-func (q *Control) SetQuota(targetPath string, quota Quota) error {
12
-	return ErrQuotaNotSupported
13
-}
14
-
15
-// GetQuota - get the quota limits of a directory that was configured with SetQuota
16
-func (q *Control) GetQuota(targetPath string, quota *Quota) error {
17
-	return ErrQuotaNotSupported
18
-}
19 1
deleted file mode 100644
... ...
@@ -1,16 +0,0 @@
1
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
2
-
3
-import "sync"
4
-
5
-// Quota limit params - currently we only control blocks hard limit
6
-type Quota struct {
7
-	Size uint64
8
-}
9
-
10
-// Control - Context to be used by storage driver (e.g. overlay)
11
-// who wants to apply project quotas to container dirs
12
-type Control struct {
13
-	backingFsBlockDev string
14
-	sync.RWMutex      // protect nextProjectID and quotas map
15
-	quotas            map[string]uint32
16
-}
... ...
@@ -6,12 +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"
10 9
 	"github.com/docker/docker/errdefs"
11 10
 	"github.com/docker/docker/pkg/containerfs"
12 11
 	"github.com/docker/docker/pkg/idtools"
13 12
 	"github.com/docker/docker/pkg/parsers"
14 13
 	"github.com/docker/docker/pkg/system"
14
+	"github.com/docker/docker/quota"
15 15
 	units "github.com/docker/go-units"
16 16
 	"github.com/opencontainers/selinux/go-selinux/label"
17 17
 	"github.com/pkg/errors"
... ...
@@ -1,7 +1,7 @@
1 1
 package vfs // import "github.com/docker/docker/daemon/graphdriver/vfs"
2 2
 
3 3
 import (
4
-	"github.com/docker/docker/daemon/graphdriver/quota"
4
+	"github.com/docker/docker/quota"
5 5
 	"github.com/sirupsen/logrus"
6 6
 )
7 7
 
... ...
@@ -2,7 +2,7 @@
2 2
 
3 3
 package vfs // import "github.com/docker/docker/daemon/graphdriver/vfs"
4 4
 
5
-import "github.com/docker/docker/daemon/graphdriver/quota"
5
+import "github.com/docker/docker/quota"
6 6
 
7 7
 type driverQuota struct {
8 8
 }
9 9
new file mode 100644
... ...
@@ -0,0 +1,19 @@
0
+package quota // import "github.com/docker/docker/quota"
1
+
2
+import "github.com/docker/docker/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
+}
0 19
new file mode 100644
... ...
@@ -0,0 +1,442 @@
0
+// +build linux,!exclude_disk_quota,cgo
1
+
2
+//
3
+// projectquota.go - implements XFS project quota controls
4
+// for setting quota limits on a newly created directory.
5
+// It currently supports the legacy XFS specific ioctls.
6
+//
7
+// TODO: use generic quota control ioctl FS_IOC_FS{GET,SET}XATTR
8
+//       for both xfs/ext4 for kernel version >= v4.5
9
+//
10
+
11
+package quota // import "github.com/docker/docker/quota"
12
+
13
+/*
14
+#include <stdlib.h>
15
+#include <dirent.h>
16
+#include <linux/fs.h>
17
+#include <linux/quota.h>
18
+#include <linux/dqblk_xfs.h>
19
+
20
+#ifndef FS_XFLAG_PROJINHERIT
21
+struct fsxattr {
22
+	__u32		fsx_xflags;
23
+	__u32		fsx_extsize;
24
+	__u32		fsx_nextents;
25
+	__u32		fsx_projid;
26
+	unsigned char	fsx_pad[12];
27
+};
28
+#define FS_XFLAG_PROJINHERIT	0x00000200
29
+#endif
30
+#ifndef FS_IOC_FSGETXATTR
31
+#define FS_IOC_FSGETXATTR		_IOR ('X', 31, struct fsxattr)
32
+#endif
33
+#ifndef FS_IOC_FSSETXATTR
34
+#define FS_IOC_FSSETXATTR		_IOW ('X', 32, struct fsxattr)
35
+#endif
36
+
37
+#ifndef PRJQUOTA
38
+#define PRJQUOTA	2
39
+#endif
40
+#ifndef XFS_PROJ_QUOTA
41
+#define XFS_PROJ_QUOTA	2
42
+#endif
43
+#ifndef Q_XSETPQLIM
44
+#define Q_XSETPQLIM QCMD(Q_XSETQLIM, PRJQUOTA)
45
+#endif
46
+#ifndef Q_XGETPQUOTA
47
+#define Q_XGETPQUOTA QCMD(Q_XGETQUOTA, PRJQUOTA)
48
+#endif
49
+
50
+const int Q_XGETQSTAT_PRJQUOTA = QCMD(Q_XGETQSTAT, PRJQUOTA);
51
+*/
52
+import "C"
53
+import (
54
+	"io/ioutil"
55
+	"path"
56
+	"path/filepath"
57
+	"sync"
58
+	"unsafe"
59
+
60
+	"github.com/containerd/containerd/sys"
61
+	"github.com/pkg/errors"
62
+	"github.com/sirupsen/logrus"
63
+	"golang.org/x/sys/unix"
64
+)
65
+
66
+type pquotaState struct {
67
+	sync.Mutex
68
+	nextProjectID uint32
69
+}
70
+
71
+var pquotaStateInst *pquotaState
72
+var pquotaStateOnce sync.Once
73
+
74
+// getPquotaState - get global pquota state tracker instance
75
+func getPquotaState() *pquotaState {
76
+	pquotaStateOnce.Do(func() {
77
+		pquotaStateInst = &pquotaState{
78
+			nextProjectID: 1,
79
+		}
80
+	})
81
+	return pquotaStateInst
82
+}
83
+
84
+// registerBasePath - register a new base path and update nextProjectID
85
+func (state *pquotaState) updateMinProjID(minProjectID uint32) {
86
+	state.Lock()
87
+	defer state.Unlock()
88
+	if state.nextProjectID <= minProjectID {
89
+		state.nextProjectID = minProjectID + 1
90
+	}
91
+}
92
+
93
+// NewControl - initialize project quota support.
94
+// Test to make sure that quota can be set on a test dir and find
95
+// the first project id to be used for the next container create.
96
+//
97
+// Returns nil (and error) if project quota is not supported.
98
+//
99
+// First get the project id of the home directory.
100
+// This test will fail if the backing fs is not xfs.
101
+//
102
+// xfs_quota tool can be used to assign a project id to the driver home directory, e.g.:
103
+//    echo 999:/var/lib/docker/overlay2 >> /etc/projects
104
+//    echo docker:999 >> /etc/projid
105
+//    xfs_quota -x -c 'project -s docker' /<xfs mount point>
106
+//
107
+// In that case, the home directory project id will be used as a "start offset"
108
+// and all containers will be assigned larger project ids (e.g. >= 1000).
109
+// This is a way to prevent xfs_quota management from conflicting with docker.
110
+//
111
+// Then try to create a test directory with the next project id and set a quota
112
+// on it. If that works, continue to scan existing containers to map allocated
113
+// project ids.
114
+//
115
+func NewControl(basePath string) (*Control, error) {
116
+	//
117
+	// If we are running in a user namespace quota won't be supported for
118
+	// now since makeBackingFsDev() will try to mknod().
119
+	//
120
+	if sys.RunningInUserNS() {
121
+		return nil, ErrQuotaNotSupported
122
+	}
123
+
124
+	//
125
+	// create backing filesystem device node
126
+	//
127
+	backingFsBlockDev, err := makeBackingFsDev(basePath)
128
+	if err != nil {
129
+		return nil, err
130
+	}
131
+
132
+	// check if we can call quotactl with project quotas
133
+	// as a mechanism to determine (early) if we have support
134
+	hasQuotaSupport, err := hasQuotaSupport(backingFsBlockDev)
135
+	if err != nil {
136
+		return nil, err
137
+	}
138
+	if !hasQuotaSupport {
139
+		return nil, ErrQuotaNotSupported
140
+	}
141
+
142
+	//
143
+	// Get project id of parent dir as minimal id to be used by driver
144
+	//
145
+	baseProjectID, err := getProjectID(basePath)
146
+	if err != nil {
147
+		return nil, err
148
+	}
149
+	minProjectID := baseProjectID + 1
150
+
151
+	//
152
+	// Test if filesystem supports project quotas by trying to set
153
+	// a quota on the first available project id
154
+	//
155
+	quota := Quota{
156
+		Size: 0,
157
+	}
158
+	if err := setProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil {
159
+		return nil, err
160
+	}
161
+
162
+	q := Control{
163
+		backingFsBlockDev: backingFsBlockDev,
164
+		quotas:            make(map[string]uint32),
165
+	}
166
+
167
+	//
168
+	// update minimum project ID
169
+	//
170
+	state := getPquotaState()
171
+	state.updateMinProjID(minProjectID)
172
+
173
+	//
174
+	// get first project id to be used for next container
175
+	//
176
+	err = q.findNextProjectID(basePath, baseProjectID)
177
+	if err != nil {
178
+		return nil, err
179
+	}
180
+
181
+	logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, state.nextProjectID)
182
+	return &q, nil
183
+}
184
+
185
+// SetQuota - assign a unique project id to directory and set the quota limits
186
+// for that project id
187
+func (q *Control) SetQuota(targetPath string, quota Quota) error {
188
+	q.RLock()
189
+	projectID, ok := q.quotas[targetPath]
190
+	q.RUnlock()
191
+	if !ok {
192
+		state := getPquotaState()
193
+		state.Lock()
194
+		projectID = state.nextProjectID
195
+
196
+		//
197
+		// assign project id to new container directory
198
+		//
199
+		err := setProjectID(targetPath, projectID)
200
+		if err != nil {
201
+			state.Unlock()
202
+			return err
203
+		}
204
+
205
+		state.nextProjectID++
206
+		state.Unlock()
207
+
208
+		q.Lock()
209
+		q.quotas[targetPath] = projectID
210
+		q.Unlock()
211
+	}
212
+
213
+	//
214
+	// set the quota limit for the container's project id
215
+	//
216
+	logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
217
+	return setProjectQuota(q.backingFsBlockDev, projectID, quota)
218
+}
219
+
220
+// setProjectQuota - set the quota for project id on xfs block device
221
+func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
222
+	var d C.fs_disk_quota_t
223
+	d.d_version = C.FS_DQUOT_VERSION
224
+	d.d_id = C.__u32(projectID)
225
+	d.d_flags = C.XFS_PROJ_QUOTA
226
+
227
+	d.d_fieldmask = C.FS_DQ_BHARD | C.FS_DQ_BSOFT
228
+	d.d_blk_hardlimit = C.__u64(quota.Size / 512)
229
+	d.d_blk_softlimit = d.d_blk_hardlimit
230
+
231
+	var cs = C.CString(backingFsBlockDev)
232
+	defer C.free(unsafe.Pointer(cs))
233
+
234
+	_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XSETPQLIM,
235
+		uintptr(unsafe.Pointer(cs)), uintptr(d.d_id),
236
+		uintptr(unsafe.Pointer(&d)), 0, 0)
237
+	if errno != 0 {
238
+		return errors.Wrapf(errno, "failed to set quota limit for projid %d on %s",
239
+			projectID, backingFsBlockDev)
240
+	}
241
+
242
+	return nil
243
+}
244
+
245
+// GetQuota - get the quota limits of a directory that was configured with SetQuota
246
+func (q *Control) GetQuota(targetPath string, quota *Quota) error {
247
+	q.RLock()
248
+	projectID, ok := q.quotas[targetPath]
249
+	q.RUnlock()
250
+	if !ok {
251
+		return errors.Errorf("quota not found for path: %s", targetPath)
252
+	}
253
+
254
+	//
255
+	// get the quota limit for the container's project id
256
+	//
257
+	var d C.fs_disk_quota_t
258
+
259
+	var cs = C.CString(q.backingFsBlockDev)
260
+	defer C.free(unsafe.Pointer(cs))
261
+
262
+	_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQUOTA,
263
+		uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)),
264
+		uintptr(unsafe.Pointer(&d)), 0, 0)
265
+	if errno != 0 {
266
+		return errors.Wrapf(errno, "Failed to get quota limit for projid %d on %s",
267
+			projectID, q.backingFsBlockDev)
268
+	}
269
+	quota.Size = uint64(d.d_blk_hardlimit) * 512
270
+
271
+	return nil
272
+}
273
+
274
+// getProjectID - get the project id of path on xfs
275
+func getProjectID(targetPath string) (uint32, error) {
276
+	dir, err := openDir(targetPath)
277
+	if err != nil {
278
+		return 0, err
279
+	}
280
+	defer closeDir(dir)
281
+
282
+	var fsx C.struct_fsxattr
283
+	_, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR,
284
+		uintptr(unsafe.Pointer(&fsx)))
285
+	if errno != 0 {
286
+		return 0, errors.Wrapf(errno, "failed to get projid for %s", targetPath)
287
+	}
288
+
289
+	return uint32(fsx.fsx_projid), nil
290
+}
291
+
292
+// setProjectID - set the project id of path on xfs
293
+func setProjectID(targetPath string, projectID uint32) error {
294
+	dir, err := openDir(targetPath)
295
+	if err != nil {
296
+		return err
297
+	}
298
+	defer closeDir(dir)
299
+
300
+	var fsx C.struct_fsxattr
301
+	_, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR,
302
+		uintptr(unsafe.Pointer(&fsx)))
303
+	if errno != 0 {
304
+		return errors.Wrapf(errno, "failed to get projid for %s", targetPath)
305
+	}
306
+	fsx.fsx_projid = C.__u32(projectID)
307
+	fsx.fsx_xflags |= C.FS_XFLAG_PROJINHERIT
308
+	_, _, errno = unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSSETXATTR,
309
+		uintptr(unsafe.Pointer(&fsx)))
310
+	if errno != 0 {
311
+		return errors.Wrapf(errno, "failed to set projid for %s", targetPath)
312
+	}
313
+
314
+	return nil
315
+}
316
+
317
+// findNextProjectID - find the next project id to be used for containers
318
+// by scanning driver home directory to find used project ids
319
+func (q *Control) findNextProjectID(home string, baseID uint32) error {
320
+	state := getPquotaState()
321
+	state.Lock()
322
+	defer state.Unlock()
323
+
324
+	checkProjID := func(path string) (uint32, error) {
325
+		projid, err := getProjectID(path)
326
+		if err != nil {
327
+			return projid, err
328
+		}
329
+		if projid > 0 {
330
+			q.quotas[path] = projid
331
+		}
332
+		if state.nextProjectID <= projid {
333
+			state.nextProjectID = projid + 1
334
+		}
335
+		return projid, nil
336
+	}
337
+
338
+	files, err := ioutil.ReadDir(home)
339
+	if err != nil {
340
+		return errors.Errorf("read directory failed: %s", home)
341
+	}
342
+	for _, file := range files {
343
+		if !file.IsDir() {
344
+			continue
345
+		}
346
+		path := filepath.Join(home, file.Name())
347
+		projid, err := checkProjID(path)
348
+		if err != nil {
349
+			return err
350
+		}
351
+		if projid > 0 && projid != baseID {
352
+			continue
353
+		}
354
+		subfiles, err := ioutil.ReadDir(path)
355
+		if err != nil {
356
+			return errors.Errorf("read directory failed: %s", path)
357
+		}
358
+		for _, subfile := range subfiles {
359
+			if !subfile.IsDir() {
360
+				continue
361
+			}
362
+			subpath := filepath.Join(path, subfile.Name())
363
+			_, err := checkProjID(subpath)
364
+			if err != nil {
365
+				return err
366
+			}
367
+		}
368
+	}
369
+
370
+	return nil
371
+}
372
+
373
+func free(p *C.char) {
374
+	C.free(unsafe.Pointer(p))
375
+}
376
+
377
+func openDir(path string) (*C.DIR, error) {
378
+	Cpath := C.CString(path)
379
+	defer free(Cpath)
380
+
381
+	dir := C.opendir(Cpath)
382
+	if dir == nil {
383
+		return nil, errors.Errorf("failed to open dir: %s", path)
384
+	}
385
+	return dir, nil
386
+}
387
+
388
+func closeDir(dir *C.DIR) {
389
+	if dir != nil {
390
+		C.closedir(dir)
391
+	}
392
+}
393
+
394
+func getDirFd(dir *C.DIR) uintptr {
395
+	return uintptr(C.dirfd(dir))
396
+}
397
+
398
+// Get the backing block device of the driver home directory
399
+// and create a block device node under the home directory
400
+// to be used by quotactl commands
401
+func makeBackingFsDev(home string) (string, error) {
402
+	var stat unix.Stat_t
403
+	if err := unix.Stat(home, &stat); err != nil {
404
+		return "", err
405
+	}
406
+
407
+	backingFsBlockDev := path.Join(home, "backingFsBlockDev")
408
+	// Re-create just in case someone copied the home directory over to a new device
409
+	unix.Unlink(backingFsBlockDev)
410
+	err := unix.Mknod(backingFsBlockDev, unix.S_IFBLK|0600, int(stat.Dev))
411
+	switch err {
412
+	case nil:
413
+		return backingFsBlockDev, nil
414
+
415
+	case unix.ENOSYS, unix.EPERM:
416
+		return "", ErrQuotaNotSupported
417
+
418
+	default:
419
+		return "", errors.Wrapf(err, "failed to mknod %s", backingFsBlockDev)
420
+	}
421
+}
422
+
423
+func hasQuotaSupport(backingFsBlockDev string) (bool, error) {
424
+	var cs = C.CString(backingFsBlockDev)
425
+	defer free(cs)
426
+	var qstat C.fs_quota_stat_t
427
+
428
+	_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, uintptr(C.Q_XGETQSTAT_PRJQUOTA), uintptr(unsafe.Pointer(cs)), 0, uintptr(unsafe.Pointer(&qstat)), 0, 0)
429
+	if errno == 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ENFD > 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ACCT > 0 {
430
+		return true, nil
431
+	}
432
+
433
+	switch errno {
434
+	// These are the known fatal errors, consider all other errors (ENOTTY, etc.. not supporting quota)
435
+	case unix.EFAULT, unix.ENOENT, unix.ENOTBLK, unix.EPERM:
436
+	default:
437
+		return false, nil
438
+	}
439
+
440
+	return false, errno
441
+}
0 442
new file mode 100644
... ...
@@ -0,0 +1,152 @@
0
+// +build linux
1
+
2
+package quota // import "github.com/docker/docker/quota"
3
+
4
+import (
5
+	"io"
6
+	"io/ioutil"
7
+	"os"
8
+	"os/exec"
9
+	"path/filepath"
10
+	"testing"
11
+
12
+	"golang.org/x/sys/unix"
13
+	"gotest.tools/v3/assert"
14
+	is "gotest.tools/v3/assert/cmp"
15
+	"gotest.tools/v3/fs"
16
+)
17
+
18
+// 10MB
19
+const testQuotaSize = 10 * 1024 * 1024
20
+const imageSize = 64 * 1024 * 1024
21
+
22
+func TestBlockDev(t *testing.T) {
23
+	mkfs, err := exec.LookPath("mkfs.xfs")
24
+	if err != nil {
25
+		t.Skip("mkfs.xfs not found in PATH")
26
+	}
27
+
28
+	// create a sparse image
29
+	imageFile, err := ioutil.TempFile("", "xfs-image")
30
+	if err != nil {
31
+		t.Fatal(err)
32
+	}
33
+	imageFileName := imageFile.Name()
34
+	defer os.Remove(imageFileName)
35
+	if _, err = imageFile.Seek(imageSize-1, 0); err != nil {
36
+		t.Fatal(err)
37
+	}
38
+	if _, err = imageFile.Write([]byte{0}); err != nil {
39
+		t.Fatal(err)
40
+	}
41
+	if err = imageFile.Close(); err != nil {
42
+		t.Fatal(err)
43
+	}
44
+
45
+	// The reason for disabling these options is sometimes people run with a newer userspace
46
+	// than kernelspace
47
+	out, err := exec.Command(mkfs, "-m", "crc=0,finobt=0", imageFileName).CombinedOutput()
48
+	if len(out) > 0 {
49
+		t.Log(string(out))
50
+	}
51
+	if err != nil {
52
+		t.Fatal(err)
53
+	}
54
+
55
+	t.Run("testBlockDevQuotaDisabled", wrapMountTest(imageFileName, false, testBlockDevQuotaDisabled))
56
+	t.Run("testBlockDevQuotaEnabled", wrapMountTest(imageFileName, true, testBlockDevQuotaEnabled))
57
+	t.Run("testSmallerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testSmallerThanQuota)))
58
+	t.Run("testBiggerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testBiggerThanQuota)))
59
+	t.Run("testRetrieveQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testRetrieveQuota)))
60
+}
61
+
62
+func wrapMountTest(imageFileName string, enableQuota bool, testFunc func(t *testing.T, mountPoint, backingFsDev string)) func(*testing.T) {
63
+	return func(t *testing.T) {
64
+		mountOptions := "loop"
65
+
66
+		if enableQuota {
67
+			mountOptions = mountOptions + ",prjquota"
68
+		}
69
+
70
+		mountPointDir := fs.NewDir(t, "xfs-mountPoint")
71
+		defer mountPointDir.Remove()
72
+		mountPoint := mountPointDir.Path()
73
+
74
+		out, err := exec.Command("mount", "-o", mountOptions, imageFileName, mountPoint).CombinedOutput()
75
+		if err != nil {
76
+			_, err := os.Stat("/proc/fs/xfs")
77
+			if os.IsNotExist(err) {
78
+				t.Skip("no /proc/fs/xfs")
79
+			}
80
+		}
81
+
82
+		assert.NilError(t, err, "mount failed: %s", out)
83
+
84
+		defer func() {
85
+			assert.NilError(t, unix.Unmount(mountPoint, 0))
86
+		}()
87
+
88
+		backingFsDev, err := makeBackingFsDev(mountPoint)
89
+		assert.NilError(t, err)
90
+
91
+		testFunc(t, mountPoint, backingFsDev)
92
+	}
93
+}
94
+
95
+func testBlockDevQuotaDisabled(t *testing.T, mountPoint, backingFsDev string) {
96
+	hasSupport, err := hasQuotaSupport(backingFsDev)
97
+	assert.NilError(t, err)
98
+	assert.Check(t, !hasSupport)
99
+}
100
+
101
+func testBlockDevQuotaEnabled(t *testing.T, mountPoint, backingFsDev string) {
102
+	hasSupport, err := hasQuotaSupport(backingFsDev)
103
+	assert.NilError(t, err)
104
+	assert.Check(t, hasSupport)
105
+}
106
+
107
+func wrapQuotaTest(testFunc func(t *testing.T, ctrl *Control, mountPoint, testDir, testSubDir string)) func(t *testing.T, mountPoint, backingFsDev string) {
108
+	return func(t *testing.T, mountPoint, backingFsDev string) {
109
+		testDir, err := ioutil.TempDir(mountPoint, "per-test")
110
+		assert.NilError(t, err)
111
+		defer os.RemoveAll(testDir)
112
+
113
+		ctrl, err := NewControl(testDir)
114
+		assert.NilError(t, err)
115
+
116
+		testSubDir, err := ioutil.TempDir(testDir, "quota-test")
117
+		assert.NilError(t, err)
118
+		testFunc(t, ctrl, mountPoint, testDir, testSubDir)
119
+	}
120
+
121
+}
122
+
123
+func testSmallerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
124
+	assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
125
+	smallerThanQuotaFile := filepath.Join(testSubDir, "smaller-than-quota")
126
+	assert.NilError(t, ioutil.WriteFile(smallerThanQuotaFile, make([]byte, testQuotaSize/2), 0644))
127
+	assert.NilError(t, os.Remove(smallerThanQuotaFile))
128
+}
129
+
130
+func testBiggerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
131
+	// Make sure the quota is being enforced
132
+	// TODO: When we implement this under EXT4, we need to shed CAP_SYS_RESOURCE, otherwise
133
+	// we're able to violate quota without issue
134
+	assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
135
+
136
+	biggerThanQuotaFile := filepath.Join(testSubDir, "bigger-than-quota")
137
+	err := ioutil.WriteFile(biggerThanQuotaFile, make([]byte, testQuotaSize+1), 0644)
138
+	assert.Assert(t, is.ErrorContains(err, ""))
139
+	if err == io.ErrShortWrite {
140
+		assert.NilError(t, os.Remove(biggerThanQuotaFile))
141
+	}
142
+}
143
+
144
+func testRetrieveQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
145
+	// Validate that we can retrieve quota
146
+	assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
147
+
148
+	var q Quota
149
+	assert.NilError(t, ctrl.GetQuota(testSubDir, &q))
150
+	assert.Check(t, is.Equal(uint64(testQuotaSize), q.Size))
151
+}
0 152
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+// +build linux,exclude_disk_quota linux,!cgo !linux
1
+
2
+package quota // import "github.com/docker/docker/quota"
3
+
4
+func NewControl(basePath string) (*Control, error) {
5
+	return nil, ErrQuotaNotSupported
6
+}
7
+
8
+// SetQuota - assign a unique project id to directory and set the quota limits
9
+// for that project id
10
+func (q *Control) SetQuota(targetPath string, quota Quota) error {
11
+	return ErrQuotaNotSupported
12
+}
13
+
14
+// GetQuota - get the quota limits of a directory that was configured with SetQuota
15
+func (q *Control) GetQuota(targetPath string, quota *Quota) error {
16
+	return ErrQuotaNotSupported
17
+}
0 18
new file mode 100644
... ...
@@ -0,0 +1,16 @@
0
+package quota // import "github.com/docker/docker/quota"
1
+
2
+import "sync"
3
+
4
+// Quota limit params - currently we only control blocks hard limit
5
+type Quota struct {
6
+	Size uint64
7
+}
8
+
9
+// Control - Context to be used by storage driver (e.g. overlay)
10
+// who wants to apply project quotas to container dirs
11
+type Control struct {
12
+	backingFsBlockDev string
13
+	sync.RWMutex      // protect nextProjectID and quotas map
14
+	quotas            map[string]uint32
15
+}