diff --git a/cli/command/service/create.go b/cli/command/service/create.go index 28790ec8e6..92cf969b4b 100644 --- a/cli/command/service/create.go +++ b/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") diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 43b7b671cb..358185c0b4 100644 --- a/cli/command/service/opts.go +++ b/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 diff --git a/cli/command/service/opts_test.go b/cli/command/service/opts_test.go index 52016cbfc5..26534cf0f5 100644 --- a/cli/command/service/opts_test.go +++ b/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{ diff --git a/cli/command/service/update.go b/cli/command/service/update.go index b76a20e97c..a9aa9c9987 100644 --- a/cli/command/service/update.go +++ b/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) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index ebd108d7c1..973684a82f 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -1268,6 +1268,7 @@ _docker_container_run() { --memory-swap --memory-swappiness --memory-reservation + --mount --name --network --network-alias diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish index e72fb4e012..80741042dc 100644 --- a/contrib/completion/fish/docker.fish +++ b/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: [], 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: [], 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: [], 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: [], 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' diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index be3c854b78..1e1e211de9 100644 --- a/contrib/completion/zsh/_docker +++ b/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: " diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md index 784b4940e1..e621efeb1f 100644 --- a/docs/reference/commandline/create.md +++ b/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") diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index ec6a412009..49009b94b3 100644 --- a/docs/reference/commandline/run.md +++ b/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 diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 3406ad68a0..b16f4c654f 100644 --- a/docs/reference/commandline/service_create.md +++ b/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 diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 4ba33a5324..a91919da7d 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/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)) + } + } + } +} diff --git a/man/docker-create.1.md b/man/docker-create.1.md index 21cbd9f9d6..fd19ee45c6 100644 --- a/man/docker-create.1.md +++ b/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"*]] diff --git a/man/docker-run.1.md b/man/docker-run.1.md index 0a46f29110..d430ff58a5 100644 --- a/man/docker-run.1.md +++ b/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"*]] diff --git a/opts/mount.go b/opts/mount.go new file mode 100644 index 0000000000..b6fccade18 --- /dev/null +++ b/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 +} diff --git a/opts/mount_test.go b/opts/mount_test.go new file mode 100644 index 0000000000..28c551bcc6 --- /dev/null +++ b/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") +} diff --git a/runconfig/opts/parse.go b/runconfig/opts/parse.go index 8233bae287..a2323bff41 100644 --- a/runconfig/opts/parse.go +++ b/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