Skip to content

Commit

Permalink
Support mount opts for local volume driver
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
cpuguy83 committed Mar 3, 2016
1 parent 2453262 commit b05b237
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 18 deletions.
30 changes: 24 additions & 6 deletions docs/reference/commandline/volume_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ parent = "smn_cli"

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:

$ docker volume create --name hello
hello
```bash
$ docker volume create --name hello
hello

$ docker run -d -v hello:/world busybox ls /world
$ docker run -d -v hello:/world busybox ls /world
```

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

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

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

$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
```bash
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
```

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

*Note*: The built-in `local` volume driver does not currently accept any options.
The built-in `local` driver on Windows does not support any options.

The built-in `local` driver on Linux accepts options similar to the linux `mount`
command:

```bash
$ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000
```

Another example:

```bash
$ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2
```


## Related information

* [volume inspect](volume_inspect.md)
* [volume ls](volume_ls.md)
* [volume rm](volume_rm.md)
* [Understand Data Volumes](../../userguide/containers/dockervolumes.md)
* [Understand Data Volumes](../../userguide/containers/dockervolumes.md)
23 changes: 23 additions & 0 deletions integration-cli/docker_cli_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,26 @@ func (s *DockerSuite) TestVolumeCliInspectTmplError(c *check.C) {
c.Assert(exitCode, checker.Equals, 1, check.Commentf("Output: %s", out))
c.Assert(out, checker.Contains, "Template parsing error")
}

func (s *DockerSuite) TestVolumeCliCreateWithOpts(c *check.C) {
testRequires(c, DaemonIsLinux)

dockerCmd(c, "volume", "create", "-d", "local", "--name", "test", "--opt=type=tmpfs", "--opt=device=tmpfs", "--opt=o=size=1m,uid=1000")
out, _ := dockerCmd(c, "run", "-v", "test:/foo", "busybox", "mount")

mounts := strings.Split(out, "\n")
var found bool
for _, m := range mounts {
if strings.Contains(m, "/foo") {
found = true
info := strings.Fields(m)
// tmpfs on <path> type tmpfs (rw,relatime,size=1024k,uid=1000)
c.Assert(info[0], checker.Equals, "tmpfs")
c.Assert(info[2], checker.Equals, "/foo")
c.Assert(info[4], checker.Equals, "tmpfs")
c.Assert(info[5], checker.Contains, "uid=1000")
c.Assert(info[5], checker.Contains, "size=1024k")
}
}
c.Assert(found, checker.Equals, true)
}
24 changes: 15 additions & 9 deletions man/docker-volume-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ docker-volume-create - Create a new volume

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:

```
$ docker volume create --name hello
hello
$ docker run -d -v hello:/world busybox ls /world
```
$ docker volume create --name hello
hello
$ docker run -d -v hello:/world busybox ls /world

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

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

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

```
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
```
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey

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

*Note*: The built-in `local` volume driver does not currently accept any options.
The built-in `local` driver on Windows does not support any options.

The built-in `local` driver on Linux accepts options similar to the linux `mount`
command:

$ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000

Another example:

$ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2


# OPTIONS
**-d**, **--driver**="*local*"
Expand Down
90 changes: 88 additions & 2 deletions volume/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
package local

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/mount"
"github.com/docker/docker/utils"
"github.com/docker/docker/volume"
)
Expand Down Expand Up @@ -40,6 +43,11 @@ func (validationError) IsValidationError() bool {
return true
}

type activeMount struct {
count uint64
mounted bool
}

// New instantiates a new Root instance with the provided scope. Scope
// is the base path that the Root instance uses to store its
// volumes. The base path is created here if it does not exist.
Expand All @@ -63,13 +71,32 @@ func New(scope string, rootUID, rootGID int) (*Root, error) {
return nil, err
}

mountInfos, err := mount.GetMounts()
if err != nil {
logrus.Debugf("error looking up mounts for local volume cleanup: %v", err)
}

for _, d := range dirs {
name := filepath.Base(d.Name())
r.volumes[name] = &localVolume{
v := &localVolume{
driverName: r.Name(),
name: name,
path: r.DataPath(name),
}
r.volumes[name] = v
if b, err := ioutil.ReadFile(filepath.Join(name, "opts.json")); err == nil {
if err := json.Unmarshal(b, v.opts); err != nil {
return nil, err
}

// unmount anything that may still be mounted (for example, from an unclean shutdown)
for _, info := range mountInfos {
if info.Mountpoint == v.path {
mount.Unmount(v.path)
break
}
}
}
}

return r, nil
Expand Down Expand Up @@ -109,7 +136,7 @@ func (r *Root) Name() string {
// Create creates a new volume.Volume with the provided name, creating
// the underlying directory tree required for this volume in the
// process.
func (r *Root) Create(name string, _ map[string]string) (volume.Volume, error) {
func (r *Root) Create(name string, opts map[string]string) (volume.Volume, error) {
if err := r.validateName(name); err != nil {
return nil, err
}
Expand All @@ -129,11 +156,34 @@ func (r *Root) Create(name string, _ map[string]string) (volume.Volume, error) {
}
return nil, err
}

var err error
defer func() {
if err != nil {
os.RemoveAll(filepath.Dir(path))
}
}()

v = &localVolume{
driverName: r.Name(),
name: name,
path: path,
}

if opts != nil {
if err = setOpts(v, opts); err != nil {
return nil, err
}
var b []byte
b, err = json.Marshal(v.opts)
if err != nil {
return nil, err
}
if err = ioutil.WriteFile(filepath.Join(filepath.Dir(path), "opts.json"), b, 600); err != nil {
return nil, err
}
}

r.volumes[name] = v
return v, nil
}
Expand Down Expand Up @@ -210,6 +260,10 @@ type localVolume struct {
path string
// driverName is the name of the driver that created the volume.
driverName string
// opts is the parsed list of options used to create the volume
opts *optsConfig
// active refcounts the active mounts
active activeMount
}

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

// Mount implements the localVolume interface, returning the data location.
func (v *localVolume) Mount() (string, error) {
v.m.Lock()
defer v.m.Unlock()
if v.opts != nil {
if !v.active.mounted {
if err := v.mount(); err != nil {
return "", err
}
v.active.mounted = true
}
v.active.count++
}
return v.path, nil
}

// Umount is for satisfying the localVolume interface and does not do anything in this driver.
func (v *localVolume) Unmount() error {
v.m.Lock()
defer v.m.Unlock()
if v.opts != nil {
v.active.count--
if v.active.count == 0 {
if err := mount.Unmount(v.path); err != nil {
v.active.count++
return err
}
v.active.mounted = false
}
}
return nil
}

func validateOpts(opts map[string]string) error {
for opt := range opts {
if !validOpts[opt] {
return validationError{fmt.Errorf("invalid option key: %q", opt)}
}
}
return nil
}
96 changes: 96 additions & 0 deletions volume/local/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"io/ioutil"
"os"
"runtime"
"strings"
"testing"

"github.com/docker/docker/pkg/mount"
)

func TestRemove(t *testing.T) {
Expand Down Expand Up @@ -151,3 +154,96 @@ func TestValidateName(t *testing.T) {
}
}
}

func TestCreateWithOpts(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
}

rootDir, err := ioutil.TempDir("", "local-volume-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootDir)

r, err := New(rootDir, 0, 0)
if err != nil {
t.Fatal(err)
}

if _, err := r.Create("test", map[string]string{"invalidopt": "notsupported"}); err == nil {
t.Fatal("expected invalid opt to cause error")
}

vol, err := r.Create("test", map[string]string{"device": "tmpfs", "type": "tmpfs", "o": "size=1m,uid=1000"})
if err != nil {
t.Fatal(err)
}
v := vol.(*localVolume)

dir, err := v.Mount()
if err != nil {
t.Fatal(err)
}
defer func() {
if err := v.Unmount(); err != nil {
t.Fatal(err)
}
}()

mountInfos, err := mount.GetMounts()
if err != nil {
t.Fatal(err)
}

var found bool
for _, info := range mountInfos {
if info.Mountpoint == dir {
found = true
if info.Fstype != "tmpfs" {
t.Fatalf("expected tmpfs mount, got %q", info.Fstype)
}
if info.Source != "tmpfs" {
t.Fatalf("expected tmpfs mount, got %q", info.Source)
}
if !strings.Contains(info.VfsOpts, "uid=1000") {
t.Fatalf("expected mount info to have uid=1000: %q", info.VfsOpts)
}
if !strings.Contains(info.VfsOpts, "size=1024k") {
t.Fatalf("expected mount info to have size=1024k: %q", info.VfsOpts)
}
break
}
}

if !found {
t.Fatal("mount not found")
}

if v.active.count != 1 {
t.Fatalf("Expected active mount count to be 1, got %d", v.active.count)
}

// test double mount
if _, err := v.Mount(); err != nil {
t.Fatal(err)
}
if v.active.count != 2 {
t.Fatalf("Expected active mount count to be 2, got %d", v.active.count)
}

if err := v.Unmount(); err != nil {
t.Fatal(err)
}
if v.active.count != 1 {
t.Fatalf("Expected active mount count to be 1, got %d", v.active.count)
}

mounted, err := mount.Mounted(v.path)
if err != nil {
t.Fatal(err)
}
if !mounted {
t.Fatal("expected mount to still be active")
}
}
Loading

0 comments on commit b05b237

Please sign in to comment.