diff --git a/volume/local/local.go b/volume/local/local.go index 17a96550c0..bf97e181e1 100644 --- a/volume/local/local.go +++ b/volume/local/local.go @@ -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) diff --git a/volume/local/local_unix.go b/volume/local/local_unix.go index 5af8822191..9d4b37f53e 100644 --- a/volume/local/local_unix.go +++ b/volume/local/local_unix.go @@ -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 { diff --git a/volume/local/local_windows.go b/volume/local/local_windows.go index d9bc8f5298..b05510d098 100644 --- a/volume/local/local_windows.go +++ b/volume/local/local_windows.go @@ -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 {