Browse code

volume/local: decouple presence of options from mounting

Signed-off-by: Timo Rothenpieler <timo@rothenpieler.org>

Timo Rothenpieler authored on 2020/08/10 04:53:09
Showing 3 changed files
... ...
@@ -16,10 +16,12 @@ import (
16 16
 	"github.com/docker/docker/daemon/names"
17 17
 	"github.com/docker/docker/errdefs"
18 18
 	"github.com/docker/docker/pkg/idtools"
19
+	"github.com/docker/docker/quota"
19 20
 	"github.com/docker/docker/volume"
20 21
 	"github.com/moby/sys/mount"
21 22
 	"github.com/moby/sys/mountinfo"
22 23
 	"github.com/pkg/errors"
24
+	"github.com/sirupsen/logrus"
23 25
 )
24 26
 
25 27
 // 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) {
66 66
 		return nil, err
67 67
 	}
68 68
 
69
+	if r.quotaCtl, err = quota.NewControl(rootDirectory); err != nil {
70
+		logrus.Debugf("No quota support for local volumes in %s: %v", rootDirectory, err)
71
+	}
72
+
69 73
 	for _, d := range dirs {
70 74
 		if !d.IsDir() {
71 75
 			continue
... ...
@@ -76,6 +82,7 @@ func New(scope string, rootIdentity idtools.Identity) (*Root, error) {
76 76
 			driverName: r.Name(),
77 77
 			name:       name,
78 78
 			path:       r.DataPath(name),
79
+			quotaCtl:   r.quotaCtl,
79 80
 		}
80 81
 		r.volumes[name] = v
81 82
 		optsFilePath := filepath.Join(rootDirectory, name, "opts.json")
... ...
@@ -105,6 +112,7 @@ type Root struct {
105 105
 	m            sync.Mutex
106 106
 	scope        string
107 107
 	path         string
108
+	quotaCtl     *quota.Control
108 109
 	volumes      map[string]*localVolume
109 110
 	rootIdentity idtools.Identity
110 111
 }
... ...
@@ -162,6 +170,7 @@ func (r *Root) Create(name string, opts map[string]string) (volume.Volume, error
162 162
 		driverName: r.Name(),
163 163
 		name:       name,
164 164
 		path:       path,
165
+		quotaCtl:   r.quotaCtl,
165 166
 	}
166 167
 
167 168
 	if len(opts) != 0 {
... ...
@@ -273,6 +282,8 @@ type localVolume struct {
273 273
 	opts *optsConfig
274 274
 	// active refcounts the active mounts
275 275
 	active activeMount
276
+	// reference to Root instances quotaCtl
277
+	quotaCtl *quota.Control
276 278
 }
277 279
 
278 280
 // Name returns the name of the given Volume.
... ...
@@ -300,7 +311,7 @@ func (v *localVolume) CachedPath() string {
300 300
 func (v *localVolume) Mount(id string) (string, error) {
301 301
 	v.m.Lock()
302 302
 	defer v.m.Unlock()
303
-	if v.opts != nil {
303
+	if v.needsMount() {
304 304
 		if !v.active.mounted {
305 305
 			if err := v.mount(); err != nil {
306 306
 				return "", errdefs.System(err)
... ...
@@ -309,6 +320,9 @@ func (v *localVolume) Mount(id string) (string, error) {
309 309
 		}
310 310
 		v.active.count++
311 311
 	}
312
+	if err := v.postMount(); err != nil {
313
+		return "", err
314
+	}
312 315
 	return v.path, nil
313 316
 }
314 317
 
... ...
@@ -322,7 +336,7 @@ func (v *localVolume) Unmount(id string) error {
322 322
 	// Essentially docker doesn't care if this fails, it will send an error, but
323 323
 	// ultimately there's nothing that can be done. If we don't decrement the count
324 324
 	// this volume can never be removed until a daemon restart occurs.
325
-	if v.opts != nil {
325
+	if v.needsMount() {
326 326
 		v.active.count--
327 327
 	}
328 328
 
... ...
@@ -334,7 +348,7 @@ func (v *localVolume) Unmount(id string) error {
334 334
 }
335 335
 
336 336
 func (v *localVolume) unmount() error {
337
-	if v.opts != nil {
337
+	if v.needsMount() {
338 338
 		if err := mount.Unmount(v.path); err != nil {
339 339
 			if mounted, mErr := mountinfo.Mounted(v.path); mounted || mErr != nil {
340 340
 				return errdefs.System(err)
... ...
@@ -15,6 +15,8 @@ import (
15 15
 	"time"
16 16
 
17 17
 	"github.com/docker/docker/errdefs"
18
+	"github.com/docker/docker/quota"
19
+	units "github.com/docker/go-units"
18 20
 	"github.com/moby/sys/mount"
19 21
 	"github.com/pkg/errors"
20 22
 )
... ...
@@ -26,10 +28,12 @@ var (
26 26
 		"type":   {}, // specify the filesystem type for mount, e.g. nfs
27 27
 		"o":      {}, // generic mount options
28 28
 		"device": {}, // device to mount from
29
+		"size":   {}, // quota size limit
29 30
 	}
30
-	mandatoryOpts = map[string]struct{}{
31
-		"device": {},
32
-		"type":   {},
31
+	mandatoryOpts = map[string][]string{
32
+		"device": []string{"type"},
33
+		"type":   []string{"device"},
34
+		"o":      []string{"device", "type"},
33 35
 	}
34 36
 )
35 37
 
... ...
@@ -37,10 +41,11 @@ type optsConfig struct {
37 37
 	MountType   string
38 38
 	MountOpts   string
39 39
 	MountDevice string
40
+	Quota       quota.Quota
40 41
 }
41 42
 
42 43
 func (o *optsConfig) String() string {
43
-	return fmt.Sprintf("type='%s' device='%s' o='%s'", o.MountType, o.MountDevice, o.MountOpts)
44
+	return fmt.Sprintf("type='%s' device='%s' o='%s' size='%d'", o.MountType, o.MountDevice, o.MountOpts, o.Quota.Size)
44 45
 }
45 46
 
46 47
 // scopedPath verifies that the path where the volume is located
... ...
@@ -63,15 +68,25 @@ func setOpts(v *localVolume, opts map[string]string) error {
63 63
 	if len(opts) == 0 {
64 64
 		return nil
65 65
 	}
66
-	if err := validateOpts(opts); err != nil {
66
+	err := validateOpts(opts)
67
+	if err != nil {
67 68
 		return err
68 69
 	}
69
-
70 70
 	v.opts = &optsConfig{
71 71
 		MountType:   opts["type"],
72 72
 		MountOpts:   opts["o"],
73 73
 		MountDevice: opts["device"],
74 74
 	}
75
+	if val, ok := opts["size"]; ok {
76
+		size, err := units.RAMInBytes(val)
77
+		if err != nil {
78
+			return err
79
+		}
80
+		if size > 0 && v.quotaCtl == nil {
81
+			return errdefs.InvalidParameter(errors.Errorf("quota size requested but no quota support"))
82
+		}
83
+		v.opts.Quota.Size = uint64(size)
84
+	}
75 85
 	return nil
76 86
 }
77 87
 
... ...
@@ -84,14 +99,28 @@ func validateOpts(opts map[string]string) error {
84 84
 			return errdefs.InvalidParameter(errors.Errorf("invalid option: %q", opt))
85 85
 		}
86 86
 	}
87
-	for opt := range mandatoryOpts {
88
-		if _, ok := opts[opt]; !ok {
89
-			return errdefs.InvalidParameter(errors.Errorf("missing required option: %q", opt))
87
+	for opt, reqopts := range mandatoryOpts {
88
+		if _, ok := opts[opt]; ok {
89
+			for _, reqopt := range reqopts {
90
+				if _, ok := opts[reqopt]; !ok {
91
+					return errdefs.InvalidParameter(errors.Errorf("missing required option: %q", reqopt))
92
+				}
93
+			}
90 94
 		}
91 95
 	}
92 96
 	return nil
93 97
 }
94 98
 
99
+func (v *localVolume) needsMount() bool {
100
+	if v.opts == nil {
101
+		return false
102
+	}
103
+	if v.opts.MountDevice != "" || v.opts.MountType != "" {
104
+		return true
105
+	}
106
+	return false
107
+}
108
+
95 109
 func (v *localVolume) mount() error {
96 110
 	if v.opts.MountDevice == "" {
97 111
 		return fmt.Errorf("missing device in volume options")
... ...
@@ -111,6 +140,23 @@ func (v *localVolume) mount() error {
111 111
 	return errors.Wrap(err, "failed to mount local volume")
112 112
 }
113 113
 
114
+func (v *localVolume) postMount() error {
115
+	if v.opts == nil {
116
+		return nil
117
+	}
118
+	if v.opts.Quota.Size > 0 {
119
+		if v.quotaCtl != nil {
120
+			err := v.quotaCtl.SetQuota(v.path, v.opts.Quota)
121
+			if err != nil {
122
+				return err
123
+			}
124
+		} else {
125
+			return fmt.Errorf("size quota requested for volume but no quota support")
126
+		}
127
+	}
128
+	return nil
129
+}
130
+
114 131
 func (v *localVolume) CreatedAt() (time.Time, error) {
115 132
 	fileInfo, err := os.Stat(v.path)
116 133
 	if err != nil {
... ...
@@ -32,10 +32,18 @@ func setOpts(v *localVolume, opts map[string]string) error {
32 32
 	return nil
33 33
 }
34 34
 
35
+func (v *localVolume) needsMount() bool {
36
+	return false
37
+}
38
+
35 39
 func (v *localVolume) mount() error {
36 40
 	return nil
37 41
 }
38 42
 
43
+func (v *localVolume) postMount() error {
44
+	return nil
45
+}
46
+
39 47
 func (v *localVolume) CreatedAt() (time.Time, error) {
40 48
 	fileInfo, err := os.Stat(v.path)
41 49
 	if err != nil {