Browse code

cli: add `--mount` to `docker run`

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>

Akihiro Suda authored on 2016/10/25 12:26:54
Showing 16 changed files
... ...
@@ -35,7 +35,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
35 35
 	flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels")
36 36
 	flags.VarP(&opts.env, flagEnv, "e", "Set environment variables")
37 37
 	flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables")
38
-	flags.Var(&opts.mounts, flagMount, "Attach a mount to the service")
38
+	flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service")
39 39
 	flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints")
40 40
 	flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
41 41
 	flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
... ...
@@ -1,7 +1,6 @@
1 1
 package service
2 2
 
3 3
 import (
4
-	"encoding/csv"
5 4
 	"fmt"
6 5
 	"math/big"
7 6
 	"strconv"
... ...
@@ -9,7 +8,6 @@ import (
9 9
 	"time"
10 10
 
11 11
 	"github.com/docker/docker/api/types/container"
12
-	mounttypes "github.com/docker/docker/api/types/mount"
13 12
 	"github.com/docker/docker/api/types/swarm"
14 13
 	"github.com/docker/docker/opts"
15 14
 	runconfigopts "github.com/docker/docker/runconfig/opts"
... ...
@@ -149,143 +147,6 @@ func (i *Uint64Opt) Value() *uint64 {
149 149
 	return i.value
150 150
 }
151 151
 
152
-// MountOpt is a Value type for parsing mounts
153
-type MountOpt struct {
154
-	values []mounttypes.Mount
155
-}
156
-
157
-// Set a new mount value
158
-func (m *MountOpt) Set(value string) error {
159
-	csvReader := csv.NewReader(strings.NewReader(value))
160
-	fields, err := csvReader.Read()
161
-	if err != nil {
162
-		return err
163
-	}
164
-
165
-	mount := mounttypes.Mount{}
166
-
167
-	volumeOptions := func() *mounttypes.VolumeOptions {
168
-		if mount.VolumeOptions == nil {
169
-			mount.VolumeOptions = &mounttypes.VolumeOptions{
170
-				Labels: make(map[string]string),
171
-			}
172
-		}
173
-		if mount.VolumeOptions.DriverConfig == nil {
174
-			mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
175
-		}
176
-		return mount.VolumeOptions
177
-	}
178
-
179
-	bindOptions := func() *mounttypes.BindOptions {
180
-		if mount.BindOptions == nil {
181
-			mount.BindOptions = new(mounttypes.BindOptions)
182
-		}
183
-		return mount.BindOptions
184
-	}
185
-
186
-	setValueOnMap := func(target map[string]string, value string) {
187
-		parts := strings.SplitN(value, "=", 2)
188
-		if len(parts) == 1 {
189
-			target[value] = ""
190
-		} else {
191
-			target[parts[0]] = parts[1]
192
-		}
193
-	}
194
-
195
-	mount.Type = mounttypes.TypeVolume // default to volume mounts
196
-	// Set writable as the default
197
-	for _, field := range fields {
198
-		parts := strings.SplitN(field, "=", 2)
199
-		key := strings.ToLower(parts[0])
200
-
201
-		if len(parts) == 1 {
202
-			switch key {
203
-			case "readonly", "ro":
204
-				mount.ReadOnly = true
205
-				continue
206
-			case "volume-nocopy":
207
-				volumeOptions().NoCopy = true
208
-				continue
209
-			}
210
-		}
211
-
212
-		if len(parts) != 2 {
213
-			return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
214
-		}
215
-
216
-		value := parts[1]
217
-		switch key {
218
-		case "type":
219
-			mount.Type = mounttypes.Type(strings.ToLower(value))
220
-		case "source", "src":
221
-			mount.Source = value
222
-		case "target", "dst", "destination":
223
-			mount.Target = value
224
-		case "readonly", "ro":
225
-			mount.ReadOnly, err = strconv.ParseBool(value)
226
-			if err != nil {
227
-				return fmt.Errorf("invalid value for %s: %s", key, value)
228
-			}
229
-		case "bind-propagation":
230
-			bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value))
231
-		case "volume-nocopy":
232
-			volumeOptions().NoCopy, err = strconv.ParseBool(value)
233
-			if err != nil {
234
-				return fmt.Errorf("invalid value for populate: %s", value)
235
-			}
236
-		case "volume-label":
237
-			setValueOnMap(volumeOptions().Labels, value)
238
-		case "volume-driver":
239
-			volumeOptions().DriverConfig.Name = value
240
-		case "volume-opt":
241
-			if volumeOptions().DriverConfig.Options == nil {
242
-				volumeOptions().DriverConfig.Options = make(map[string]string)
243
-			}
244
-			setValueOnMap(volumeOptions().DriverConfig.Options, value)
245
-		default:
246
-			return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
247
-		}
248
-	}
249
-
250
-	if mount.Type == "" {
251
-		return fmt.Errorf("type is required")
252
-	}
253
-
254
-	if mount.Target == "" {
255
-		return fmt.Errorf("target is required")
256
-	}
257
-
258
-	if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
259
-		return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
260
-	}
261
-	if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
262
-		return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
263
-	}
264
-
265
-	m.values = append(m.values, mount)
266
-	return nil
267
-}
268
-
269
-// Type returns the type of this option
270
-func (m *MountOpt) Type() string {
271
-	return "mount"
272
-}
273
-
274
-// String returns a string repr of this option
275
-func (m *MountOpt) String() string {
276
-	mounts := []string{}
277
-	for _, mount := range m.values {
278
-		repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
279
-		mounts = append(mounts, repr)
280
-	}
281
-	return strings.Join(mounts, ", ")
282
-}
283
-
284
-// Value returns the mounts
285
-func (m *MountOpt) Value() []mounttypes.Mount {
286
-	return m.values
287
-}
288
-
289 152
 type updateOptions struct {
290 153
 	parallelism     uint64
291 154
 	delay           time.Duration
... ...
@@ -460,7 +321,7 @@ type serviceOptions struct {
460 460
 	workdir         string
461 461
 	user            string
462 462
 	groups          []string
463
-	mounts          MountOpt
463
+	mounts          opts.MountOpt
464 464
 
465 465
 	resources resourceOptions
466 466
 	stopGrace DurationOpt
... ...
@@ -6,7 +6,6 @@ import (
6 6
 	"time"
7 7
 
8 8
 	"github.com/docker/docker/api/types/container"
9
-	mounttypes "github.com/docker/docker/api/types/mount"
10 9
 	"github.com/docker/docker/pkg/testutil/assert"
11 10
 )
12 11
 
... ...
@@ -68,151 +67,6 @@ func TestUint64OptSetAndValue(t *testing.T) {
68 68
 	assert.Equal(t, *opt.Value(), uint64(14445))
69 69
 }
70 70
 
71
-func TestMountOptString(t *testing.T) {
72
-	mount := MountOpt{
73
-		values: []mounttypes.Mount{
74
-			{
75
-				Type:   mounttypes.TypeBind,
76
-				Source: "/home/path",
77
-				Target: "/target",
78
-			},
79
-			{
80
-				Type:   mounttypes.TypeVolume,
81
-				Source: "foo",
82
-				Target: "/target/foo",
83
-			},
84
-		},
85
-	}
86
-	expected := "bind /home/path /target, volume foo /target/foo"
87
-	assert.Equal(t, mount.String(), expected)
88
-}
89
-
90
-func TestMountOptSetBindNoErrorBind(t *testing.T) {
91
-	for _, testcase := range []string{
92
-		// tests several aliases that should have same result.
93
-		"type=bind,target=/target,source=/source",
94
-		"type=bind,src=/source,dst=/target",
95
-		"type=bind,source=/source,dst=/target",
96
-		"type=bind,src=/source,target=/target",
97
-	} {
98
-		var mount MountOpt
99
-
100
-		assert.NilError(t, mount.Set(testcase))
101
-
102
-		mounts := mount.Value()
103
-		assert.Equal(t, len(mounts), 1)
104
-		assert.Equal(t, mounts[0], mounttypes.Mount{
105
-			Type:   mounttypes.TypeBind,
106
-			Source: "/source",
107
-			Target: "/target",
108
-		})
109
-	}
110
-}
111
-
112
-func TestMountOptSetVolumeNoError(t *testing.T) {
113
-	for _, testcase := range []string{
114
-		// tests several aliases that should have same result.
115
-		"type=volume,target=/target,source=/source",
116
-		"type=volume,src=/source,dst=/target",
117
-		"type=volume,source=/source,dst=/target",
118
-		"type=volume,src=/source,target=/target",
119
-	} {
120
-		var mount MountOpt
121
-
122
-		assert.NilError(t, mount.Set(testcase))
123
-
124
-		mounts := mount.Value()
125
-		assert.Equal(t, len(mounts), 1)
126
-		assert.Equal(t, mounts[0], mounttypes.Mount{
127
-			Type:   mounttypes.TypeVolume,
128
-			Source: "/source",
129
-			Target: "/target",
130
-		})
131
-	}
132
-}
133
-
134
-// TestMountOptDefaultType ensures that a mount without the type defaults to a
135
-// volume mount.
136
-func TestMountOptDefaultType(t *testing.T) {
137
-	var mount MountOpt
138
-	assert.NilError(t, mount.Set("target=/target,source=/foo"))
139
-	assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume)
140
-}
141
-
142
-func TestMountOptSetErrorNoTarget(t *testing.T) {
143
-	var mount MountOpt
144
-	assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
145
-}
146
-
147
-func TestMountOptSetErrorInvalidKey(t *testing.T) {
148
-	var mount MountOpt
149
-	assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
150
-}
151
-
152
-func TestMountOptSetErrorInvalidField(t *testing.T) {
153
-	var mount MountOpt
154
-	assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
155
-}
156
-
157
-func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
158
-	var mount MountOpt
159
-	assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
160
-	assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
161
-}
162
-
163
-func TestMountOptDefaultEnableReadOnly(t *testing.T) {
164
-	var m MountOpt
165
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
166
-	assert.Equal(t, m.values[0].ReadOnly, false)
167
-
168
-	m = MountOpt{}
169
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
170
-	assert.Equal(t, m.values[0].ReadOnly, true)
171
-
172
-	m = MountOpt{}
173
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
174
-	assert.Equal(t, m.values[0].ReadOnly, true)
175
-
176
-	m = MountOpt{}
177
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true"))
178
-	assert.Equal(t, m.values[0].ReadOnly, true)
179
-
180
-	m = MountOpt{}
181
-	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
182
-	assert.Equal(t, m.values[0].ReadOnly, false)
183
-}
184
-
185
-func TestMountOptVolumeNoCopy(t *testing.T) {
186
-	var m MountOpt
187
-	assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy"))
188
-	assert.Equal(t, m.values[0].Source, "")
189
-
190
-	m = MountOpt{}
191
-	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
192
-	assert.Equal(t, m.values[0].VolumeOptions == nil, true)
193
-
194
-	m = MountOpt{}
195
-	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
196
-	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
197
-	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
198
-
199
-	m = MountOpt{}
200
-	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
201
-	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
202
-	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
203
-
204
-	m = MountOpt{}
205
-	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
206
-	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
207
-	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
208
-}
209
-
210
-func TestMountOptTypeConflict(t *testing.T) {
211
-	var m MountOpt
212
-	assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
213
-	assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
214
-}
215
-
216 71
 func TestHealthCheckOptionsToHealthConfig(t *testing.T) {
217 72
 	dur := time.Second
218 73
 	opt := healthCheckOptions{
... ...
@@ -404,7 +404,7 @@ func removeItems(
404 404
 
405 405
 func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) {
406 406
 	if flags.Changed(flagMountAdd) {
407
-		values := flags.Lookup(flagMountAdd).Value.(*MountOpt).Value()
407
+		values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value()
408 408
 		*mounts = append(*mounts, values...)
409 409
 	}
410 410
 	toRemove := buildToRemoveSet(flags, flagMountRemove)
... ...
@@ -1268,6 +1268,7 @@ _docker_container_run() {
1268 1268
 		--memory-swap
1269 1269
 		--memory-swappiness
1270 1270
 		--memory-reservation
1271
+		--mount
1271 1272
 		--name
1272 1273
 		--network
1273 1274
 		--network-alias
... ...
@@ -137,6 +137,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l link -d 'Add
137 137
 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)'
138 138
 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)'
139 139
 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)"
140
+complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l mount -d 'Attach a filesystem mount to the container'
140 141
 complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l name -d 'Assign a name to the container'
141 142
 complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l net -d 'Set the Network mode for the container'
142 143
 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
328 328
 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)'
329 329
 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)'
330 330
 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)"
331
+complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l mount -d 'Attach a filesystem mount to the container'
331 332
 complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container'
332 333
 complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l net -d 'Set the Network mode for the container'
333 334
 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'
... ...
@@ -1101,7 +1101,7 @@ __docker_service_subcommand() {
1101 1101
         "($help)--limit-memory=[Limit Memory]:value: "
1102 1102
         "($help)--log-driver=[Logging driver for service]:logging driver:__docker_log_drivers"
1103 1103
         "($help)*--log-opt=[Logging driver options]:log driver options:__docker_log_options"
1104
-        "($help)*--mount=[Attach a mount to the service]:mount: "
1104
+        "($help)*--mount=[Attach a filesystem mount to the service]:mount: "
1105 1105
         "($help)*--network=[Network attachments]:network: "
1106 1106
         "($help)--no-healthcheck[Disable any container-specified HEALTHCHECK]"
1107 1107
         "($help)*"{-p=,--publish=}"[Publish a port as a node port]:port: "
... ...
@@ -1481,6 +1481,7 @@ __docker_subcommand() {
1481 1481
         "($help)--log-driver=[Default driver for container logs]:logging driver:__docker_log_drivers"
1482 1482
         "($help)*--log-opt=[Log driver specific options]:log driver options:__docker_log_options"
1483 1483
         "($help)--mac-address=[Container MAC address]:MAC address: "
1484
+        "($help)*--mount=[Attach a filesystem mount to the container]:mount: "
1484 1485
         "($help)--name=[Container name]:name: "
1485 1486
         "($help)--network=[Connect a container to a network]:network mode:(bridge none container host)"
1486 1487
         "($help)*--network-alias=[Add network-scoped alias for the container]:alias: "
... ...
@@ -78,6 +78,7 @@ Options:
78 78
       --memory-reservation string   Memory soft limit
79 79
       --memory-swap string          Swap limit equal to memory plus swap: '-1' to enable unlimited swap
80 80
       --memory-swappiness int       Tune container memory swappiness (0 to 100) (default -1)
81
+      --mount value                 Attach a filesytem mount to the container (default [])
81 82
       --name string                 Assign a name to the container
82 83
       --network-alias value         Add network-scoped alias for the container (default [])
83 84
       --network string              Connect a container to a network (default "default")
... ...
@@ -84,6 +84,7 @@ Options:
84 84
       --memory-reservation string   Memory soft limit
85 85
       --memory-swap string          Swap limit equal to memory plus swap: '-1' to enable unlimited swap
86 86
       --memory-swappiness int       Tune container memory swappiness (0 to 100) (default -1)
87
+      --mount value                 Attach a filesystem mount to the container (default [])
87 88
       --name string                 Assign a name to the container
88 89
       --network-alias value         Add network-scoped alias for the container (default [])
89 90
       --network string              Connect a container to a network
... ...
@@ -255,6 +256,21 @@ Docker daemon.
255 255
 
256 256
 For in-depth information about volumes, refer to [manage data in containers](https://docs.docker.com/engine/tutorials/dockervolumes/)
257 257
 
258
+### Add bin-mounts or volumes using the --mounts flag
259
+
260
+The `--mounts` flag allows you to mount volumes, host-directories and `tmpfs`
261
+mounts in a container.
262
+
263
+The `--mount` flag supports most options that are supported by the `-v` or the
264
+`--volume` flag, but uses a different syntax. For in-depth information on the
265
+`--mount` flag, and a comparison between `--volume` and `--mount`, refer to
266
+the [service create command reference](service_create.md#add-bind-mounts-or-volumes).
267
+
268
+Examples:
269
+
270
+    $ docker run --read-only --mount type=volume,target=/icanwrite busybox touch /icanwrite/here
271
+    $ docker run -t -i --mount type=bind,src=/data,dst=/data busybox sh
272
+
258 273
 ### Publish or expose port (-p, --expose)
259 274
 
260 275
     $ docker run -p 127.0.0.1:80:8080 ubuntu bash
... ...
@@ -38,7 +38,7 @@ Options:
38 38
       --log-driver string                Logging driver for service
39 39
       --log-opt value                    Logging driver options (default [])
40 40
       --mode string                      Service mode (replicated or global) (default "replicated")
41
-      --mount value                      Attach a mount to the service
41
+      --mount value                      Attach a filesystem mount to the service
42 42
       --name string                      Service name
43 43
       --network value                    Network attachments (default [])
44 44
       --no-healthcheck                   Disable any container-specified HEALTHCHECK
... ...
@@ -4588,3 +4588,181 @@ func (s *DockerSuite) TestRunDuplicateMount(c *check.C) {
4588 4588
 	out = inspectFieldJSON(c, name, "Config.Volumes")
4589 4589
 	c.Assert(out, checker.Contains, "null")
4590 4590
 }
4591
+
4592
+func (s *DockerSuite) TestRunMount(c *check.C) {
4593
+	testRequires(c, DaemonIsLinux, SameHostDaemon, NotUserNamespace)
4594
+
4595
+	// mnt1, mnt2, and testCatFooBar are commonly used in multiple test cases
4596
+	tmpDir, err := ioutil.TempDir("", "mount")
4597
+	if err != nil {
4598
+		c.Fatal(err)
4599
+	}
4600
+	defer os.RemoveAll(tmpDir)
4601
+	mnt1, mnt2 := path.Join(tmpDir, "mnt1"), path.Join(tmpDir, "mnt2")
4602
+	if err := os.Mkdir(mnt1, 0755); err != nil {
4603
+		c.Fatal(err)
4604
+	}
4605
+	if err := os.Mkdir(mnt2, 0755); err != nil {
4606
+		c.Fatal(err)
4607
+	}
4608
+	if err := ioutil.WriteFile(path.Join(mnt1, "test1"), []byte("test1"), 0644); err != nil {
4609
+		c.Fatal(err)
4610
+	}
4611
+	if err := ioutil.WriteFile(path.Join(mnt2, "test2"), []byte("test2"), 0644); err != nil {
4612
+		c.Fatal(err)
4613
+	}
4614
+	testCatFooBar := func(cName string) error {
4615
+		out, _ := dockerCmd(c, "exec", cName, "cat", "/foo/test1")
4616
+		if out != "test1" {
4617
+			return fmt.Errorf("%s not mounted on /foo", mnt1)
4618
+		}
4619
+		out, _ = dockerCmd(c, "exec", cName, "cat", "/bar/test2")
4620
+		if out != "test2" {
4621
+			return fmt.Errorf("%s not mounted on /bar", mnt2)
4622
+		}
4623
+		return nil
4624
+	}
4625
+
4626
+	type testCase struct {
4627
+		equivalents [][]string
4628
+		valid       bool
4629
+		// fn should be nil if valid==false
4630
+		fn func(cName string) error
4631
+	}
4632
+	cases := []testCase{
4633
+		{
4634
+			equivalents: [][]string{
4635
+				{
4636
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
4637
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/bar", mnt2),
4638
+				},
4639
+				{
4640
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
4641
+					"--mount", fmt.Sprintf("type=bind,src=%s,target=/bar", mnt2),
4642
+				},
4643
+				{
4644
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
4645
+					"--mount", fmt.Sprintf("type=bind,src=%s,target=/bar", mnt2),
4646
+				},
4647
+			},
4648
+			valid: true,
4649
+			fn:    testCatFooBar,
4650
+		},
4651
+		{
4652
+			equivalents: [][]string{
4653
+				{
4654
+					"--mount", fmt.Sprintf("type=volume,src=%s,dst=/foo", mnt1),
4655
+					"--mount", fmt.Sprintf("type=volume,src=%s,dst=/bar", mnt2),
4656
+				},
4657
+				{
4658
+					"--mount", fmt.Sprintf("type=volume,src=%s,dst=/foo", mnt1),
4659
+					"--mount", fmt.Sprintf("type=volume,src=%s,target=/bar", mnt2),
4660
+				},
4661
+			},
4662
+			valid: false,
4663
+		},
4664
+		{
4665
+			equivalents: [][]string{
4666
+				{
4667
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
4668
+					"--mount", fmt.Sprintf("type=volume,src=%s,dst=/bar", mnt2),
4669
+				},
4670
+				{
4671
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
4672
+					"--mount", fmt.Sprintf("type=volume,src=%s,target=/bar", mnt2),
4673
+				},
4674
+			},
4675
+			valid: false,
4676
+			fn:    testCatFooBar,
4677
+		},
4678
+		{
4679
+			equivalents: [][]string{
4680
+				{
4681
+					"--read-only",
4682
+					"--mount", "type=volume,dst=/bar",
4683
+				},
4684
+			},
4685
+			valid: true,
4686
+			fn: func(cName string) error {
4687
+				_, _, err := dockerCmdWithError("exec", cName, "touch", "/bar/icanwritehere")
4688
+				return err
4689
+			},
4690
+		},
4691
+		{
4692
+			equivalents: [][]string{
4693
+				{
4694
+					"--read-only",
4695
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
4696
+					"--mount", "type=volume,dst=/bar",
4697
+				},
4698
+				{
4699
+					"--read-only",
4700
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
4701
+					"--mount", "type=volume,dst=/bar",
4702
+				},
4703
+			},
4704
+			valid: true,
4705
+			fn: func(cName string) error {
4706
+				out, _ := dockerCmd(c, "exec", cName, "cat", "/foo/test1")
4707
+				if out != "test1" {
4708
+					return fmt.Errorf("%s not mounted on /foo", mnt1)
4709
+				}
4710
+				_, _, err := dockerCmdWithError("exec", cName, "touch", "/bar/icanwritehere")
4711
+				return err
4712
+			},
4713
+		},
4714
+		{
4715
+			equivalents: [][]string{
4716
+				{
4717
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
4718
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt2),
4719
+				},
4720
+				{
4721
+					"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
4722
+					"--mount", fmt.Sprintf("type=bind,src=%s,target=/foo", mnt2),
4723
+				},
4724
+				{
4725
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
4726
+					"--mount", fmt.Sprintf("type=bind,src=%s,target=/foo", mnt2),
4727
+				},
4728
+			},
4729
+			valid: false,
4730
+		},
4731
+		{
4732
+			equivalents: [][]string{
4733
+				{
4734
+					"--volume", fmt.Sprintf("%s:/foo", mnt1),
4735
+					"--mount", fmt.Sprintf("type=volume,src=%s,target=/foo", mnt2),
4736
+				},
4737
+			},
4738
+			valid: false,
4739
+		},
4740
+		{
4741
+			equivalents: [][]string{
4742
+				{
4743
+					"--mount", "type=volume,target=/foo",
4744
+					"--mount", "type=volume,target=/foo",
4745
+				},
4746
+			},
4747
+			valid: false,
4748
+		},
4749
+	}
4750
+
4751
+	for i, testCase := range cases {
4752
+		for j, opts := range testCase.equivalents {
4753
+			cName := fmt.Sprintf("mount-%d-%d", i, j)
4754
+			_, _, err := dockerCmdWithError(append([]string{"run", "-i", "-d", "--name", cName},
4755
+				append(opts, []string{"busybox", "top"}...)...)...)
4756
+			if testCase.valid {
4757
+				c.Assert(err, check.IsNil,
4758
+					check.Commentf("got error while creating a container with %v (%s)", opts, cName))
4759
+				c.Assert(testCase.fn(cName), check.IsNil,
4760
+					check.Commentf("got error while executing test for %v (%s)", opts, cName))
4761
+				dockerCmd(c, "rm", "-f", cName)
4762
+			} else {
4763
+				c.Assert(err, checker.NotNil,
4764
+					check.Commentf("got nil while creating a container with %v (%s)", opts, cName))
4765
+			}
4766
+		}
4767
+	}
4768
+}
... ...
@@ -53,6 +53,7 @@ docker-create - Create a new container
53 53
 [**--memory-reservation**[=*MEMORY-RESERVATION*]]
54 54
 [**--memory-swap**[=*LIMIT*]]
55 55
 [**--memory-swappiness**[=*MEMORY-SWAPPINESS*]]
56
+[**--mount**[=*MOUNT*]]
56 57
 [**--name**[=*NAME*]]
57 58
 [**--network-alias**[=*[]*]]
58 59
 [**--network**[=*"bridge"*]]
... ...
@@ -55,6 +55,7 @@ docker-run - Run a command in a new container
55 55
 [**--memory-reservation**[=*MEMORY-RESERVATION*]]
56 56
 [**--memory-swap**[=*LIMIT*]]
57 57
 [**--memory-swappiness**[=*MEMORY-SWAPPINESS*]]
58
+[**--mount**[=*MOUNT*]]
58 59
 [**--name**[=*NAME*]]
59 60
 [**--network-alias**[=*[]*]]
60 61
 [**--network**[=*"bridge"*]]
61 62
new file mode 100644
... ...
@@ -0,0 +1,147 @@
0
+package opts
1
+
2
+import (
3
+	"encoding/csv"
4
+	"fmt"
5
+	"strconv"
6
+	"strings"
7
+
8
+	mounttypes "github.com/docker/docker/api/types/mount"
9
+)
10
+
11
+// MountOpt is a Value type for parsing mounts
12
+type MountOpt struct {
13
+	values []mounttypes.Mount
14
+}
15
+
16
+// Set a new mount value
17
+func (m *MountOpt) Set(value string) error {
18
+	csvReader := csv.NewReader(strings.NewReader(value))
19
+	fields, err := csvReader.Read()
20
+	if err != nil {
21
+		return err
22
+	}
23
+
24
+	mount := mounttypes.Mount{}
25
+
26
+	volumeOptions := func() *mounttypes.VolumeOptions {
27
+		if mount.VolumeOptions == nil {
28
+			mount.VolumeOptions = &mounttypes.VolumeOptions{
29
+				Labels: make(map[string]string),
30
+			}
31
+		}
32
+		if mount.VolumeOptions.DriverConfig == nil {
33
+			mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
34
+		}
35
+		return mount.VolumeOptions
36
+	}
37
+
38
+	bindOptions := func() *mounttypes.BindOptions {
39
+		if mount.BindOptions == nil {
40
+			mount.BindOptions = new(mounttypes.BindOptions)
41
+		}
42
+		return mount.BindOptions
43
+	}
44
+
45
+	setValueOnMap := func(target map[string]string, value string) {
46
+		parts := strings.SplitN(value, "=", 2)
47
+		if len(parts) == 1 {
48
+			target[value] = ""
49
+		} else {
50
+			target[parts[0]] = parts[1]
51
+		}
52
+	}
53
+
54
+	mount.Type = mounttypes.TypeVolume // default to volume mounts
55
+	// Set writable as the default
56
+	for _, field := range fields {
57
+		parts := strings.SplitN(field, "=", 2)
58
+		key := strings.ToLower(parts[0])
59
+
60
+		if len(parts) == 1 {
61
+			switch key {
62
+			case "readonly", "ro":
63
+				mount.ReadOnly = true
64
+				continue
65
+			case "volume-nocopy":
66
+				volumeOptions().NoCopy = true
67
+				continue
68
+			}
69
+		}
70
+
71
+		if len(parts) != 2 {
72
+			return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
73
+		}
74
+
75
+		value := parts[1]
76
+		switch key {
77
+		case "type":
78
+			mount.Type = mounttypes.Type(strings.ToLower(value))
79
+		case "source", "src":
80
+			mount.Source = value
81
+		case "target", "dst", "destination":
82
+			mount.Target = value
83
+		case "readonly", "ro":
84
+			mount.ReadOnly, err = strconv.ParseBool(value)
85
+			if err != nil {
86
+				return fmt.Errorf("invalid value for %s: %s", key, value)
87
+			}
88
+		case "bind-propagation":
89
+			bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value))
90
+		case "volume-nocopy":
91
+			volumeOptions().NoCopy, err = strconv.ParseBool(value)
92
+			if err != nil {
93
+				return fmt.Errorf("invalid value for populate: %s", value)
94
+			}
95
+		case "volume-label":
96
+			setValueOnMap(volumeOptions().Labels, value)
97
+		case "volume-driver":
98
+			volumeOptions().DriverConfig.Name = value
99
+		case "volume-opt":
100
+			if volumeOptions().DriverConfig.Options == nil {
101
+				volumeOptions().DriverConfig.Options = make(map[string]string)
102
+			}
103
+			setValueOnMap(volumeOptions().DriverConfig.Options, value)
104
+		default:
105
+			return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
106
+		}
107
+	}
108
+
109
+	if mount.Type == "" {
110
+		return fmt.Errorf("type is required")
111
+	}
112
+
113
+	if mount.Target == "" {
114
+		return fmt.Errorf("target is required")
115
+	}
116
+
117
+	if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
118
+		return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
119
+	}
120
+	if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
121
+		return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
122
+	}
123
+
124
+	m.values = append(m.values, mount)
125
+	return nil
126
+}
127
+
128
+// Type returns the type of this option
129
+func (m *MountOpt) Type() string {
130
+	return "mount"
131
+}
132
+
133
+// String returns a string repr of this option
134
+func (m *MountOpt) String() string {
135
+	mounts := []string{}
136
+	for _, mount := range m.values {
137
+		repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
138
+		mounts = append(mounts, repr)
139
+	}
140
+	return strings.Join(mounts, ", ")
141
+}
142
+
143
+// Value returns the mounts
144
+func (m *MountOpt) Value() []mounttypes.Mount {
145
+	return m.values
146
+}
0 147
new file mode 100644
... ...
@@ -0,0 +1,153 @@
0
+package opts
1
+
2
+import (
3
+	"testing"
4
+
5
+	mounttypes "github.com/docker/docker/api/types/mount"
6
+	"github.com/docker/docker/pkg/testutil/assert"
7
+)
8
+
9
+func TestMountOptString(t *testing.T) {
10
+	mount := MountOpt{
11
+		values: []mounttypes.Mount{
12
+			{
13
+				Type:   mounttypes.TypeBind,
14
+				Source: "/home/path",
15
+				Target: "/target",
16
+			},
17
+			{
18
+				Type:   mounttypes.TypeVolume,
19
+				Source: "foo",
20
+				Target: "/target/foo",
21
+			},
22
+		},
23
+	}
24
+	expected := "bind /home/path /target, volume foo /target/foo"
25
+	assert.Equal(t, mount.String(), expected)
26
+}
27
+
28
+func TestMountOptSetBindNoErrorBind(t *testing.T) {
29
+	for _, testcase := range []string{
30
+		// tests several aliases that should have same result.
31
+		"type=bind,target=/target,source=/source",
32
+		"type=bind,src=/source,dst=/target",
33
+		"type=bind,source=/source,dst=/target",
34
+		"type=bind,src=/source,target=/target",
35
+	} {
36
+		var mount MountOpt
37
+
38
+		assert.NilError(t, mount.Set(testcase))
39
+
40
+		mounts := mount.Value()
41
+		assert.Equal(t, len(mounts), 1)
42
+		assert.Equal(t, mounts[0], mounttypes.Mount{
43
+			Type:   mounttypes.TypeBind,
44
+			Source: "/source",
45
+			Target: "/target",
46
+		})
47
+	}
48
+}
49
+
50
+func TestMountOptSetVolumeNoError(t *testing.T) {
51
+	for _, testcase := range []string{
52
+		// tests several aliases that should have same result.
53
+		"type=volume,target=/target,source=/source",
54
+		"type=volume,src=/source,dst=/target",
55
+		"type=volume,source=/source,dst=/target",
56
+		"type=volume,src=/source,target=/target",
57
+	} {
58
+		var mount MountOpt
59
+
60
+		assert.NilError(t, mount.Set(testcase))
61
+
62
+		mounts := mount.Value()
63
+		assert.Equal(t, len(mounts), 1)
64
+		assert.Equal(t, mounts[0], mounttypes.Mount{
65
+			Type:   mounttypes.TypeVolume,
66
+			Source: "/source",
67
+			Target: "/target",
68
+		})
69
+	}
70
+}
71
+
72
+// TestMountOptDefaultType ensures that a mount without the type defaults to a
73
+// volume mount.
74
+func TestMountOptDefaultType(t *testing.T) {
75
+	var mount MountOpt
76
+	assert.NilError(t, mount.Set("target=/target,source=/foo"))
77
+	assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume)
78
+}
79
+
80
+func TestMountOptSetErrorNoTarget(t *testing.T) {
81
+	var mount MountOpt
82
+	assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
83
+}
84
+
85
+func TestMountOptSetErrorInvalidKey(t *testing.T) {
86
+	var mount MountOpt
87
+	assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
88
+}
89
+
90
+func TestMountOptSetErrorInvalidField(t *testing.T) {
91
+	var mount MountOpt
92
+	assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
93
+}
94
+
95
+func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
96
+	var mount MountOpt
97
+	assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
98
+	assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
99
+}
100
+
101
+func TestMountOptDefaultEnableReadOnly(t *testing.T) {
102
+	var m MountOpt
103
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
104
+	assert.Equal(t, m.values[0].ReadOnly, false)
105
+
106
+	m = MountOpt{}
107
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
108
+	assert.Equal(t, m.values[0].ReadOnly, true)
109
+
110
+	m = MountOpt{}
111
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
112
+	assert.Equal(t, m.values[0].ReadOnly, true)
113
+
114
+	m = MountOpt{}
115
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true"))
116
+	assert.Equal(t, m.values[0].ReadOnly, true)
117
+
118
+	m = MountOpt{}
119
+	assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
120
+	assert.Equal(t, m.values[0].ReadOnly, false)
121
+}
122
+
123
+func TestMountOptVolumeNoCopy(t *testing.T) {
124
+	var m MountOpt
125
+	assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy"))
126
+	assert.Equal(t, m.values[0].Source, "")
127
+
128
+	m = MountOpt{}
129
+	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
130
+	assert.Equal(t, m.values[0].VolumeOptions == nil, true)
131
+
132
+	m = MountOpt{}
133
+	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
134
+	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
135
+	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
136
+
137
+	m = MountOpt{}
138
+	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
139
+	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
140
+	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
141
+
142
+	m = MountOpt{}
143
+	assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
144
+	assert.Equal(t, m.values[0].VolumeOptions != nil, true)
145
+	assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
146
+}
147
+
148
+func TestMountOptTypeConflict(t *testing.T) {
149
+	var m MountOpt
150
+	assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
151
+	assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
152
+}
... ...
@@ -26,6 +26,7 @@ type ContainerOptions struct {
26 26
 	attach             opts.ListOpts
27 27
 	volumes            opts.ListOpts
28 28
 	tmpfs              opts.ListOpts
29
+	mounts             opts.MountOpt
29 30
 	blkioWeightDevice  WeightdeviceOpt
30 31
 	deviceReadBps      ThrottledeviceOpt
31 32
 	deviceWriteBps     ThrottledeviceOpt
... ...
@@ -210,6 +211,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions {
210 210
 	flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory")
211 211
 	flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)")
212 212
 	flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume")
213
+	flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container")
213 214
 
214 215
 	// Health-checking
215 216
 	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
347 347
 		}
348 348
 	}
349 349
 
350
+	mounts := copts.mounts.Value()
351
+
350 352
 	var binds []string
351 353
 	volumes := copts.volumes.GetMap()
352 354
 	// add any bind targets to the list of container volumes
... ...
@@ -612,6 +616,7 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c
612 612
 		Tmpfs:          tmpfs,
613 613
 		Sysctls:        copts.sysctls.GetAll(),
614 614
 		Runtime:        copts.runtime,
615
+		Mounts:         mounts,
615 616
 	}
616 617
 
617 618
 	// only set this value if the user provided the flag, else it should default to nil