Browse code

Move quota to daemon/internal/quota

Signed-off-by: Derek McGowan <derek@mcg.dev>

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