Selaa lähdekoodia

Merge pull request #20262 from cpuguy83/implemnt_mount_opts_for_local_driver

Support mount opts for `local` volume driver
David Calavera 9 vuotta sitten
vanhempi
commit
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:
 
-    $ 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)

+ 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(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:
 
-  ```
-  $ 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*"

+ 88 - 2
volume/local/local.go

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

+ 96 - 0
volume/local/local_test.go

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

+ 41 - 1
volume/local/local_unix.go

@@ -6,11 +6,28 @@
 package local
 
 import (
+	"fmt"
 	"path/filepath"
 	"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
 // 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)
+}

+ 16 - 0
volume/local/local_windows.go

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