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:
Sargun Dhillon 2017-10-30 13:18:14 -07:00
parent b00b1b1c40
commit 7a1618ced3
8 changed files with 133 additions and 14 deletions

View File

@ -13,6 +13,7 @@ import (
"unsafe"
"github.com/docker/docker/daemon/graphdriver"
"github.com/docker/docker/daemon/graphdriver/quota"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/stretchr/testify/assert"
@ -310,7 +311,7 @@ func writeRandomFile(path string, size uint64) error {
}
// 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)
defer PutDriver(t)
@ -318,19 +319,34 @@ func DriverTestSetQuota(t *testing.T, drivername string) {
createOpts := &graphdriver.CreateOpts{}
createOpts.StorageOpt = make(map[string]string, 1)
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)
}
mountPath, err := driver.Get("zfsTest", "")
mountPath, err := driver.Get(layerName, "")
if err != nil {
t.Fatal(err)
}
quota := uint64(50 * units.MiB)
err = writeRandomFile(path.Join(mountPath.Path(), "file"), quota*2)
if pathError, ok := err.(*os.PathError); ok && pathError.Err != unix.EDQUOT {
t.Fatalf("expect write() to fail with %v, got %v", unix.EDQUOT, err)
// Try to write a file smaller than quota, and ensure it works
err = writeRandomFile(path.Join(mountPath.Path(), "smallfile"), quota/2)
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)
}
}

View 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"
}

View File

@ -58,15 +58,10 @@ import (
"path/filepath"
"unsafe"
"errors"
"github.com/sirupsen/logrus"
"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
type Quota struct {
Size uint64

View File

@ -6,10 +6,12 @@ import (
"path/filepath"
"github.com/docker/docker/daemon/graphdriver"
"github.com/docker/docker/daemon/graphdriver/quota"
"github.com/docker/docker/pkg/chrootarchive"
"github.com/docker/docker/pkg/containerfs"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/system"
units "github.com/docker/go-units"
"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 {
return nil, err
}
if err := setupDriverQuota(d); err != nil {
return nil, err
}
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.
// Driver must be wrapped in NaiveDiffDriver to be used as a graphdriver.Driver
type Driver struct {
driverQuota
home string
idMappings *idtools.IDMappings
}
@ -67,15 +75,38 @@ func (d *Driver) Cleanup() error {
// CreateReadWrite creates a layer that is writable for use as a container
// file system.
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.
func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error {
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)
rootIDs := d.idMappings.RootPair()
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 {
return err
}
if size != 0 {
if err := d.setupQuota(dir, size); err != nil {
return err
}
}
labelOpts := []string{"level:s0"}
if _, mountLabel, err := label.InitLabels(labelOpts); err == nil {
label.SetFileLabel(dir, mountLabel)

View 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
}

View 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
}

View File

@ -32,6 +32,10 @@ func TestVfsCreateSnap(t *testing.T) {
graphtest.DriverTestCreateSnap(t, "vfs")
}
func TestVfsSetQuota(t *testing.T) {
graphtest.DriverTestSetQuota(t, "vfs", false)
}
func TestVfsTeardown(t *testing.T) {
graphtest.PutDriver(t)
}

View File

@ -27,7 +27,7 @@ func TestZfsCreateSnap(t *testing.T) {
}
func TestZfsSetQuota(t *testing.T) {
graphtest.DriverTestSetQuota(t, "zfs")
graphtest.DriverTestSetQuota(t, "zfs", true)
}
func TestZfsTeardown(t *testing.T) {