Merge pull request #20262 from cpuguy83/implemnt_mount_opts_for_local_driver

Support mount opts for `local` volume driver
This commit is contained in:
David Calavera 2016-03-03 09:02:12 -08:00
commit c4be28d6a8
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: 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 ```bash
hello $ 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. The mount is created inside the container's `/world` directory. Docker does not support relative paths for mount points inside the container.
@ -42,12 +44,28 @@ 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: 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 These options are passed directly to the volume driver. Options for
different volume drivers may do different things (or nothing at all). 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 ## Related information

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(exitCode, checker.Equals, 1, check.Commentf("Output: %s", out))
c.Assert(out, checker.Contains, "Template parsing error") 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: 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
$ docker volume create --name hello 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 `/src` directory. Docker doesn't not support relative paths for mount points inside the container. 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: 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 These options are passed directly to the volume driver. Options for
different volume drivers may do different things (or nothing at all). 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 # OPTIONS
**-d**, **--driver**="*local*" **-d**, **--driver**="*local*"

View File

@ -4,13 +4,16 @@
package local package local
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/mount"
"github.com/docker/docker/utils" "github.com/docker/docker/utils"
"github.com/docker/docker/volume" "github.com/docker/docker/volume"
) )
@ -40,6 +43,11 @@ func (validationError) IsValidationError() bool {
return true return true
} }
type activeMount struct {
count uint64
mounted bool
}
// New instantiates a new Root instance with the provided scope. Scope // New instantiates a new Root instance with the provided scope. Scope
// is the base path that the Root instance uses to store its // is the base path that the Root instance uses to store its
// volumes. The base path is created here if it does not exist. // 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 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 { for _, d := range dirs {
name := filepath.Base(d.Name()) name := filepath.Base(d.Name())
r.volumes[name] = &localVolume{ v := &localVolume{
driverName: r.Name(), driverName: r.Name(),
name: name, name: name,
path: r.DataPath(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 return r, nil
@ -109,7 +136,7 @@ func (r *Root) Name() string {
// Create creates a new volume.Volume with the provided name, creating // Create creates a new volume.Volume with the provided name, creating
// the underlying directory tree required for this volume in the // the underlying directory tree required for this volume in the
// process. // 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 { if err := r.validateName(name); err != nil {
return nil, err return nil, err
} }
@ -129,11 +156,34 @@ func (r *Root) Create(name string, _ map[string]string) (volume.Volume, error) {
} }
return nil, err return nil, err
} }
var err error
defer func() {
if err != nil {
os.RemoveAll(filepath.Dir(path))
}
}()
v = &localVolume{ v = &localVolume{
driverName: r.Name(), driverName: r.Name(),
name: name, name: name,
path: path, 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 r.volumes[name] = v
return v, nil return v, nil
} }
@ -210,6 +260,10 @@ type localVolume struct {
path string path string
// driverName is the name of the driver that created the volume. // driverName is the name of the driver that created the volume.
driverName string 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. // 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. // Mount implements the localVolume interface, returning the data location.
func (v *localVolume) Mount() (string, error) { 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 return v.path, nil
} }
// Umount is for satisfying the localVolume interface and does not do anything in this driver. // Umount is for satisfying the localVolume interface and does not do anything in this driver.
func (v *localVolume) Unmount() error { 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 return nil
} }

View File

@ -4,7 +4,10 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"runtime" "runtime"
"strings"
"testing" "testing"
"github.com/docker/docker/pkg/mount"
) )
func TestRemove(t *testing.T) { 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 package local
import ( import (
"fmt"
"path/filepath" "path/filepath"
"strings" "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 // scopedPath verifies that the path where the volume is located
// is under Docker's root and the valid local paths. // is under Docker's root and the valid local paths.
@ -27,3 +44,26 @@ func (r *Root) scopedPath(realPath string) bool {
return false 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 package local
import ( import (
"fmt"
"path/filepath" "path/filepath"
"strings" "strings"
) )
type optsConfig struct{}
var validOpts map[string]bool
// scopedPath verifies that the path where the volume is located // scopedPath verifies that the path where the volume is located
// is under Docker's root and the valid local paths. // is under Docker's root and the valid local paths.
func (r *Root) scopedPath(realPath string) bool { func (r *Root) scopedPath(realPath string) bool {
@ -16,3 +21,14 @@ func (r *Root) scopedPath(realPath string) bool {
} }
return false 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
}