mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Add quota support to VFS graphdriver
This patch adds the capability for the VFS graphdriver to use XFS project quotas. It reuses the existing quota management code that was created by overlay2 on XFS. It doesn't rely on a filesystem whitelist, but instead the quota-capability detection code. Signed-off-by: Sargun Dhillon <sargun@sargun.me>
This commit is contained in:
parent
b00b1b1c40
commit
7a1618ced3
8 changed files with 133 additions and 14 deletions
|
@ -13,6 +13,7 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/docker/docker/daemon/graphdriver"
|
"github.com/docker/docker/daemon/graphdriver"
|
||||||
|
"github.com/docker/docker/daemon/graphdriver/quota"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -310,7 +311,7 @@ func writeRandomFile(path string, size uint64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DriverTestSetQuota Create a driver and test setting quota.
|
// DriverTestSetQuota Create a driver and test setting quota.
|
||||||
func DriverTestSetQuota(t *testing.T, drivername string) {
|
func DriverTestSetQuota(t *testing.T, drivername string, required bool) {
|
||||||
driver := GetDriver(t, drivername)
|
driver := GetDriver(t, drivername)
|
||||||
defer PutDriver(t)
|
defer PutDriver(t)
|
||||||
|
|
||||||
|
@ -318,19 +319,34 @@ func DriverTestSetQuota(t *testing.T, drivername string) {
|
||||||
createOpts := &graphdriver.CreateOpts{}
|
createOpts := &graphdriver.CreateOpts{}
|
||||||
createOpts.StorageOpt = make(map[string]string, 1)
|
createOpts.StorageOpt = make(map[string]string, 1)
|
||||||
createOpts.StorageOpt["size"] = "50M"
|
createOpts.StorageOpt["size"] = "50M"
|
||||||
if err := driver.Create("zfsTest", "Base", createOpts); err != nil {
|
layerName := drivername + "Test"
|
||||||
|
if err := driver.CreateReadWrite(layerName, "Base", createOpts); err == quota.ErrQuotaNotSupported && !required {
|
||||||
|
t.Skipf("Quota not supported on underlying filesystem: %v", err)
|
||||||
|
} else if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mountPath, err := driver.Get("zfsTest", "")
|
mountPath, err := driver.Get(layerName, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
quota := uint64(50 * units.MiB)
|
quota := uint64(50 * units.MiB)
|
||||||
|
|
||||||
err = writeRandomFile(path.Join(mountPath.Path(), "file"), quota*2)
|
// Try to write a file smaller than quota, and ensure it works
|
||||||
if pathError, ok := err.(*os.PathError); ok && pathError.Err != unix.EDQUOT {
|
err = writeRandomFile(path.Join(mountPath.Path(), "smallfile"), quota/2)
|
||||||
t.Fatalf("expect write() to fail with %v, got %v", unix.EDQUOT, err)
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(path.Join(mountPath.Path(), "smallfile"))
|
||||||
|
|
||||||
|
// Try to write a file bigger than quota. We've already filled up half the quota, so hitting the limit should be easy
|
||||||
|
err = writeRandomFile(path.Join(mountPath.Path(), "bigfile"), quota)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected write to fail(), instead had success")
|
||||||
|
}
|
||||||
|
if pathError, ok := err.(*os.PathError); ok && pathError.Err != unix.EDQUOT && pathError.Err != unix.ENOSPC {
|
||||||
|
os.Remove(path.Join(mountPath.Path(), "bigfile"))
|
||||||
|
t.Fatalf("expect write() to fail with %v or %v, got %v", unix.EDQUOT, unix.ENOSPC, pathError.Err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
daemon/graphdriver/quota/errors.go
Normal file
19
daemon/graphdriver/quota/errors.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package quota
|
||||||
|
|
||||||
|
import "github.com/docker/docker/api/errdefs"
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ errdefs.ErrNotImplemented = (*errQuotaNotSupported)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrQuotaNotSupported indicates if were found the FS didn't have projects quotas available
|
||||||
|
var ErrQuotaNotSupported = errQuotaNotSupported{}
|
||||||
|
|
||||||
|
type errQuotaNotSupported struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errQuotaNotSupported) NotImplemented() {}
|
||||||
|
|
||||||
|
func (e errQuotaNotSupported) Error() string {
|
||||||
|
return "Filesystem does not support, or has not enabled quotas"
|
||||||
|
}
|
|
@ -58,15 +58,10 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrQuotaNotSupported indicates if were found the FS does not have projects quotas available
|
|
||||||
var ErrQuotaNotSupported = errors.New("Filesystem does not support or has not enabled quotas")
|
|
||||||
|
|
||||||
// Quota limit params - currently we only control blocks hard limit
|
// Quota limit params - currently we only control blocks hard limit
|
||||||
type Quota struct {
|
type Quota struct {
|
||||||
Size uint64
|
Size uint64
|
||||||
|
|
|
@ -6,10 +6,12 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/docker/docker/daemon/graphdriver"
|
"github.com/docker/docker/daemon/graphdriver"
|
||||||
|
"github.com/docker/docker/daemon/graphdriver/quota"
|
||||||
"github.com/docker/docker/pkg/chrootarchive"
|
"github.com/docker/docker/pkg/chrootarchive"
|
||||||
"github.com/docker/docker/pkg/containerfs"
|
"github.com/docker/docker/pkg/containerfs"
|
||||||
"github.com/docker/docker/pkg/idtools"
|
"github.com/docker/docker/pkg/idtools"
|
||||||
"github.com/docker/docker/pkg/system"
|
"github.com/docker/docker/pkg/system"
|
||||||
|
units "github.com/docker/go-units"
|
||||||
"github.com/opencontainers/selinux/go-selinux/label"
|
"github.com/opencontainers/selinux/go-selinux/label"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,6 +35,11 @@ func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap
|
||||||
if err := idtools.MkdirAllAndChown(home, 0700, rootIDs); err != nil {
|
if err := idtools.MkdirAllAndChown(home, 0700, rootIDs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := setupDriverQuota(d); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps), nil
|
return graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +48,7 @@ func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap
|
||||||
// In order to support layering, files are copied from the parent layer into the new layer. There is no copy-on-write support.
|
// In order to support layering, files are copied from the parent layer into the new layer. There is no copy-on-write support.
|
||||||
// Driver must be wrapped in NaiveDiffDriver to be used as a graphdriver.Driver
|
// Driver must be wrapped in NaiveDiffDriver to be used as a graphdriver.Driver
|
||||||
type Driver struct {
|
type Driver struct {
|
||||||
|
driverQuota
|
||||||
home string
|
home string
|
||||||
idMappings *idtools.IDMappings
|
idMappings *idtools.IDMappings
|
||||||
}
|
}
|
||||||
|
@ -67,15 +75,38 @@ func (d *Driver) Cleanup() error {
|
||||||
// CreateReadWrite creates a layer that is writable for use as a container
|
// CreateReadWrite creates a layer that is writable for use as a container
|
||||||
// file system.
|
// file system.
|
||||||
func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error {
|
func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error {
|
||||||
return d.Create(id, parent, opts)
|
var err error
|
||||||
|
var size int64
|
||||||
|
|
||||||
|
if opts != nil {
|
||||||
|
for key, val := range opts.StorageOpt {
|
||||||
|
switch key {
|
||||||
|
case "size":
|
||||||
|
if !d.quotaSupported() {
|
||||||
|
return quota.ErrQuotaNotSupported
|
||||||
|
}
|
||||||
|
if size, err = units.RAMInBytes(val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Storage opt %s not supported", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.create(id, parent, uint64(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create prepares the filesystem for the VFS driver and copies the directory for the given id under the parent.
|
// Create prepares the filesystem for the VFS driver and copies the directory for the given id under the parent.
|
||||||
func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error {
|
func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error {
|
||||||
if opts != nil && len(opts.StorageOpt) != 0 {
|
if opts != nil && len(opts.StorageOpt) != 0 {
|
||||||
return fmt.Errorf("--storage-opt is not supported for vfs")
|
return fmt.Errorf("--storage-opt is not supported for vfs on read-only layers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return d.create(id, parent, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) create(id, parent string, size uint64) error {
|
||||||
dir := d.dir(id)
|
dir := d.dir(id)
|
||||||
rootIDs := d.idMappings.RootPair()
|
rootIDs := d.idMappings.RootPair()
|
||||||
if err := idtools.MkdirAllAndChown(filepath.Dir(dir), 0700, rootIDs); err != nil {
|
if err := idtools.MkdirAllAndChown(filepath.Dir(dir), 0700, rootIDs); err != nil {
|
||||||
|
@ -84,6 +115,13 @@ func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error {
|
||||||
if err := idtools.MkdirAndChown(dir, 0755, rootIDs); err != nil {
|
if err := idtools.MkdirAndChown(dir, 0755, rootIDs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if size != 0 {
|
||||||
|
if err := d.setupQuota(dir, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
labelOpts := []string{"level:s0"}
|
labelOpts := []string{"level:s0"}
|
||||||
if _, mountLabel, err := label.InitLabels(labelOpts); err == nil {
|
if _, mountLabel, err := label.InitLabels(labelOpts); err == nil {
|
||||||
label.SetFileLabel(dir, mountLabel)
|
label.SetFileLabel(dir, mountLabel)
|
||||||
|
|
27
daemon/graphdriver/vfs/quota_linux.go
Normal file
27
daemon/graphdriver/vfs/quota_linux.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package vfs
|
||||||
|
|
||||||
|
import "github.com/docker/docker/daemon/graphdriver/quota"
|
||||||
|
|
||||||
|
type driverQuota struct {
|
||||||
|
quotaCtl *quota.Control
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDriverQuota(driver *Driver) error {
|
||||||
|
if quotaCtl, err := quota.NewControl(driver.home); err == nil {
|
||||||
|
driver.quotaCtl = quotaCtl
|
||||||
|
} else if err != quota.ErrQuotaNotSupported {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) setupQuota(dir string, size uint64) error {
|
||||||
|
return d.quotaCtl.SetQuota(dir, quota.Quota{Size: size})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) quotaSupported() bool {
|
||||||
|
return d.quotaCtl != nil
|
||||||
|
}
|
20
daemon/graphdriver/vfs/quota_unsupported.go
Normal file
20
daemon/graphdriver/vfs/quota_unsupported.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package vfs
|
||||||
|
|
||||||
|
import "github.com/docker/docker/daemon/graphdriver/quota"
|
||||||
|
|
||||||
|
type driverQuota struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDriverQuota(driver *Driver) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) setupQuota(dir string, size uint64) error {
|
||||||
|
return quota.ErrQuotaNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) quotaSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
|
@ -32,6 +32,10 @@ func TestVfsCreateSnap(t *testing.T) {
|
||||||
graphtest.DriverTestCreateSnap(t, "vfs")
|
graphtest.DriverTestCreateSnap(t, "vfs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVfsSetQuota(t *testing.T) {
|
||||||
|
graphtest.DriverTestSetQuota(t, "vfs", false)
|
||||||
|
}
|
||||||
|
|
||||||
func TestVfsTeardown(t *testing.T) {
|
func TestVfsTeardown(t *testing.T) {
|
||||||
graphtest.PutDriver(t)
|
graphtest.PutDriver(t)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ func TestZfsCreateSnap(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestZfsSetQuota(t *testing.T) {
|
func TestZfsSetQuota(t *testing.T) {
|
||||||
graphtest.DriverTestSetQuota(t, "zfs")
|
graphtest.DriverTestSetQuota(t, "zfs", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestZfsTeardown(t *testing.T) {
|
func TestZfsTeardown(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue