1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

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>
This commit is contained in:
Brian Goff 2016-02-11 21:48:16 -05:00
parent 2453262e7b
commit b05b237075
7 changed files with 303 additions and 18 deletions

View file

@ -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.
@ -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)

View file

@ -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)
}

View file

@ -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.
@ -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*"

View file

@ -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"
)
@ -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.
@ -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
@ -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
}
@ -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
}
@ -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.
@ -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
}

View file

@ -4,7 +4,10 @@ import (
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"github.com/docker/docker/pkg/mount"
)
func TestRemove(t *testing.T) {
@ -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")
}
}

View file

@ -6,11 +6,28 @@
package local
import (
"fmt"
"path/filepath"
"strings"
"github.com/docker/docker/pkg/mount"
)
var oldVfsDir = filepath.Join("vfs", "dir")
var (
oldVfsDir = filepath.Join("vfs", "dir")
validOpts = map[string]bool{
"type": true, // specify the filesystem type for mount, e.g. nfs
"o": true, // generic mount options
"device": true, // device to mount from
}
)
type optsConfig struct {
MountType string
MountOpts string
MountDevice string
}
// scopedPath verifies that the path where the volume is located
// is under Docker's root and the valid local paths.
@ -27,3 +44,26 @@ func (r *Root) scopedPath(realPath string) bool {
return false
}
func setOpts(v *localVolume, opts map[string]string) error {
if len(opts) == 0 {
return nil
}
if err := validateOpts(opts); err != nil {
return err
}
v.opts = &optsConfig{
MountType: opts["type"],
MountOpts: opts["o"],
MountDevice: opts["device"],
}
return nil
}
func (v *localVolume) mount() error {
if v.opts.MountDevice == "" {
return fmt.Errorf("missing device in volume options")
}
return mount.Mount(v.opts.MountDevice, v.path, v.opts.MountType, v.opts.MountOpts)
}

View file

@ -4,10 +4,15 @@
package local
import (
"fmt"
"path/filepath"
"strings"
)
type optsConfig struct{}
var validOpts map[string]bool
// scopedPath verifies that the path where the volume is located
// is under Docker's root and the valid local paths.
func (r *Root) scopedPath(realPath string) bool {
@ -16,3 +21,14 @@ func (r *Root) scopedPath(realPath string) bool {
}
return false
}
func setOpts(v *localVolume, opts map[string]string) error {
if len(opts) > 0 {
return fmt.Errorf("options are not supported on this platform")
}
return nil
}
func (v *localVolume) mount() error {
return nil
}