Merge pull request #41330 from BtbN/vol-pquota
Add size option to volumes on linux/unix via xfs pquota
This commit is contained in:
commit
ffd0861b8b
16 changed files with 469 additions and 194 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
78
quota/projectquota_test.go
Normal file
78
quota/projectquota_test.go
Normal 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))
|
||||
}
|
|
@ -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
122
quota/testhelpers.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
97
volume/local/local_linux_test.go
Normal file
97
volume/local/local_linux_test.go
Normal 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")
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue