Browse Source

Merge pull request #26825 from AkihiroSuda/mountcli

cli: add `--mount` to `docker run`
Vincent Demeester 8 years ago
parent
commit
e93f84a48b

+ 1 - 1
cli/command/service/create.go

@@ -35,7 +35,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels")
 	flags.VarP(&opts.env, flagEnv, "e", "Set environment variables")
 	flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables")
-	flags.Var(&opts.mounts, flagMount, "Attach a mount to the service")
+	flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service")
 	flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints")
 	flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
 	flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")

+ 1 - 140
cli/command/service/opts.go

@@ -1,7 +1,6 @@
 package service
 
 import (
-	"encoding/csv"
 	"fmt"
 	"math/big"
 	"strconv"
@@ -9,7 +8,6 @@ import (
 	"time"
 
 	"github.com/docker/docker/api/types/container"
-	mounttypes "github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/opts"
 	runconfigopts "github.com/docker/docker/runconfig/opts"
@@ -149,143 +147,6 @@ func (i *Uint64Opt) Value() *uint64 {
 	return i.value
 }
 
-// MountOpt is a Value type for parsing mounts
-type MountOpt struct {
-	values []mounttypes.Mount
-}
-
-// Set a new mount value
-func (m *MountOpt) Set(value string) error {
-	csvReader := csv.NewReader(strings.NewReader(value))
-	fields, err := csvReader.Read()
-	if err != nil {
-		return err
-	}
-
-	mount := mounttypes.Mount{}
-
-	volumeOptions := func() *mounttypes.VolumeOptions {
-		if mount.VolumeOptions == nil {
-			mount.VolumeOptions = &mounttypes.VolumeOptions{
-				Labels: make(map[string]string),
-			}
-		}
-		if mount.VolumeOptions.DriverConfig == nil {
-			mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
-		}
-		return mount.VolumeOptions
-	}
-
-	bindOptions := func() *mounttypes.BindOptions {
-		if mount.BindOptions == nil {
-			mount.BindOptions = new(mounttypes.BindOptions)
-		}
-		return mount.BindOptions
-	}
-
-	setValueOnMap := func(target map[string]string, value string) {
-		parts := strings.SplitN(value, "=", 2)
-		if len(parts) == 1 {
-			target[value] = ""
-		} else {
-			target[parts[0]] = parts[1]
-		}
-	}
-
-	mount.Type = mounttypes.TypeVolume // default to volume mounts
-	// Set writable as the default
-	for _, field := range fields {
-		parts := strings.SplitN(field, "=", 2)
-		key := strings.ToLower(parts[0])
-
-		if len(parts) == 1 {
-			switch key {
-			case "readonly", "ro":
-				mount.ReadOnly = true
-				continue
-			case "volume-nocopy":
-				volumeOptions().NoCopy = true
-				continue
-			}
-		}
-
-		if len(parts) != 2 {
-			return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
-		}
-
-		value := parts[1]
-		switch key {
-		case "type":
-			mount.Type = mounttypes.Type(strings.ToLower(value))
-		case "source", "src":
-			mount.Source = value
-		case "target", "dst", "destination":
-			mount.Target = value
-		case "readonly", "ro":
-			mount.ReadOnly, err = strconv.ParseBool(value)
-			if err != nil {
-				return fmt.Errorf("invalid value for %s: %s", key, value)
-			}
-		case "bind-propagation":
-			bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value))
-		case "volume-nocopy":
-			volumeOptions().NoCopy, err = strconv.ParseBool(value)
-			if err != nil {
-				return fmt.Errorf("invalid value for populate: %s", value)
-			}
-		case "volume-label":
-			setValueOnMap(volumeOptions().Labels, value)
-		case "volume-driver":
-			volumeOptions().DriverConfig.Name = value
-		case "volume-opt":
-			if volumeOptions().DriverConfig.Options == nil {
-				volumeOptions().DriverConfig.Options = make(map[string]string)
-			}
-			setValueOnMap(volumeOptions().DriverConfig.Options, value)
-		default:
-			return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
-		}
-	}
-
-	if mount.Type == "" {
-		return fmt.Errorf("type is required")
-	}
-
-	if mount.Target == "" {
-		return fmt.Errorf("target is required")
-	}
-
-	if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
-		return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
-	}
-	if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
-		return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
-	}
-
-	m.values = append(m.values, mount)
-	return nil
-}
-
-// Type returns the type of this option
-func (m *MountOpt) Type() string {
-	return "mount"
-}
-
-// String returns a string repr of this option
-func (m *MountOpt) String() string {
-	mounts := []string{}
-	for _, mount := range m.values {
-		repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
-		mounts = append(mounts, repr)
-	}
-	return strings.Join(mounts, ", ")
-}
-
-// Value returns the mounts
-func (m *MountOpt) Value() []mounttypes.Mount {
-	return m.values
-}
-
 type updateOptions struct {
 	parallelism     uint64
 	delay           time.Duration
@@ -460,7 +321,7 @@ type serviceOptions struct {
 	workdir         string
 	user            string
 	groups          []string
-	mounts          MountOpt
+	mounts          opts.MountOpt
 
 	resources resourceOptions
 	stopGrace DurationOpt

+ 0 - 146
cli/command/service/opts_test.go

@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/docker/docker/api/types/container"
-	mounttypes "github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/pkg/testutil/assert"
 )
 
@@ -68,151 +67,6 @@ func TestUint64OptSetAndValue(t *testing.T) {
 	assert.Equal(t, *opt.Value(), uint64(14445))
 }
 
-func TestMountOptString(t *testing.T) {
-	mount := MountOpt{
-		values: []mounttypes.Mount{
-			{
-				Type:   mounttypes.TypeBind,
-				Source: "/home/path",
-				Target: "/target",
-			},
-			{
-				Type:   mounttypes.TypeVolume,
-				Source: "foo",
-				Target: "/target/foo",
-			},
-		},
-	}
-	expected := "bind /home/path /target, volume foo /target/foo"
-	assert.Equal(t, mount.String(), expected)
-}
-
-func TestMountOptSetBindNoErrorBind(t *testing.T) {
-	for _, testcase := range []string{
-		// tests several aliases that should have same result.
-		"type=bind,target=/target,source=/source",
-		"type=bind,src=/source,dst=/target",
-		"type=bind,source=/source,dst=/target",
-		"type=bind,src=/source,target=/target",
-	} {
-		var mount MountOpt
-
-		assert.NilError(t, mount.Set(testcase))
-
-		mounts := mount.Value()
-		assert.Equal(t, len(mounts), 1)
-		assert.Equal(t, mounts[0], mounttypes.Mount{
-			Type:   mounttypes.TypeBind,
-			Source: "/source",
-			Target: "/target",
-		})
-	}
-}
-
-func TestMountOptSetVolumeNoError(t *testing.T) {
-	for _, testcase := range []string{
-		// tests several aliases that should have same result.
-		"type=volume,target=/target,source=/source",
-		"type=volume,src=/source,dst=/target",
-		"type=volume,source=/source,dst=/target",
-		"type=volume,src=/source,target=/target",
-	} {
-		var mount MountOpt
-
-		assert.NilError(t, mount.Set(testcase))
-
-		mounts := mount.Value()
-		assert.Equal(t, len(mounts), 1)
-		assert.Equal(t, mounts[0], mounttypes.Mount{
-			Type:   mounttypes.TypeVolume,
-			Source: "/source",
-			Target: "/target",
-		})
-	}
-}
-
-// TestMountOptDefaultType ensures that a mount without the type defaults to a
-// volume mount.
-func TestMountOptDefaultType(t *testing.T) {
-	var mount MountOpt
-	assert.NilError(t, mount.Set("target=/target,source=/foo"))
-	assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume)
-}
-
-func TestMountOptSetErrorNoTarget(t *testing.T) {
-	var mount MountOpt
-	assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
-}
-
-func TestMountOptSetErrorInvalidKey(t *testing.T) {
-	var mount MountOpt
-	assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
-}
-
-func TestMountOptSetErrorInvalidField(t *testing.T) {
-	var mount MountOpt
-	assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
-}
-
-func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
-	var mount MountOpt
-	assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
-	assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
-}
-
-func TestMountOptDefaultEnableReadOnly(t *testing.T) {
-	var m MountOpt
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
-	assert.Equal(t, m.values[0].ReadOnly, false)
-
-	m = MountOpt{}
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
-	assert.Equal(t, m.values[0].ReadOnly, true)
-
-	m = MountOpt{}
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
-	assert.Equal(t, m.values[0].ReadOnly, true)
-
-	m = MountOpt{}
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true"))
-	assert.Equal(t, m.values[0].ReadOnly, true)
-
-	m = MountOpt{}
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
-	assert.Equal(t, m.values[0].ReadOnly, false)
-}
-
-func TestMountOptVolumeNoCopy(t *testing.T) {
-	var m MountOpt
-	assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy"))
-	assert.Equal(t, m.values[0].Source, "")
-
-	m = MountOpt{}
-	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
-	assert.Equal(t, m.values[0].VolumeOptions == nil, true)
-
-	m = MountOpt{}
-	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
-	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
-	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
-
-	m = MountOpt{}
-	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
-	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
-	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
-
-	m = MountOpt{}
-	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
-	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
-	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
-}
-
-func TestMountOptTypeConflict(t *testing.T) {
-	var m MountOpt
-	assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
-	assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
-}
-
 func TestHealthCheckOptionsToHealthConfig(t *testing.T) {
 	dur := time.Second
 	opt := healthCheckOptions{

+ 1 - 1
cli/command/service/update.go

@@ -404,7 +404,7 @@ func removeItems(
 
 func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) {
 	if flags.Changed(flagMountAdd) {
-		values := flags.Lookup(flagMountAdd).Value.(*MountOpt).Value()
+		values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value()
 		*mounts = append(*mounts, values...)
 	}
 	toRemove := buildToRemoveSet(flags, flagMountRemove)

+ 1 - 0
contrib/completion/bash/docker

@@ -1268,6 +1268,7 @@ _docker_container_run() {
 		--memory-swap
 		--memory-swappiness
 		--memory-reservation
+		--mount
 		--name
 		--network
 		--network-alias

+ 2 - 0
contrib/completion/fish/docker.fish

@@ -137,6 +137,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l link -d 'Add
 complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s m -l memory -d 'Memory limit (format: <number>[<unit>], where unit = b, k, m or g)'
 complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l mac-address -d 'Container MAC address (e.g. 92:d0:c6:0a:29:33)'
 complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l memory-swap -d "Total memory usage (memory + swap), set '-1' to disable swap (format: <number>[<unit>], where unit = b, k, m or g)"
+complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l mount -d 'Attach a filesystem mount to the container'
 complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l name -d 'Assign a name to the container'
 complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l net -d 'Set the Network mode for the container'
 complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces'
@@ -328,6 +329,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l link -d 'Add li
 complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s m -l memory -d 'Memory limit (format: <number>[<unit>], where unit = b, k, m or g)'
 complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l mac-address -d 'Container MAC address (e.g. 92:d0:c6:0a:29:33)'
 complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l memory-swap -d "Total memory usage (memory + swap), set '-1' to disable swap (format: <number>[<unit>], where unit = b, k, m or g)"
+complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l mount -d 'Attach a filesystem mount to the container'
 complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container'
 complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l net -d 'Set the Network mode for the container'
 complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces'

+ 2 - 1
contrib/completion/zsh/_docker

@@ -1101,7 +1101,7 @@ __docker_service_subcommand() {
         "($help)--limit-memory=[Limit Memory]:value: "
         "($help)--log-driver=[Logging driver for service]:logging driver:__docker_log_drivers"
         "($help)*--log-opt=[Logging driver options]:log driver options:__docker_log_options"
-        "($help)*--mount=[Attach a mount to the service]:mount: "
+        "($help)*--mount=[Attach a filesystem mount to the service]:mount: "
         "($help)*--network=[Network attachments]:network: "
         "($help)--no-healthcheck[Disable any container-specified HEALTHCHECK]"
         "($help)*"{-p=,--publish=}"[Publish a port as a node port]:port: "
@@ -1481,6 +1481,7 @@ __docker_subcommand() {
         "($help)--log-driver=[Default driver for container logs]:logging driver:__docker_log_drivers"
         "($help)*--log-opt=[Log driver specific options]:log driver options:__docker_log_options"
         "($help)--mac-address=[Container MAC address]:MAC address: "
+        "($help)*--mount=[Attach a filesystem mount to the container]:mount: "
         "($help)--name=[Container name]:name: "
         "($help)--network=[Connect a container to a network]:network mode:(bridge none container host)"
         "($help)*--network-alias=[Add network-scoped alias for the container]:alias: "

+ 1 - 0
docs/reference/commandline/create.md

@@ -78,6 +78,7 @@ Options:
       --memory-reservation string   Memory soft limit
       --memory-swap string          Swap limit equal to memory plus swap: '-1' to enable unlimited swap
       --memory-swappiness int       Tune container memory swappiness (0 to 100) (default -1)
+      --mount value                 Attach a filesytem mount to the container (default [])
       --name string                 Assign a name to the container
       --network-alias value         Add network-scoped alias for the container (default [])
       --network string              Connect a container to a network (default "default")

+ 16 - 0
docs/reference/commandline/run.md

@@ -84,6 +84,7 @@ Options:
       --memory-reservation string   Memory soft limit
       --memory-swap string          Swap limit equal to memory plus swap: '-1' to enable unlimited swap
       --memory-swappiness int       Tune container memory swappiness (0 to 100) (default -1)
+      --mount value                 Attach a filesystem mount to the container (default [])
       --name string                 Assign a name to the container
       --network-alias value         Add network-scoped alias for the container (default [])
       --network string              Connect a container to a network
@@ -255,6 +256,21 @@ Docker daemon.
 
 For in-depth information about volumes, refer to [manage data in containers](https://docs.docker.com/engine/tutorials/dockervolumes/)
 
+### Add bin-mounts or volumes using the --mounts flag
+
+The `--mounts` flag allows you to mount volumes, host-directories and `tmpfs`
+mounts in a container.
+
+The `--mount` flag supports most options that are supported by the `-v` or the
+`--volume` flag, but uses a different syntax. For in-depth information on the
+`--mount` flag, and a comparison between `--volume` and `--mount`, refer to
+the [service create command reference](service_create.md#add-bind-mounts-or-volumes).
+
+Examples:
+
+    $ docker run --read-only --mount type=volume,target=/icanwrite busybox touch /icanwrite/here
+    $ docker run -t -i --mount type=bind,src=/data,dst=/data busybox sh
+
 ### Publish or expose port (-p, --expose)
 
     $ docker run -p 127.0.0.1:80:8080 ubuntu bash

+ 1 - 1
docs/reference/commandline/service_create.md

@@ -38,7 +38,7 @@ Options:
       --log-driver string                Logging driver for service
       --log-opt value                    Logging driver options (default [])
       --mode string                      Service mode (replicated or global) (default "replicated")
-      --mount value                      Attach a mount to the service
+      --mount value                      Attach a filesystem mount to the service
       --name string                      Service name
       --network value                    Network attachments (default [])
       --no-healthcheck                   Disable any container-specified HEALTHCHECK

+ 178 - 0
integration-cli/docker_cli_run_test.go

@@ -4588,3 +4588,181 @@ func (s *DockerSuite) TestRunDuplicateMount(c *check.C) {
 	out = inspectFieldJSON(c, name, "Config.Volumes")
 	c.Assert(out, checker.Contains, "null")
 }
+
+func (s *DockerSuite) TestRunMount(c *check.C) {
+	testRequires(c, DaemonIsLinux, SameHostDaemon, NotUserNamespace)
+
+	// mnt1, mnt2, and testCatFooBar are commonly used in multiple test cases
+	tmpDir, err := ioutil.TempDir("", "mount")
+	if err != nil {
+		c.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+	mnt1, mnt2 := path.Join(tmpDir, "mnt1"), path.Join(tmpDir, "mnt2")
+	if err := os.Mkdir(mnt1, 0755); err != nil {
+		c.Fatal(err)
+	}
+	if err := os.Mkdir(mnt2, 0755); err != nil {
+		c.Fatal(err)
+	}
+	if err := ioutil.WriteFile(path.Join(mnt1, "test1"), []byte("test1"), 0644); err != nil {
+		c.Fatal(err)
+	}
+	if err := ioutil.WriteFile(path.Join(mnt2, "test2"), []byte("test2"), 0644); err != nil {
+		c.Fatal(err)
+	}
+	testCatFooBar := func(cName string) error {
+		out, _ := dockerCmd(c, "exec", cName, "cat", "/foo/test1")
+		if out != "test1" {
+			return fmt.Errorf("%s not mounted on /foo", mnt1)
+		}
+		out, _ = dockerCmd(c, "exec", cName, "cat", "/bar/test2")
+		if out != "test2" {
+			return fmt.Errorf("%s not mounted on /bar", mnt2)
+		}
+		return nil
+	}
+
+	type testCase struct {
+		equivalents [][]string
+		valid       bool
+		// fn should be nil if valid==false
+		fn func(cName string) error
+	}
+	cases := []testCase{
+		{
+			equivalents: [][]string{
+				{
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/bar", mnt2),
+				},
+				{
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
+					"--mount", fmt.Sprintf("type=bind,src=%s,target=/bar", mnt2),
+				},
+				{
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
+					"--mount", fmt.Sprintf("type=bind,src=%s,target=/bar", mnt2),
+				},
+			},
+			valid: true,
+			fn:    testCatFooBar,
+		},
+		{
+			equivalents: [][]string{
+				{
+					"--mount", fmt.Sprintf("type=volume,src=%s,dst=/foo", mnt1),
+					"--mount", fmt.Sprintf("type=volume,src=%s,dst=/bar", mnt2),
+				},
+				{
+					"--mount", fmt.Sprintf("type=volume,src=%s,dst=/foo", mnt1),
+					"--mount", fmt.Sprintf("type=volume,src=%s,target=/bar", mnt2),
+				},
+			},
+			valid: false,
+		},
+		{
+			equivalents: [][]string{
+				{
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
+					"--mount", fmt.Sprintf("type=volume,src=%s,dst=/bar", mnt2),
+				},
+				{
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
+					"--mount", fmt.Sprintf("type=volume,src=%s,target=/bar", mnt2),
+				},
+			},
+			valid: false,
+			fn:    testCatFooBar,
+		},
+		{
+			equivalents: [][]string{
+				{
+					"--read-only",
+					"--mount", "type=volume,dst=/bar",
+				},
+			},
+			valid: true,
+			fn: func(cName string) error {
+				_, _, err := dockerCmdWithError("exec", cName, "touch", "/bar/icanwritehere")
+				return err
+			},
+		},
+		{
+			equivalents: [][]string{
+				{
+					"--read-only",
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
+					"--mount", "type=volume,dst=/bar",
+				},
+				{
+					"--read-only",
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
+					"--mount", "type=volume,dst=/bar",
+				},
+			},
+			valid: true,
+			fn: func(cName string) error {
+				out, _ := dockerCmd(c, "exec", cName, "cat", "/foo/test1")
+				if out != "test1" {
+					return fmt.Errorf("%s not mounted on /foo", mnt1)
+				}
+				_, _, err := dockerCmdWithError("exec", cName, "touch", "/bar/icanwritehere")
+				return err
+			},
+		},
+		{
+			equivalents: [][]string{
+				{
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt2),
+				},
+				{
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
+					"--mount", fmt.Sprintf("type=bind,src=%s,target=/foo", mnt2),
+				},
+				{
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
+					"--mount", fmt.Sprintf("type=bind,src=%s,target=/foo", mnt2),
+				},
+			},
+			valid: false,
+		},
+		{
+			equivalents: [][]string{
+				{
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
+					"--mount", fmt.Sprintf("type=volume,src=%s,target=/foo", mnt2),
+				},
+			},
+			valid: false,
+		},
+		{
+			equivalents: [][]string{
+				{
+					"--mount", "type=volume,target=/foo",
+					"--mount", "type=volume,target=/foo",
+				},
+			},
+			valid: false,
+		},
+	}
+
+	for i, testCase := range cases {
+		for j, opts := range testCase.equivalents {
+			cName := fmt.Sprintf("mount-%d-%d", i, j)
+			_, _, err := dockerCmdWithError(append([]string{"run", "-i", "-d", "--name", cName},
+				append(opts, []string{"busybox", "top"}...)...)...)
+			if testCase.valid {
+				c.Assert(err, check.IsNil,
+					check.Commentf("got error while creating a container with %v (%s)", opts, cName))
+				c.Assert(testCase.fn(cName), check.IsNil,
+					check.Commentf("got error while executing test for %v (%s)", opts, cName))
+				dockerCmd(c, "rm", "-f", cName)
+			} else {
+				c.Assert(err, checker.NotNil,
+					check.Commentf("got nil while creating a container with %v (%s)", opts, cName))
+			}
+		}
+	}
+}

+ 1 - 0
man/docker-create.1.md

@@ -53,6 +53,7 @@ docker-create - Create a new container
 [**--memory-reservation**[=*MEMORY-RESERVATION*]]
 [**--memory-swap**[=*LIMIT*]]
 [**--memory-swappiness**[=*MEMORY-SWAPPINESS*]]
+[**--mount**[=*MOUNT*]]
 [**--name**[=*NAME*]]
 [**--network-alias**[=*[]*]]
 [**--network**[=*"bridge"*]]

+ 1 - 0
man/docker-run.1.md

@@ -55,6 +55,7 @@ docker-run - Run a command in a new container
 [**--memory-reservation**[=*MEMORY-RESERVATION*]]
 [**--memory-swap**[=*LIMIT*]]
 [**--memory-swappiness**[=*MEMORY-SWAPPINESS*]]
+[**--mount**[=*MOUNT*]]
 [**--name**[=*NAME*]]
 [**--network-alias**[=*[]*]]
 [**--network**[=*"bridge"*]]

+ 147 - 0
opts/mount.go

@@ -0,0 +1,147 @@
+package opts
+
+import (
+	"encoding/csv"
+	"fmt"
+	"strconv"
+	"strings"
+
+	mounttypes "github.com/docker/docker/api/types/mount"
+)
+
+// MountOpt is a Value type for parsing mounts
+type MountOpt struct {
+	values []mounttypes.Mount
+}
+
+// Set a new mount value
+func (m *MountOpt) Set(value string) error {
+	csvReader := csv.NewReader(strings.NewReader(value))
+	fields, err := csvReader.Read()
+	if err != nil {
+		return err
+	}
+
+	mount := mounttypes.Mount{}
+
+	volumeOptions := func() *mounttypes.VolumeOptions {
+		if mount.VolumeOptions == nil {
+			mount.VolumeOptions = &mounttypes.VolumeOptions{
+				Labels: make(map[string]string),
+			}
+		}
+		if mount.VolumeOptions.DriverConfig == nil {
+			mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
+		}
+		return mount.VolumeOptions
+	}
+
+	bindOptions := func() *mounttypes.BindOptions {
+		if mount.BindOptions == nil {
+			mount.BindOptions = new(mounttypes.BindOptions)
+		}
+		return mount.BindOptions
+	}
+
+	setValueOnMap := func(target map[string]string, value string) {
+		parts := strings.SplitN(value, "=", 2)
+		if len(parts) == 1 {
+			target[value] = ""
+		} else {
+			target[parts[0]] = parts[1]
+		}
+	}
+
+	mount.Type = mounttypes.TypeVolume // default to volume mounts
+	// Set writable as the default
+	for _, field := range fields {
+		parts := strings.SplitN(field, "=", 2)
+		key := strings.ToLower(parts[0])
+
+		if len(parts) == 1 {
+			switch key {
+			case "readonly", "ro":
+				mount.ReadOnly = true
+				continue
+			case "volume-nocopy":
+				volumeOptions().NoCopy = true
+				continue
+			}
+		}
+
+		if len(parts) != 2 {
+			return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
+		}
+
+		value := parts[1]
+		switch key {
+		case "type":
+			mount.Type = mounttypes.Type(strings.ToLower(value))
+		case "source", "src":
+			mount.Source = value
+		case "target", "dst", "destination":
+			mount.Target = value
+		case "readonly", "ro":
+			mount.ReadOnly, err = strconv.ParseBool(value)
+			if err != nil {
+				return fmt.Errorf("invalid value for %s: %s", key, value)
+			}
+		case "bind-propagation":
+			bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value))
+		case "volume-nocopy":
+			volumeOptions().NoCopy, err = strconv.ParseBool(value)
+			if err != nil {
+				return fmt.Errorf("invalid value for populate: %s", value)
+			}
+		case "volume-label":
+			setValueOnMap(volumeOptions().Labels, value)
+		case "volume-driver":
+			volumeOptions().DriverConfig.Name = value
+		case "volume-opt":
+			if volumeOptions().DriverConfig.Options == nil {
+				volumeOptions().DriverConfig.Options = make(map[string]string)
+			}
+			setValueOnMap(volumeOptions().DriverConfig.Options, value)
+		default:
+			return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
+		}
+	}
+
+	if mount.Type == "" {
+		return fmt.Errorf("type is required")
+	}
+
+	if mount.Target == "" {
+		return fmt.Errorf("target is required")
+	}
+
+	if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
+		return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
+	}
+	if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
+		return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
+	}
+
+	m.values = append(m.values, mount)
+	return nil
+}
+
+// Type returns the type of this option
+func (m *MountOpt) Type() string {
+	return "mount"
+}
+
+// String returns a string repr of this option
+func (m *MountOpt) String() string {
+	mounts := []string{}
+	for _, mount := range m.values {
+		repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
+		mounts = append(mounts, repr)
+	}
+	return strings.Join(mounts, ", ")
+}
+
+// Value returns the mounts
+func (m *MountOpt) Value() []mounttypes.Mount {
+	return m.values
+}

+ 153 - 0
opts/mount_test.go

@@ -0,0 +1,153 @@
+package opts
+
+import (
+	"testing"
+
+	mounttypes "github.com/docker/docker/api/types/mount"
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestMountOptString(t *testing.T) {
+	mount := MountOpt{
+		values: []mounttypes.Mount{
+			{
+				Type:   mounttypes.TypeBind,
+				Source: "/home/path",
+				Target: "/target",
+			},
+			{
+				Type:   mounttypes.TypeVolume,
+				Source: "foo",
+				Target: "/target/foo",
+			},
+		},
+	}
+	expected := "bind /home/path /target, volume foo /target/foo"
+	assert.Equal(t, mount.String(), expected)
+}
+
+func TestMountOptSetBindNoErrorBind(t *testing.T) {
+	for _, testcase := range []string{
+		// tests several aliases that should have same result.
+		"type=bind,target=/target,source=/source",
+		"type=bind,src=/source,dst=/target",
+		"type=bind,source=/source,dst=/target",
+		"type=bind,src=/source,target=/target",
+	} {
+		var mount MountOpt
+
+		assert.NilError(t, mount.Set(testcase))
+
+		mounts := mount.Value()
+		assert.Equal(t, len(mounts), 1)
+		assert.Equal(t, mounts[0], mounttypes.Mount{
+			Type:   mounttypes.TypeBind,
+			Source: "/source",
+			Target: "/target",
+		})
+	}
+}
+
+func TestMountOptSetVolumeNoError(t *testing.T) {
+	for _, testcase := range []string{
+		// tests several aliases that should have same result.
+		"type=volume,target=/target,source=/source",
+		"type=volume,src=/source,dst=/target",
+		"type=volume,source=/source,dst=/target",
+		"type=volume,src=/source,target=/target",
+	} {
+		var mount MountOpt
+
+		assert.NilError(t, mount.Set(testcase))
+
+		mounts := mount.Value()
+		assert.Equal(t, len(mounts), 1)
+		assert.Equal(t, mounts[0], mounttypes.Mount{
+			Type:   mounttypes.TypeVolume,
+			Source: "/source",
+			Target: "/target",
+		})
+	}
+}
+
+// TestMountOptDefaultType ensures that a mount without the type defaults to a
+// volume mount.
+func TestMountOptDefaultType(t *testing.T) {
+	var mount MountOpt
+	assert.NilError(t, mount.Set("target=/target,source=/foo"))
+	assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume)
+}
+
+func TestMountOptSetErrorNoTarget(t *testing.T) {
+	var mount MountOpt
+	assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
+}
+
+func TestMountOptSetErrorInvalidKey(t *testing.T) {
+	var mount MountOpt
+	assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
+}
+
+func TestMountOptSetErrorInvalidField(t *testing.T) {
+	var mount MountOpt
+	assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
+}
+
+func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
+	var mount MountOpt
+	assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
+	assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
+}
+
+func TestMountOptDefaultEnableReadOnly(t *testing.T) {
+	var m MountOpt
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
+	assert.Equal(t, m.values[0].ReadOnly, false)
+
+	m = MountOpt{}
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
+	assert.Equal(t, m.values[0].ReadOnly, true)
+
+	m = MountOpt{}
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
+	assert.Equal(t, m.values[0].ReadOnly, true)
+
+	m = MountOpt{}
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true"))
+	assert.Equal(t, m.values[0].ReadOnly, true)
+
+	m = MountOpt{}
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
+	assert.Equal(t, m.values[0].ReadOnly, false)
+}
+
+func TestMountOptVolumeNoCopy(t *testing.T) {
+	var m MountOpt
+	assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy"))
+	assert.Equal(t, m.values[0].Source, "")
+
+	m = MountOpt{}
+	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
+	assert.Equal(t, m.values[0].VolumeOptions == nil, true)
+
+	m = MountOpt{}
+	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
+	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
+	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
+
+	m = MountOpt{}
+	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
+	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
+	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
+
+	m = MountOpt{}
+	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
+	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
+	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
+}
+
+func TestMountOptTypeConflict(t *testing.T) {
+	var m MountOpt
+	assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
+	assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
+}

+ 5 - 0
runconfig/opts/parse.go

@@ -26,6 +26,7 @@ type ContainerOptions struct {
 	attach             opts.ListOpts
 	volumes            opts.ListOpts
 	tmpfs              opts.ListOpts
+	mounts             opts.MountOpt
 	blkioWeightDevice  WeightdeviceOpt
 	deviceReadBps      ThrottledeviceOpt
 	deviceWriteBps     ThrottledeviceOpt
@@ -210,6 +211,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions {
 	flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory")
 	flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)")
 	flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume")
+	flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container")
 
 	// Health-checking
 	flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health")
@@ -347,6 +349,8 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c
 		}
 	}
 
+	mounts := copts.mounts.Value()
+
 	var binds []string
 	volumes := copts.volumes.GetMap()
 	// add any bind targets to the list of container volumes
@@ -612,6 +616,7 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c
 		Tmpfs:          tmpfs,
 		Sysctls:        copts.sysctls.GetAll(),
 		Runtime:        copts.runtime,
+		Mounts:         mounts,
 	}
 
 	// only set this value if the user provided the flag, else it should default to nil