mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
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 <cpuguy83@gmail.com>
This commit is contained in:
parent
2453262e7b
commit
b05b237075
7 changed files with 303 additions and 18 deletions
|
@ -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)
|
||||
|
|
|
@ -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,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*"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,28 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"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
|
||||
// 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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue