Skip to content

Commit b05b237

Browse files
committed
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 <[email protected]>
1 parent 2453262 commit b05b237

7 files changed

Lines changed: 303 additions & 18 deletions

File tree

docs/reference/commandline/volume_create.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ parent = "smn_cli"
2121

2222
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:
2323

24-
$ docker volume create --name hello
25-
hello
24+
```bash
25+
$ docker volume create --name hello
26+
hello
2627

27-
$ docker run -d -v hello:/world busybox ls /world
28+
$ docker run -d -v hello:/world busybox ls /world
29+
```
2830

2931
The mount is created inside the container's `/world` directory. Docker does not support relative paths for mount points inside the container.
3032

@@ -42,16 +44,32 @@ If you specify a volume name already in use on the current driver, Docker assume
4244

4345
Some volume drivers may take options to customize the volume creation. Use the `-o` or `--opt` flags to pass driver options:
4446

45-
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
47+
```bash
48+
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
49+
```
4650

4751
These options are passed directly to the volume driver. Options for
4852
different volume drivers may do different things (or nothing at all).
4953

50-
*Note*: The built-in `local` volume driver does not currently accept any options.
54+
The built-in `local` driver on Windows does not support any options.
55+
56+
The built-in `local` driver on Linux accepts options similar to the linux `mount`
57+
command:
58+
59+
```bash
60+
$ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000
61+
```
62+
63+
Another example:
64+
65+
```bash
66+
$ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2
67+
```
68+
5169

5270
## Related information
5371

5472
* [volume inspect](volume_inspect.md)
5573
* [volume ls](volume_ls.md)
5674
* [volume rm](volume_rm.md)
57-
* [Understand Data Volumes](../../userguide/containers/dockervolumes.md)
75+
* [Understand Data Volumes](../../userguide/containers/dockervolumes.md)

integration-cli/docker_cli_volume_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,26 @@ func (s *DockerSuite) TestVolumeCliInspectTmplError(c *check.C) {
218218
c.Assert(exitCode, checker.Equals, 1, check.Commentf("Output: %s", out))
219219
c.Assert(out, checker.Contains, "Template parsing error")
220220
}
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+
}

man/docker-volume-create.1.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@ docker-volume-create - Create a new volume
1515

1616
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:
1717

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
2321

2422
The mount is created inside the container's `/src` directory. Docker doesn't not support relative paths for mount points inside the container.
2523

@@ -29,14 +27,22 @@ Multiple containers can use the same volume in the same time period. This is use
2927

3028
Some volume drivers may take options to customize the volume creation. Use the `-o` or `--opt` flags to pass driver options:
3129

32-
```
33-
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
34-
```
30+
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
3531

3632
These options are passed directly to the volume driver. Options for
3733
different volume drivers may do different things (or nothing at all).
3834

39-
*Note*: The built-in `local` volume driver does not currently accept any options.
35+
The built-in `local` driver on Windows does not support any options.
36+
37+
The built-in `local` driver on Linux accepts options similar to the linux `mount`
38+
command:
39+
40+
$ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000
41+
42+
Another example:
43+
44+
$ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2
45+
4046

4147
# OPTIONS
4248
**-d**, **--driver**="*local*"

volume/local/local.go

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
package local
55

66
import (
7+
"encoding/json"
78
"fmt"
89
"io/ioutil"
910
"os"
1011
"path/filepath"
1112
"sync"
1213

14+
"github.com/Sirupsen/logrus"
1315
"github.com/docker/docker/pkg/idtools"
16+
"github.com/docker/docker/pkg/mount"
1417
"github.com/docker/docker/utils"
1518
"github.com/docker/docker/volume"
1619
)
@@ -40,6 +43,11 @@ func (validationError) IsValidationError() bool {
4043
return true
4144
}
4245

46+
type activeMount struct {
47+
count uint64
48+
mounted bool
49+
}
50+
4351
// New instantiates a new Root instance with the provided scope. Scope
4452
// is the base path that the Root instance uses to store its
4553
// 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) {
6371
return nil, err
6472
}
6573

74+
mountInfos, err := mount.GetMounts()
75+
if err != nil {
76+
logrus.Debugf("error looking up mounts for local volume cleanup: %v", err)
77+
}
78+
6679
for _, d := range dirs {
6780
name := filepath.Base(d.Name())
68-
r.volumes[name] = &localVolume{
81+
v := &localVolume{
6982
driverName: r.Name(),
7083
name: name,
7184
path: r.DataPath(name),
7285
}
86+
r.volumes[name] = v
87+
if b, err := ioutil.ReadFile(filepath.Join(name, "opts.json")); err == nil {
88+
if err := json.Unmarshal(b, v.opts); err != nil {
89+
return nil, err
90+
}
91+
92+
// unmount anything that may still be mounted (for example, from an unclean shutdown)
93+
for _, info := range mountInfos {
94+
if info.Mountpoint == v.path {
95+
mount.Unmount(v.path)
96+
break
97+
}
98+
}
99+
}
73100
}
74101

75102
return r, nil
@@ -109,7 +136,7 @@ func (r *Root) Name() string {
109136
// Create creates a new volume.Volume with the provided name, creating
110137
// the underlying directory tree required for this volume in the
111138
// process.
112-
func (r *Root) Create(name string, _ map[string]string) (volume.Volume, error) {
139+
func (r *Root) Create(name string, opts map[string]string) (volume.Volume, error) {
113140
if err := r.validateName(name); err != nil {
114141
return nil, err
115142
}
@@ -129,11 +156,34 @@ func (r *Root) Create(name string, _ map[string]string) (volume.Volume, error) {
129156
}
130157
return nil, err
131158
}
159+
160+
var err error
161+
defer func() {
162+
if err != nil {
163+
os.RemoveAll(filepath.Dir(path))
164+
}
165+
}()
166+
132167
v = &localVolume{
133168
driverName: r.Name(),
134169
name: name,
135170
path: path,
136171
}
172+
173+
if opts != nil {
174+
if err = setOpts(v, opts); err != nil {
175+
return nil, err
176+
}
177+
var b []byte
178+
b, err = json.Marshal(v.opts)
179+
if err != nil {
180+
return nil, err
181+
}
182+
if err = ioutil.WriteFile(filepath.Join(filepath.Dir(path), "opts.json"), b, 600); err != nil {
183+
return nil, err
184+
}
185+
}
186+
137187
r.volumes[name] = v
138188
return v, nil
139189
}
@@ -210,6 +260,10 @@ type localVolume struct {
210260
path string
211261
// driverName is the name of the driver that created the volume.
212262
driverName string
263+
// opts is the parsed list of options used to create the volume
264+
opts *optsConfig
265+
// active refcounts the active mounts
266+
active activeMount
213267
}
214268

215269
// Name returns the name of the given Volume.
@@ -229,10 +283,42 @@ func (v *localVolume) Path() string {
229283

230284
// Mount implements the localVolume interface, returning the data location.
231285
func (v *localVolume) Mount() (string, error) {
286+
v.m.Lock()
287+
defer v.m.Unlock()
288+
if v.opts != nil {
289+
if !v.active.mounted {
290+
if err := v.mount(); err != nil {
291+
return "", err
292+
}
293+
v.active.mounted = true
294+
}
295+
v.active.count++
296+
}
232297
return v.path, nil
233298
}
234299

235300
// Umount is for satisfying the localVolume interface and does not do anything in this driver.
236301
func (v *localVolume) Unmount() error {
302+
v.m.Lock()
303+
defer v.m.Unlock()
304+
if v.opts != nil {
305+
v.active.count--
306+
if v.active.count == 0 {
307+
if err := mount.Unmount(v.path); err != nil {
308+
v.active.count++
309+
return err
310+
}
311+
v.active.mounted = false
312+
}
313+
}
314+
return nil
315+
}
316+
317+
func validateOpts(opts map[string]string) error {
318+
for opt := range opts {
319+
if !validOpts[opt] {
320+
return validationError{fmt.Errorf("invalid option key: %q", opt)}
321+
}
322+
}
237323
return nil
238324
}

volume/local/local_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"io/ioutil"
55
"os"
66
"runtime"
7+
"strings"
78
"testing"
9+
10+
"github.com/docker/docker/pkg/mount"
811
)
912

1013
func TestRemove(t *testing.T) {
@@ -151,3 +154,96 @@ func TestValidateName(t *testing.T) {
151154
}
152155
}
153156
}
157+
158+
func TestCreateWithOpts(t *testing.T) {
159+
if runtime.GOOS == "windows" {
160+
t.Skip()
161+
}
162+
163+
rootDir, err := ioutil.TempDir("", "local-volume-test")
164+
if err != nil {
165+
t.Fatal(err)
166+
}
167+
defer os.RemoveAll(rootDir)
168+
169+
r, err := New(rootDir, 0, 0)
170+
if err != nil {
171+
t.Fatal(err)
172+
}
173+
174+
if _, err := r.Create("test", map[string]string{"invalidopt": "notsupported"}); err == nil {
175+
t.Fatal("expected invalid opt to cause error")
176+
}
177+
178+
vol, err := r.Create("test", map[string]string{"device": "tmpfs", "type": "tmpfs", "o": "size=1m,uid=1000"})
179+
if err != nil {
180+
t.Fatal(err)
181+
}
182+
v := vol.(*localVolume)
183+
184+
dir, err := v.Mount()
185+
if err != nil {
186+
t.Fatal(err)
187+
}
188+
defer func() {
189+
if err := v.Unmount(); err != nil {
190+
t.Fatal(err)
191+
}
192+
}()
193+
194+
mountInfos, err := mount.GetMounts()
195+
if err != nil {
196+
t.Fatal(err)
197+
}
198+
199+
var found bool
200+
for _, info := range mountInfos {
201+
if info.Mountpoint == dir {
202+
found = true
203+
if info.Fstype != "tmpfs" {
204+
t.Fatalf("expected tmpfs mount, got %q", info.Fstype)
205+
}
206+
if info.Source != "tmpfs" {
207+
t.Fatalf("expected tmpfs mount, got %q", info.Source)
208+
}
209+
if !strings.Contains(info.VfsOpts, "uid=1000") {
210+
t.Fatalf("expected mount info to have uid=1000: %q", info.VfsOpts)
211+
}
212+
if !strings.Contains(info.VfsOpts, "size=1024k") {
213+
t.Fatalf("expected mount info to have size=1024k: %q", info.VfsOpts)
214+
}
215+
break
216+
}
217+
}
218+
219+
if !found {
220+
t.Fatal("mount not found")
221+
}
222+
223+
if v.active.count != 1 {
224+
t.Fatalf("Expected active mount count to be 1, got %d", v.active.count)
225+
}
226+
227+
// test double mount
228+
if _, err := v.Mount(); err != nil {
229+
t.Fatal(err)
230+
}
231+
if v.active.count != 2 {
232+
t.Fatalf("Expected active mount count to be 2, got %d", v.active.count)
233+
}
234+
235+
if err := v.Unmount(); err != nil {
236+
t.Fatal(err)
237+
}
238+
if v.active.count != 1 {
239+
t.Fatalf("Expected active mount count to be 1, got %d", v.active.count)
240+
}
241+
242+
mounted, err := mount.Mounted(v.path)
243+
if err != nil {
244+
t.Fatal(err)
245+
}
246+
if !mounted {
247+
t.Fatal("expected mount to still be active")
248+
}
249+
}

0 commit comments

Comments
 (0)