Browse code

Support mount opts for `local` volume driver

Allows users to submit options similar to the `mount` command when
creating a volume with the `local` volume driver.

For example:

```go
$ docker volume create -d local --opt type=nfs --opt device=myNfsServer:/data --opt o=noatime,nosuid
```

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2016/02/12 11:48:16
Showing 7 changed files
... ...
@@ -21,10 +21,12 @@ parent = "smn_cli"
21 21
 
22 22
 Creates a new volume that containers can consume and store data in. If a name is not specified, Docker generates a random name. You create a volume and then configure the container to use it, for example:
23 23
 
24
-    $ docker volume create --name hello
25
-    hello
24
+```bash
25
+$ docker volume create --name hello
26
+hello
26 27
 
27
-    $ docker run -d -v hello:/world busybox ls /world
28
+$ docker run -d -v hello:/world busybox ls /world
29
+```
28 30
 
29 31
 The mount is created inside the container's `/world` directory. Docker does not support relative paths for mount points inside the container.
30 32
 
... ...
@@ -42,16 +44,32 @@ If you specify a volume name already in use on the current driver, Docker assume
42 42
 
43 43
 Some volume drivers may take options to customize the volume creation. Use the `-o` or `--opt` flags to pass driver options:
44 44
 
45
-    $ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
45
+```bash
46
+$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
47
+```
46 48
 
47 49
 These options are passed directly to the volume driver. Options for
48 50
 different volume drivers may do different things (or nothing at all).
49 51
 
50
-*Note*: The built-in `local` volume driver does not currently accept any options.
52
+The built-in `local` driver on Windows does not support any options.
53
+
54
+The built-in `local` driver on Linux accepts options similar to the linux `mount`
55
+command:
56
+
57
+```bash
58
+$ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000
59
+```
60
+
61
+Another example:
62
+
63
+```bash
64
+$ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2
65
+```
66
+
51 67
 
52 68
 ## Related information
53 69
 
54 70
 * [volume inspect](volume_inspect.md)
55 71
 * [volume ls](volume_ls.md)
56 72
 * [volume rm](volume_rm.md)
57
-* [Understand Data Volumes](../../userguide/containers/dockervolumes.md)
58 73
\ No newline at end of file
74
+* [Understand Data Volumes](../../userguide/containers/dockervolumes.md)
... ...
@@ -218,3 +218,26 @@ func (s *DockerSuite) TestVolumeCliInspectTmplError(c *check.C) {
218 218
 	c.Assert(exitCode, checker.Equals, 1, check.Commentf("Output: %s", out))
219 219
 	c.Assert(out, checker.Contains, "Template parsing error")
220 220
 }
221
+
222
+func (s *DockerSuite) TestVolumeCliCreateWithOpts(c *check.C) {
223
+	testRequires(c, DaemonIsLinux)
224
+
225
+	dockerCmd(c, "volume", "create", "-d", "local", "--name", "test", "--opt=type=tmpfs", "--opt=device=tmpfs", "--opt=o=size=1m,uid=1000")
226
+	out, _ := dockerCmd(c, "run", "-v", "test:/foo", "busybox", "mount")
227
+
228
+	mounts := strings.Split(out, "\n")
229
+	var found bool
230
+	for _, m := range mounts {
231
+		if strings.Contains(m, "/foo") {
232
+			found = true
233
+			info := strings.Fields(m)
234
+			// tmpfs on <path> type tmpfs (rw,relatime,size=1024k,uid=1000)
235
+			c.Assert(info[0], checker.Equals, "tmpfs")
236
+			c.Assert(info[2], checker.Equals, "/foo")
237
+			c.Assert(info[4], checker.Equals, "tmpfs")
238
+			c.Assert(info[5], checker.Contains, "uid=1000")
239
+			c.Assert(info[5], checker.Contains, "size=1024k")
240
+		}
241
+	}
242
+	c.Assert(found, checker.Equals, true)
243
+}
... ...
@@ -15,11 +15,9 @@ docker-volume-create - Create a new volume
15 15
 
16 16
 Creates a new volume that containers can consume and store data in. If a name is not specified, Docker generates a random name. You create a volume and then configure the container to use it, for example:
17 17
 
18
-  ```
19
-  $ docker volume create --name hello
20
-  hello
21
-  $ docker run -d -v hello:/world busybox ls /world
22
-  ```
18
+    $ docker volume create --name hello
19
+    hello
20
+    $ docker run -d -v hello:/world busybox ls /world
23 21
 
24 22
 The mount is created inside the container's `/src` directory. Docker doesn't not support relative paths for mount points inside the container. 
25 23
 
... ...
@@ -29,14 +27,22 @@ Multiple containers can use the same volume in the same time period. This is use
29 29
 
30 30
 Some volume drivers may take options to customize the volume creation. Use the `-o` or `--opt` flags to pass driver options:
31 31
 
32
-  ```
33
-  $ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
34
-  ```
32
+    $ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
35 33
 
36 34
 These options are passed directly to the volume driver. Options for
37 35
 different volume drivers may do different things (or nothing at all).
38 36
 
39
-*Note*: The built-in `local` volume driver does not currently accept any options.
37
+The built-in `local` driver on Windows does not support any options.
38
+
39
+The built-in `local` driver on Linux accepts options similar to the linux `mount`
40
+command:
41
+
42
+    $ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000
43
+
44
+Another example:
45
+
46
+    $ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2
47
+
40 48
 
41 49
 # OPTIONS
42 50
 **-d**, **--driver**="*local*"
... ...
@@ -4,13 +4,16 @@
4 4
 package local
5 5
 
6 6
 import (
7
+	"encoding/json"
7 8
 	"fmt"
8 9
 	"io/ioutil"
9 10
 	"os"
10 11
 	"path/filepath"
11 12
 	"sync"
12 13
 
14
+	"github.com/Sirupsen/logrus"
13 15
 	"github.com/docker/docker/pkg/idtools"
16
+	"github.com/docker/docker/pkg/mount"
14 17
 	"github.com/docker/docker/utils"
15 18
 	"github.com/docker/docker/volume"
16 19
 )
... ...
@@ -40,6 +43,11 @@ func (validationError) IsValidationError() bool {
40 40
 	return true
41 41
 }
42 42
 
43
+type activeMount struct {
44
+	count   uint64
45
+	mounted bool
46
+}
47
+
43 48
 // New instantiates a new Root instance with the provided scope. Scope
44 49
 // is the base path that the Root instance uses to store its
45 50
 // volumes. The base path is created here if it does not exist.
... ...
@@ -63,13 +71,32 @@ func New(scope string, rootUID, rootGID int) (*Root, error) {
63 63
 		return nil, err
64 64
 	}
65 65
 
66
+	mountInfos, err := mount.GetMounts()
67
+	if err != nil {
68
+		logrus.Debugf("error looking up mounts for local volume cleanup: %v", err)
69
+	}
70
+
66 71
 	for _, d := range dirs {
67 72
 		name := filepath.Base(d.Name())
68
-		r.volumes[name] = &localVolume{
73
+		v := &localVolume{
69 74
 			driverName: r.Name(),
70 75
 			name:       name,
71 76
 			path:       r.DataPath(name),
72 77
 		}
78
+		r.volumes[name] = v
79
+		if b, err := ioutil.ReadFile(filepath.Join(name, "opts.json")); err == nil {
80
+			if err := json.Unmarshal(b, v.opts); err != nil {
81
+				return nil, err
82
+			}
83
+
84
+			// unmount anything that may still be mounted (for example, from an unclean shutdown)
85
+			for _, info := range mountInfos {
86
+				if info.Mountpoint == v.path {
87
+					mount.Unmount(v.path)
88
+					break
89
+				}
90
+			}
91
+		}
73 92
 	}
74 93
 
75 94
 	return r, nil
... ...
@@ -109,7 +136,7 @@ func (r *Root) Name() string {
109 109
 // Create creates a new volume.Volume with the provided name, creating
110 110
 // the underlying directory tree required for this volume in the
111 111
 // process.
112
-func (r *Root) Create(name string, _ map[string]string) (volume.Volume, error) {
112
+func (r *Root) Create(name string, opts map[string]string) (volume.Volume, error) {
113 113
 	if err := r.validateName(name); err != nil {
114 114
 		return nil, err
115 115
 	}
... ...
@@ -129,11 +156,34 @@ func (r *Root) Create(name string, _ map[string]string) (volume.Volume, error) {
129 129
 		}
130 130
 		return nil, err
131 131
 	}
132
+
133
+	var err error
134
+	defer func() {
135
+		if err != nil {
136
+			os.RemoveAll(filepath.Dir(path))
137
+		}
138
+	}()
139
+
132 140
 	v = &localVolume{
133 141
 		driverName: r.Name(),
134 142
 		name:       name,
135 143
 		path:       path,
136 144
 	}
145
+
146
+	if opts != nil {
147
+		if err = setOpts(v, opts); err != nil {
148
+			return nil, err
149
+		}
150
+		var b []byte
151
+		b, err = json.Marshal(v.opts)
152
+		if err != nil {
153
+			return nil, err
154
+		}
155
+		if err = ioutil.WriteFile(filepath.Join(filepath.Dir(path), "opts.json"), b, 600); err != nil {
156
+			return nil, err
157
+		}
158
+	}
159
+
137 160
 	r.volumes[name] = v
138 161
 	return v, nil
139 162
 }
... ...
@@ -210,6 +260,10 @@ type localVolume struct {
210 210
 	path string
211 211
 	// driverName is the name of the driver that created the volume.
212 212
 	driverName string
213
+	// opts is the parsed list of options used to create the volume
214
+	opts *optsConfig
215
+	// active refcounts the active mounts
216
+	active activeMount
213 217
 }
214 218
 
215 219
 // Name returns the name of the given Volume.
... ...
@@ -229,10 +283,42 @@ func (v *localVolume) Path() string {
229 229
 
230 230
 // Mount implements the localVolume interface, returning the data location.
231 231
 func (v *localVolume) Mount() (string, error) {
232
+	v.m.Lock()
233
+	defer v.m.Unlock()
234
+	if v.opts != nil {
235
+		if !v.active.mounted {
236
+			if err := v.mount(); err != nil {
237
+				return "", err
238
+			}
239
+			v.active.mounted = true
240
+		}
241
+		v.active.count++
242
+	}
232 243
 	return v.path, nil
233 244
 }
234 245
 
235 246
 // Umount is for satisfying the localVolume interface and does not do anything in this driver.
236 247
 func (v *localVolume) Unmount() error {
248
+	v.m.Lock()
249
+	defer v.m.Unlock()
250
+	if v.opts != nil {
251
+		v.active.count--
252
+		if v.active.count == 0 {
253
+			if err := mount.Unmount(v.path); err != nil {
254
+				v.active.count++
255
+				return err
256
+			}
257
+			v.active.mounted = false
258
+		}
259
+	}
260
+	return nil
261
+}
262
+
263
+func validateOpts(opts map[string]string) error {
264
+	for opt := range opts {
265
+		if !validOpts[opt] {
266
+			return validationError{fmt.Errorf("invalid option key: %q", opt)}
267
+		}
268
+	}
237 269
 	return nil
238 270
 }
... ...
@@ -4,7 +4,10 @@ import (
4 4
 	"io/ioutil"
5 5
 	"os"
6 6
 	"runtime"
7
+	"strings"
7 8
 	"testing"
9
+
10
+	"github.com/docker/docker/pkg/mount"
8 11
 )
9 12
 
10 13
 func TestRemove(t *testing.T) {
... ...
@@ -151,3 +154,96 @@ func TestValidateName(t *testing.T) {
151 151
 		}
152 152
 	}
153 153
 }
154
+
155
+func TestCreateWithOpts(t *testing.T) {
156
+	if runtime.GOOS == "windows" {
157
+		t.Skip()
158
+	}
159
+
160
+	rootDir, err := ioutil.TempDir("", "local-volume-test")
161
+	if err != nil {
162
+		t.Fatal(err)
163
+	}
164
+	defer os.RemoveAll(rootDir)
165
+
166
+	r, err := New(rootDir, 0, 0)
167
+	if err != nil {
168
+		t.Fatal(err)
169
+	}
170
+
171
+	if _, err := r.Create("test", map[string]string{"invalidopt": "notsupported"}); err == nil {
172
+		t.Fatal("expected invalid opt to cause error")
173
+	}
174
+
175
+	vol, err := r.Create("test", map[string]string{"device": "tmpfs", "type": "tmpfs", "o": "size=1m,uid=1000"})
176
+	if err != nil {
177
+		t.Fatal(err)
178
+	}
179
+	v := vol.(*localVolume)
180
+
181
+	dir, err := v.Mount()
182
+	if err != nil {
183
+		t.Fatal(err)
184
+	}
185
+	defer func() {
186
+		if err := v.Unmount(); err != nil {
187
+			t.Fatal(err)
188
+		}
189
+	}()
190
+
191
+	mountInfos, err := mount.GetMounts()
192
+	if err != nil {
193
+		t.Fatal(err)
194
+	}
195
+
196
+	var found bool
197
+	for _, info := range mountInfos {
198
+		if info.Mountpoint == dir {
199
+			found = true
200
+			if info.Fstype != "tmpfs" {
201
+				t.Fatalf("expected tmpfs mount, got %q", info.Fstype)
202
+			}
203
+			if info.Source != "tmpfs" {
204
+				t.Fatalf("expected tmpfs mount, got %q", info.Source)
205
+			}
206
+			if !strings.Contains(info.VfsOpts, "uid=1000") {
207
+				t.Fatalf("expected mount info to have uid=1000: %q", info.VfsOpts)
208
+			}
209
+			if !strings.Contains(info.VfsOpts, "size=1024k") {
210
+				t.Fatalf("expected mount info to have size=1024k: %q", info.VfsOpts)
211
+			}
212
+			break
213
+		}
214
+	}
215
+
216
+	if !found {
217
+		t.Fatal("mount not found")
218
+	}
219
+
220
+	if v.active.count != 1 {
221
+		t.Fatalf("Expected active mount count to be 1, got %d", v.active.count)
222
+	}
223
+
224
+	// test double mount
225
+	if _, err := v.Mount(); err != nil {
226
+		t.Fatal(err)
227
+	}
228
+	if v.active.count != 2 {
229
+		t.Fatalf("Expected active mount count to be 2, got %d", v.active.count)
230
+	}
231
+
232
+	if err := v.Unmount(); err != nil {
233
+		t.Fatal(err)
234
+	}
235
+	if v.active.count != 1 {
236
+		t.Fatalf("Expected active mount count to be 1, got %d", v.active.count)
237
+	}
238
+
239
+	mounted, err := mount.Mounted(v.path)
240
+	if err != nil {
241
+		t.Fatal(err)
242
+	}
243
+	if !mounted {
244
+		t.Fatal("expected mount to still be active")
245
+	}
246
+}
... ...
@@ -6,11 +6,28 @@
6 6
 package local
7 7
 
8 8
 import (
9
+	"fmt"
9 10
 	"path/filepath"
10 11
 	"strings"
12
+
13
+	"github.com/docker/docker/pkg/mount"
14
+)
15
+
16
+var (
17
+	oldVfsDir = filepath.Join("vfs", "dir")
18
+
19
+	validOpts = map[string]bool{
20
+		"type":   true, // specify the filesystem type for mount, e.g. nfs
21
+		"o":      true, // generic mount options
22
+		"device": true, // device to mount from
23
+	}
11 24
 )
12 25
 
13
-var oldVfsDir = filepath.Join("vfs", "dir")
26
+type optsConfig struct {
27
+	MountType   string
28
+	MountOpts   string
29
+	MountDevice string
30
+}
14 31
 
15 32
 // scopedPath verifies that the path where the volume is located
16 33
 // is under Docker's root and the valid local paths.
... ...
@@ -27,3 +44,26 @@ func (r *Root) scopedPath(realPath string) bool {
27 27
 
28 28
 	return false
29 29
 }
30
+
31
+func setOpts(v *localVolume, opts map[string]string) error {
32
+	if len(opts) == 0 {
33
+		return nil
34
+	}
35
+	if err := validateOpts(opts); err != nil {
36
+		return err
37
+	}
38
+
39
+	v.opts = &optsConfig{
40
+		MountType:   opts["type"],
41
+		MountOpts:   opts["o"],
42
+		MountDevice: opts["device"],
43
+	}
44
+	return nil
45
+}
46
+
47
+func (v *localVolume) mount() error {
48
+	if v.opts.MountDevice == "" {
49
+		return fmt.Errorf("missing device in volume options")
50
+	}
51
+	return mount.Mount(v.opts.MountDevice, v.path, v.opts.MountType, v.opts.MountOpts)
52
+}
... ...
@@ -4,10 +4,15 @@
4 4
 package local
5 5
 
6 6
 import (
7
+	"fmt"
7 8
 	"path/filepath"
8 9
 	"strings"
9 10
 )
10 11
 
12
+type optsConfig struct{}
13
+
14
+var validOpts map[string]bool
15
+
11 16
 // scopedPath verifies that the path where the volume is located
12 17
 // is under Docker's root and the valid local paths.
13 18
 func (r *Root) scopedPath(realPath string) bool {
... ...
@@ -16,3 +21,14 @@ func (r *Root) scopedPath(realPath string) bool {
16 16
 	}
17 17
 	return false
18 18
 }
19
+
20
+func setOpts(v *localVolume, opts map[string]string) error {
21
+	if len(opts) > 0 {
22
+		return fmt.Errorf("options are not supported on this platform")
23
+	}
24
+	return nil
25
+}
26
+
27
+func (v *localVolume) mount() error {
28
+	return nil
29
+}