From b05b2370757d7143d761e5e6abb8c0f9b009f737 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 11 Feb 2016 21:48:16 -0500 Subject: [PATCH] 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 --- docs/reference/commandline/volume_create.md | 30 +++++-- integration-cli/docker_cli_volume_test.go | 23 +++++ man/docker-volume-create.1.md | 24 ++++-- volume/local/local.go | 90 ++++++++++++++++++- volume/local/local_test.go | 96 +++++++++++++++++++++ volume/local/local_unix.go | 42 ++++++++- volume/local/local_windows.go | 16 ++++ 7 files changed, 303 insertions(+), 18 deletions(-) diff --git a/docs/reference/commandline/volume_create.md b/docs/reference/commandline/volume_create.md index 79e794698f8b0..da2c66de80e87 100644 --- a/docs/reference/commandline/volume_create.md +++ b/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) \ No newline at end of file +* [Understand Data Volumes](../../userguide/containers/dockervolumes.md) diff --git a/integration-cli/docker_cli_volume_test.go b/integration-cli/docker_cli_volume_test.go index 4855524797a45..ddbba0db2d3cc 100644 --- a/integration-cli/docker_cli_volume_test.go +++ b/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 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) +} diff --git a/man/docker-volume-create.1.md b/man/docker-volume-create.1.md index 24b39bc5a2133..43338095c7dec 100644 --- a/man/docker-volume-create.1.md +++ b/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*" diff --git a/volume/local/local.go b/volume/local/local.go index 794cb17a13a31..b154a36e7b946 100644 --- a/volume/local/local.go +++ b/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 } diff --git a/volume/local/local_test.go b/volume/local/local_test.go index 38d8343708f7e..1baa085457e7c 100644 --- a/volume/local/local_test.go +++ b/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") + } +} diff --git a/volume/local/local_unix.go b/volume/local/local_unix.go index 60f0e765d84d6..2e63777a19f43 100644 --- a/volume/local/local_unix.go +++ b/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) +} diff --git a/volume/local/local_windows.go b/volume/local/local_windows.go index 38812aa2f5431..1bdb368a0f860 100644 --- a/volume/local/local_windows.go +++ b/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 +}