Merge pull request #20262 from cpuguy83/implemnt_mount_opts_for_local_driver
Support mount opts for `local` volume driver
This commit is contained in:
commit
c4be28d6a8
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,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*"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue