Merge pull request #41330 from BtbN/vol-pquota

Add size option to volumes on linux/unix via xfs pquota
This commit is contained in:
Sebastiaan van Stijn 2020-10-07 17:50:39 +02:00 committed by GitHub
commit ffd0861b8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 469 additions and 194 deletions

View file

@ -13,8 +13,8 @@ import (
"unsafe"
"github.com/docker/docker/daemon/graphdriver"
"github.com/docker/docker/daemon/graphdriver/quota"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/quota"
units "github.com/docker/go-units"
"golang.org/x/sys/unix"
"gotest.tools/v3/assert"

View file

@ -18,7 +18,6 @@ import (
"github.com/containerd/containerd/sys"
"github.com/docker/docker/daemon/graphdriver"
"github.com/docker/docker/daemon/graphdriver/overlayutils"
"github.com/docker/docker/daemon/graphdriver/quota"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/chrootarchive"
"github.com/docker/docker/pkg/containerfs"
@ -27,6 +26,7 @@ import (
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/parsers"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/quota"
units "github.com/docker/go-units"
"github.com/moby/locker"
"github.com/moby/sys/mount"

View file

@ -1,152 +0,0 @@
// +build linux
package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
import (
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
"golang.org/x/sys/unix"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
)
// 10MB
const testQuotaSize = 10 * 1024 * 1024
const imageSize = 64 * 1024 * 1024
func TestBlockDev(t *testing.T) {
mkfs, err := exec.LookPath("mkfs.xfs")
if err != nil {
t.Skip("mkfs.xfs not found in PATH")
}
// create a sparse image
imageFile, err := ioutil.TempFile("", "xfs-image")
if err != nil {
t.Fatal(err)
}
imageFileName := imageFile.Name()
defer os.Remove(imageFileName)
if _, err = imageFile.Seek(imageSize-1, 0); err != nil {
t.Fatal(err)
}
if _, err = imageFile.Write([]byte{0}); err != nil {
t.Fatal(err)
}
if err = imageFile.Close(); err != nil {
t.Fatal(err)
}
// The reason for disabling these options is sometimes people run with a newer userspace
// than kernelspace
out, err := exec.Command(mkfs, "-m", "crc=0,finobt=0", imageFileName).CombinedOutput()
if len(out) > 0 {
t.Log(string(out))
}
if err != nil {
t.Fatal(err)
}
t.Run("testBlockDevQuotaDisabled", wrapMountTest(imageFileName, false, testBlockDevQuotaDisabled))
t.Run("testBlockDevQuotaEnabled", wrapMountTest(imageFileName, true, testBlockDevQuotaEnabled))
t.Run("testSmallerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testSmallerThanQuota)))
t.Run("testBiggerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testBiggerThanQuota)))
t.Run("testRetrieveQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testRetrieveQuota)))
}
func wrapMountTest(imageFileName string, enableQuota bool, testFunc func(t *testing.T, mountPoint, backingFsDev string)) func(*testing.T) {
return func(t *testing.T) {
mountOptions := "loop"
if enableQuota {
mountOptions = mountOptions + ",prjquota"
}
mountPointDir := fs.NewDir(t, "xfs-mountPoint")
defer mountPointDir.Remove()
mountPoint := mountPointDir.Path()
out, err := exec.Command("mount", "-o", mountOptions, imageFileName, mountPoint).CombinedOutput()
if err != nil {
_, err := os.Stat("/proc/fs/xfs")
if os.IsNotExist(err) {
t.Skip("no /proc/fs/xfs")
}
}
assert.NilError(t, err, "mount failed: %s", out)
defer func() {
assert.NilError(t, unix.Unmount(mountPoint, 0))
}()
backingFsDev, err := makeBackingFsDev(mountPoint)
assert.NilError(t, err)
testFunc(t, mountPoint, backingFsDev)
}
}
func testBlockDevQuotaDisabled(t *testing.T, mountPoint, backingFsDev string) {
hasSupport, err := hasQuotaSupport(backingFsDev)
assert.NilError(t, err)
assert.Check(t, !hasSupport)
}
func testBlockDevQuotaEnabled(t *testing.T, mountPoint, backingFsDev string) {
hasSupport, err := hasQuotaSupport(backingFsDev)
assert.NilError(t, err)
assert.Check(t, hasSupport)
}
func wrapQuotaTest(testFunc func(t *testing.T, ctrl *Control, mountPoint, testDir, testSubDir string)) func(t *testing.T, mountPoint, backingFsDev string) {
return func(t *testing.T, mountPoint, backingFsDev string) {
testDir, err := ioutil.TempDir(mountPoint, "per-test")
assert.NilError(t, err)
defer os.RemoveAll(testDir)
ctrl, err := NewControl(testDir)
assert.NilError(t, err)
testSubDir, err := ioutil.TempDir(testDir, "quota-test")
assert.NilError(t, err)
testFunc(t, ctrl, mountPoint, testDir, testSubDir)
}
}
func testSmallerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
smallerThanQuotaFile := filepath.Join(testSubDir, "smaller-than-quota")
assert.NilError(t, ioutil.WriteFile(smallerThanQuotaFile, make([]byte, testQuotaSize/2), 0644))
assert.NilError(t, os.Remove(smallerThanQuotaFile))
}
func testBiggerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
// Make sure the quota is being enforced
// TODO: When we implement this under EXT4, we need to shed CAP_SYS_RESOURCE, otherwise
// we're able to violate quota without issue
assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
biggerThanQuotaFile := filepath.Join(testSubDir, "bigger-than-quota")
err := ioutil.WriteFile(biggerThanQuotaFile, make([]byte, testQuotaSize+1), 0644)
assert.Assert(t, is.ErrorContains(err, ""))
if err == io.ErrShortWrite {
assert.NilError(t, os.Remove(biggerThanQuotaFile))
}
}
func testRetrieveQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
// Validate that we can retrieve quota
assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
var q Quota
assert.NilError(t, ctrl.GetQuota(testSubDir, &q))
assert.Check(t, is.Equal(uint64(testQuotaSize), q.Size))
}

View file

@ -6,12 +6,12 @@ import (
"path/filepath"
"github.com/docker/docker/daemon/graphdriver"
"github.com/docker/docker/daemon/graphdriver/quota"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/containerfs"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/parsers"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/quota"
units "github.com/docker/go-units"
"github.com/opencontainers/selinux/go-selinux/label"
"github.com/pkg/errors"

View file

@ -1,7 +1,7 @@
package vfs // import "github.com/docker/docker/daemon/graphdriver/vfs"
import (
"github.com/docker/docker/daemon/graphdriver/quota"
"github.com/docker/docker/quota"
"github.com/sirupsen/logrus"
)

View file

@ -2,7 +2,7 @@
package vfs // import "github.com/docker/docker/daemon/graphdriver/vfs"
import "github.com/docker/docker/daemon/graphdriver/quota"
import "github.com/docker/docker/quota"
type driverQuota struct {
}

View file

@ -1,4 +1,4 @@
package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
package quota // import "github.com/docker/docker/quota"
import "github.com/docker/docker/errdefs"

View file

@ -9,7 +9,7 @@
// for both xfs/ext4 for kernel version >= v4.5
//
package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
package quota // import "github.com/docker/docker/quota"
/*
#include <stdlib.h>
@ -55,6 +55,7 @@ import (
"io/ioutil"
"path"
"path/filepath"
"sync"
"unsafe"
"github.com/containerd/containerd/sys"
@ -63,6 +64,33 @@ import (
"golang.org/x/sys/unix"
)
type pquotaState struct {
sync.Mutex
nextProjectID uint32
}
var pquotaStateInst *pquotaState
var pquotaStateOnce sync.Once
// getPquotaState - get global pquota state tracker instance
func getPquotaState() *pquotaState {
pquotaStateOnce.Do(func() {
pquotaStateInst = &pquotaState{
nextProjectID: 1,
}
})
return pquotaStateInst
}
// registerBasePath - register a new base path and update nextProjectID
func (state *pquotaState) updateMinProjID(minProjectID uint32) {
state.Lock()
defer state.Unlock()
if state.nextProjectID <= minProjectID {
state.nextProjectID = minProjectID + 1
}
}
// NewControl - initialize project quota support.
// Test to make sure that quota can be set on a test dir and find
// the first project id to be used for the next container create.
@ -115,11 +143,11 @@ func NewControl(basePath string) (*Control, error) {
//
// Get project id of parent dir as minimal id to be used by driver
//
minProjectID, err := getProjectID(basePath)
baseProjectID, err := getProjectID(basePath)
if err != nil {
return nil, err
}
minProjectID++
minProjectID := baseProjectID + 1
//
// Test if filesystem supports project quotas by trying to set
@ -134,19 +162,24 @@ func NewControl(basePath string) (*Control, error) {
q := Control{
backingFsBlockDev: backingFsBlockDev,
nextProjectID: minProjectID + 1,
quotas: make(map[string]uint32),
}
//
// update minimum project ID
//
state := getPquotaState()
state.updateMinProjID(minProjectID)
//
// get first project id to be used for next container
//
err = q.findNextProjectID(basePath)
err = q.findNextProjectID(basePath, baseProjectID)
if err != nil {
return nil, err
}
logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, q.nextProjectID)
logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, state.nextProjectID)
return &q, nil
}
@ -157,19 +190,24 @@ func (q *Control) SetQuota(targetPath string, quota Quota) error {
projectID, ok := q.quotas[targetPath]
q.RUnlock()
if !ok {
q.Lock()
projectID = q.nextProjectID
state := getPquotaState()
state.Lock()
projectID = state.nextProjectID
//
// assign project id to new container directory
//
err := setProjectID(targetPath, projectID)
if err != nil {
q.Unlock()
state.Unlock()
return err
}
state.nextProjectID++
state.Unlock()
q.Lock()
q.quotas[targetPath] = projectID
q.nextProjectID++
q.Unlock()
}
@ -279,9 +317,25 @@ func setProjectID(targetPath string, projectID uint32) error {
// findNextProjectID - find the next project id to be used for containers
// by scanning driver home directory to find used project ids
func (q *Control) findNextProjectID(home string) error {
q.Lock()
defer q.Unlock()
func (q *Control) findNextProjectID(home string, baseID uint32) error {
state := getPquotaState()
state.Lock()
defer state.Unlock()
checkProjID := func(path string) (uint32, error) {
projid, err := getProjectID(path)
if err != nil {
return projid, err
}
if projid > 0 {
q.quotas[path] = projid
}
if state.nextProjectID <= projid {
state.nextProjectID = projid + 1
}
return projid, nil
}
files, err := ioutil.ReadDir(home)
if err != nil {
return errors.Errorf("read directory failed: %s", home)
@ -291,15 +345,26 @@ func (q *Control) findNextProjectID(home string) error {
continue
}
path := filepath.Join(home, file.Name())
projid, err := getProjectID(path)
projid, err := checkProjID(path)
if err != nil {
return err
}
if projid > 0 {
q.quotas[path] = projid
if projid > 0 && projid != baseID {
continue
}
if q.nextProjectID <= projid {
q.nextProjectID = projid + 1
subfiles, err := ioutil.ReadDir(path)
if err != nil {
return errors.Errorf("read directory failed: %s", path)
}
for _, subfile := range subfiles {
if !subfile.IsDir() {
continue
}
subpath := filepath.Join(path, subfile.Name())
_, err := checkProjID(subpath)
if err != nil {
return err
}
}
}

View file

@ -0,0 +1,78 @@
// +build linux
package quota // import "github.com/docker/docker/quota"
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
// 10MB
const testQuotaSize = 10 * 1024 * 1024
func TestBlockDev(t *testing.T) {
if msg, ok := CanTestQuota(); !ok {
t.Skip(msg)
}
// get sparse xfs test image
imageFileName, err := PrepareQuotaTestImage(t)
if err != nil {
t.Fatal(err)
}
defer os.Remove(imageFileName)
t.Run("testBlockDevQuotaDisabled", WrapMountTest(imageFileName, false, testBlockDevQuotaDisabled))
t.Run("testBlockDevQuotaEnabled", WrapMountTest(imageFileName, true, testBlockDevQuotaEnabled))
t.Run("testSmallerThanQuota", WrapMountTest(imageFileName, true, WrapQuotaTest(testSmallerThanQuota)))
t.Run("testBiggerThanQuota", WrapMountTest(imageFileName, true, WrapQuotaTest(testBiggerThanQuota)))
t.Run("testRetrieveQuota", WrapMountTest(imageFileName, true, WrapQuotaTest(testRetrieveQuota)))
}
func testBlockDevQuotaDisabled(t *testing.T, mountPoint, backingFsDev, testDir string) {
hasSupport, err := hasQuotaSupport(backingFsDev)
assert.NilError(t, err)
assert.Check(t, !hasSupport)
}
func testBlockDevQuotaEnabled(t *testing.T, mountPoint, backingFsDev, testDir string) {
hasSupport, err := hasQuotaSupport(backingFsDev)
assert.NilError(t, err)
assert.Check(t, hasSupport)
}
func testSmallerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
smallerThanQuotaFile := filepath.Join(testSubDir, "smaller-than-quota")
assert.NilError(t, ioutil.WriteFile(smallerThanQuotaFile, make([]byte, testQuotaSize/2), 0644))
assert.NilError(t, os.Remove(smallerThanQuotaFile))
}
func testBiggerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
// Make sure the quota is being enforced
// TODO: When we implement this under EXT4, we need to shed CAP_SYS_RESOURCE, otherwise
// we're able to violate quota without issue
assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
biggerThanQuotaFile := filepath.Join(testSubDir, "bigger-than-quota")
err := ioutil.WriteFile(biggerThanQuotaFile, make([]byte, testQuotaSize+1), 0644)
assert.Assert(t, is.ErrorContains(err, ""))
if err == io.ErrShortWrite {
assert.NilError(t, os.Remove(biggerThanQuotaFile))
}
}
func testRetrieveQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
// Validate that we can retrieve quota
assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
var q Quota
assert.NilError(t, ctrl.GetQuota(testSubDir, &q))
assert.Check(t, is.Equal(uint64(testQuotaSize), q.Size))
}

View file

@ -1,6 +1,6 @@
// +build linux,exclude_disk_quota linux,!cgo
// +build linux,exclude_disk_quota linux,!cgo !linux
package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
package quota // import "github.com/docker/docker/quota"
func NewControl(basePath string) (*Control, error) {
return nil, ErrQuotaNotSupported

122
quota/testhelpers.go Normal file
View file

@ -0,0 +1,122 @@
// +build linux
package quota // import "github.com/docker/docker/quota"
import (
"io/ioutil"
"os"
"os/exec"
"testing"
"golang.org/x/sys/unix"
"gotest.tools/v3/assert"
"gotest.tools/v3/fs"
)
const imageSize = 64 * 1024 * 1024
// CanTestQuota - checks if xfs prjquota can be tested
// returns a reason if not
func CanTestQuota() (string, bool) {
if os.Getuid() != 0 {
return "requires mounts", false
}
_, err := exec.LookPath("mkfs.xfs")
if err != nil {
return "mkfs.xfs not found in PATH", false
}
return "", true
}
// PrepareQuotaTestImage - prepares an xfs prjquota test image
// returns the path the the image on success
func PrepareQuotaTestImage(t *testing.T) (string, error) {
mkfs, err := exec.LookPath("mkfs.xfs")
if err != nil {
return "", err
}
// create a sparse image
imageFile, err := ioutil.TempFile("", "xfs-image")
if err != nil {
return "", err
}
imageFileName := imageFile.Name()
if _, err = imageFile.Seek(imageSize-1, 0); err != nil {
os.Remove(imageFileName)
return "", err
}
if _, err = imageFile.Write([]byte{0}); err != nil {
os.Remove(imageFileName)
return "", err
}
if err = imageFile.Close(); err != nil {
os.Remove(imageFileName)
return "", err
}
// The reason for disabling these options is sometimes people run with a newer userspace
// than kernelspace
out, err := exec.Command(mkfs, "-m", "crc=0,finobt=0", imageFileName).CombinedOutput()
if len(out) > 0 {
t.Log(string(out))
}
if err != nil {
os.Remove(imageFileName)
return "", err
}
return imageFileName, nil
}
// WrapMountTest - wraps a test function such that it has easy access to a mountPoint and testDir
// with guaranteed prjquota or guaranteed no prjquota support.
func WrapMountTest(imageFileName string, enableQuota bool, testFunc func(t *testing.T, mountPoint, backingFsDev, testDir string)) func(*testing.T) {
return func(t *testing.T) {
mountOptions := "loop"
if enableQuota {
mountOptions = mountOptions + ",prjquota"
}
mountPointDir := fs.NewDir(t, "xfs-mountPoint")
defer mountPointDir.Remove()
mountPoint := mountPointDir.Path()
out, err := exec.Command("mount", "-o", mountOptions, imageFileName, mountPoint).CombinedOutput()
if err != nil {
_, err := os.Stat("/proc/fs/xfs")
if os.IsNotExist(err) {
t.Skip("no /proc/fs/xfs")
}
}
assert.NilError(t, err, "mount failed: %s", out)
defer func() {
assert.NilError(t, unix.Unmount(mountPoint, 0))
}()
backingFsDev, err := makeBackingFsDev(mountPoint)
assert.NilError(t, err)
testDir, err := ioutil.TempDir(mountPoint, "per-test")
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testFunc(t, mountPoint, backingFsDev, testDir)
}
}
// WrapQuotaTest - wraps a test function such that is has easy and guaranteed access to a quota Control
// instance with a quota test dir under its control.
func WrapQuotaTest(testFunc func(t *testing.T, ctrl *Control, mountPoint, testDir, testSubDir string)) func(t *testing.T, mountPoint, backingFsDev, testDir string) {
return func(t *testing.T, mountPoint, backingFsDev, testDir string) {
ctrl, err := NewControl(testDir)
assert.NilError(t, err)
testSubDir, err := ioutil.TempDir(testDir, "quota-test")
assert.NilError(t, err)
testFunc(t, ctrl, mountPoint, testDir, testSubDir)
}
}

View file

@ -1,6 +1,4 @@
// +build linux
package quota // import "github.com/docker/docker/daemon/graphdriver/quota"
package quota // import "github.com/docker/docker/quota"
import "sync"
@ -14,6 +12,5 @@ type Quota struct {
type Control struct {
backingFsBlockDev string
sync.RWMutex // protect nextProjectID and quotas map
nextProjectID uint32
quotas map[string]uint32
}

View file

@ -16,10 +16,12 @@ import (
"github.com/docker/docker/daemon/names"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/quota"
"github.com/docker/docker/volume"
"github.com/moby/sys/mount"
"github.com/moby/sys/mountinfo"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// VolumeDataPathName is the name of the directory where the volume data is stored.
@ -66,6 +68,10 @@ func New(scope string, rootIdentity idtools.Identity) (*Root, error) {
return nil, err
}
if r.quotaCtl, err = quota.NewControl(rootDirectory); err != nil {
logrus.Debugf("No quota support for local volumes in %s: %v", rootDirectory, err)
}
for _, d := range dirs {
if !d.IsDir() {
continue
@ -76,6 +82,7 @@ func New(scope string, rootIdentity idtools.Identity) (*Root, error) {
driverName: r.Name(),
name: name,
path: r.DataPath(name),
quotaCtl: r.quotaCtl,
}
r.volumes[name] = v
optsFilePath := filepath.Join(rootDirectory, name, "opts.json")
@ -105,6 +112,7 @@ type Root struct {
m sync.Mutex
scope string
path string
quotaCtl *quota.Control
volumes map[string]*localVolume
rootIdentity idtools.Identity
}
@ -162,6 +170,7 @@ func (r *Root) Create(name string, opts map[string]string) (volume.Volume, error
driverName: r.Name(),
name: name,
path: path,
quotaCtl: r.quotaCtl,
}
if len(opts) != 0 {
@ -273,6 +282,8 @@ type localVolume struct {
opts *optsConfig
// active refcounts the active mounts
active activeMount
// reference to Root instances quotaCtl
quotaCtl *quota.Control
}
// Name returns the name of the given Volume.
@ -300,7 +311,7 @@ func (v *localVolume) CachedPath() string {
func (v *localVolume) Mount(id string) (string, error) {
v.m.Lock()
defer v.m.Unlock()
if v.opts != nil {
if v.needsMount() {
if !v.active.mounted {
if err := v.mount(); err != nil {
return "", errdefs.System(err)
@ -309,6 +320,9 @@ func (v *localVolume) Mount(id string) (string, error) {
}
v.active.count++
}
if err := v.postMount(); err != nil {
return "", err
}
return v.path, nil
}
@ -322,7 +336,7 @@ func (v *localVolume) Unmount(id string) error {
// Essentially docker doesn't care if this fails, it will send an error, but
// ultimately there's nothing that can be done. If we don't decrement the count
// this volume can never be removed until a daemon restart occurs.
if v.opts != nil {
if v.needsMount() {
v.active.count--
}
@ -334,7 +348,7 @@ func (v *localVolume) Unmount(id string) error {
}
func (v *localVolume) unmount() error {
if v.opts != nil {
if v.needsMount() {
if err := mount.Unmount(v.path); err != nil {
if mounted, mErr := mountinfo.Mounted(v.path); mounted || mErr != nil {
return errdefs.System(err)

View file

@ -0,0 +1,97 @@
// +build linux
package local // import "github.com/docker/docker/volume/local"
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/quota"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
const quotaSize = 1024 * 1024
const quotaSizeLiteral = "1M"
func TestQuota(t *testing.T) {
if msg, ok := quota.CanTestQuota(); !ok {
t.Skip(msg)
}
// get sparse xfs test image
imageFileName, err := quota.PrepareQuotaTestImage(t)
if err != nil {
t.Fatal(err)
}
defer os.Remove(imageFileName)
t.Run("testVolWithQuota", quota.WrapMountTest(imageFileName, true, testVolWithQuota))
t.Run("testVolQuotaUnsupported", quota.WrapMountTest(imageFileName, false, testVolQuotaUnsupported))
}
func testVolWithQuota(t *testing.T, mountPoint, backingFsDev, testDir string) {
r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
if err != nil {
t.Fatal(err)
}
assert.Assert(t, r.quotaCtl != nil)
vol, err := r.Create("testing", map[string]string{"size": quotaSizeLiteral})
if err != nil {
t.Fatal(err)
}
dir, err := vol.Mount("1234")
if err != nil {
t.Fatal(err)
}
defer func() {
if err := vol.Unmount("1234"); err != nil {
t.Fatal(err)
}
}()
testfile := filepath.Join(dir, "testfile")
// test writing file smaller than quota
assert.NilError(t, ioutil.WriteFile(testfile, make([]byte, quotaSize/2), 0644))
assert.NilError(t, os.Remove(testfile))
// test writing fiel larger than quota
err = ioutil.WriteFile(testfile, make([]byte, quotaSize+1), 0644)
assert.ErrorContains(t, err, "")
if _, err := os.Stat(testfile); err == nil {
assert.NilError(t, os.Remove(testfile))
}
}
func testVolQuotaUnsupported(t *testing.T, mountPoint, backingFsDev, testDir string) {
r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
if err != nil {
t.Fatal(err)
}
assert.Assert(t, is.Nil(r.quotaCtl))
_, err = r.Create("testing", map[string]string{"size": quotaSizeLiteral})
assert.ErrorContains(t, err, "no quota support")
vol, err := r.Create("testing", nil)
if err != nil {
t.Fatal(err)
}
// this could happen if someone moves volumes from storage with
// quota support to some place without
lv, ok := vol.(*localVolume)
assert.Assert(t, ok)
lv.opts = &optsConfig{
Quota: quota.Quota{Size: quotaSize},
}
_, err = vol.Mount("1234")
assert.ErrorContains(t, err, "no quota support")
}

View file

@ -15,6 +15,8 @@ import (
"time"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/quota"
units "github.com/docker/go-units"
"github.com/moby/sys/mount"
"github.com/pkg/errors"
)
@ -26,10 +28,12 @@ var (
"type": {}, // specify the filesystem type for mount, e.g. nfs
"o": {}, // generic mount options
"device": {}, // device to mount from
"size": {}, // quota size limit
}
mandatoryOpts = map[string]struct{}{
"device": {},
"type": {},
mandatoryOpts = map[string][]string{
"device": []string{"type"},
"type": []string{"device"},
"o": []string{"device", "type"},
}
)
@ -37,10 +41,11 @@ type optsConfig struct {
MountType string
MountOpts string
MountDevice string
Quota quota.Quota
}
func (o *optsConfig) String() string {
return fmt.Sprintf("type='%s' device='%s' o='%s'", o.MountType, o.MountDevice, o.MountOpts)
return fmt.Sprintf("type='%s' device='%s' o='%s' size='%d'", o.MountType, o.MountDevice, o.MountOpts, o.Quota.Size)
}
// scopedPath verifies that the path where the volume is located
@ -63,15 +68,25 @@ func setOpts(v *localVolume, opts map[string]string) error {
if len(opts) == 0 {
return nil
}
if err := validateOpts(opts); err != nil {
err := validateOpts(opts)
if err != nil {
return err
}
v.opts = &optsConfig{
MountType: opts["type"],
MountOpts: opts["o"],
MountDevice: opts["device"],
}
if val, ok := opts["size"]; ok {
size, err := units.RAMInBytes(val)
if err != nil {
return err
}
if size > 0 && v.quotaCtl == nil {
return errdefs.InvalidParameter(errors.Errorf("quota size requested but no quota support"))
}
v.opts.Quota.Size = uint64(size)
}
return nil
}
@ -84,14 +99,28 @@ func validateOpts(opts map[string]string) error {
return errdefs.InvalidParameter(errors.Errorf("invalid option: %q", opt))
}
}
for opt := range mandatoryOpts {
if _, ok := opts[opt]; !ok {
return errdefs.InvalidParameter(errors.Errorf("missing required option: %q", opt))
for opt, reqopts := range mandatoryOpts {
if _, ok := opts[opt]; ok {
for _, reqopt := range reqopts {
if _, ok := opts[reqopt]; !ok {
return errdefs.InvalidParameter(errors.Errorf("missing required option: %q", reqopt))
}
}
}
}
return nil
}
func (v *localVolume) needsMount() bool {
if v.opts == nil {
return false
}
if v.opts.MountDevice != "" || v.opts.MountType != "" {
return true
}
return false
}
func (v *localVolume) mount() error {
if v.opts.MountDevice == "" {
return fmt.Errorf("missing device in volume options")
@ -111,6 +140,23 @@ func (v *localVolume) mount() error {
return errors.Wrap(err, "failed to mount local volume")
}
func (v *localVolume) postMount() error {
if v.opts == nil {
return nil
}
if v.opts.Quota.Size > 0 {
if v.quotaCtl != nil {
err := v.quotaCtl.SetQuota(v.path, v.opts.Quota)
if err != nil {
return err
}
} else {
return fmt.Errorf("size quota requested for volume but no quota support")
}
}
return nil
}
func (v *localVolume) CreatedAt() (time.Time, error) {
fileInfo, err := os.Stat(v.path)
if err != nil {

View file

@ -32,10 +32,18 @@ func setOpts(v *localVolume, opts map[string]string) error {
return nil
}
func (v *localVolume) needsMount() bool {
return false
}
func (v *localVolume) mount() error {
return nil
}
func (v *localVolume) postMount() error {
return nil
}
func (v *localVolume) CreatedAt() (time.Time, error) {
fileInfo, err := os.Stat(v.path)
if err != nil {