瀏覽代碼

Merge pull request #20262 from cpuguy83/implemnt_mount_opts_for_local_driver

Support mount opts for `local` volume driver
David Calavera 9 年之前
父節點
當前提交
c4be28d6a8

+ 24 - 6
docs/reference/commandline/volume_create.md

@@ -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
-    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.
 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:
 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
 
 
 * [volume inspect](volume_inspect.md)
 * [volume inspect](volume_inspect.md)
 * [volume ls](volume_ls.md)
 * [volume ls](volume_ls.md)
 * [volume rm](volume_rm.md)
 * [volume rm](volume_rm.md)
-* [Understand Data Volumes](../../userguide/containers/dockervolumes.md)
+* [Understand Data Volumes](../../userguide/containers/dockervolumes.md)

+ 23 - 0
integration-cli/docker_cli_volume_test.go

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

+ 15 - 9
man/docker-volume-create.1.md

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

+ 88 - 2
volume/local/local.go

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

+ 96 - 0
volume/local/local_test.go

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

+ 41 - 1
volume/local/local_unix.go

@@ -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")
+
+	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
+	}
 )
 )
 
 
-var oldVfsDir = filepath.Join("vfs", "dir")
+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)
+}

+ 16 - 0
volume/local/local_windows.go

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