Browse code

Add new `HostConfig` field, `Mounts`.

`Mounts` allows users to specify in a much safer way the volumes they
want to use in the container.
This replaces `Binds` and `Volumes`, which both still exist, but
`Mounts` and `Binds`/`Volumes` are exclussive.
The CLI will continue to use `Binds` and `Volumes` due to concerns with
parsing the volume specs on the client side and cross-platform support
(for now).

The new API follows exactly the services mount API.

Example usage of `Mounts`:

```
$ curl -XPOST localhost:2375/containers/create -d '{
"Image": "alpine:latest",
"HostConfig": {
"Mounts": [{
"Type": "Volume",
"Target": "/foo"
},{
"Type": "bind",
"Source": "/var/run/docker.sock",
"Target": "/var/run/docker.sock",
},{
"Type": "volume",
"Name": "important_data",
"Target": "/var/data",
"ReadOnly": true,
"VolumeOptions": {
"DriverConfig": {
Name: "awesomeStorage",
Options: {"size": "10m"},
Labels: {"some":"label"}
}
}]
}
}'
```

There are currently 2 types of mounts:

- **bind**: Paths on the host that get mounted into the
container. Paths must exist prior to creating the container.
- **volume**: Volumes that persist after the
container is removed.

Not all fields are available in each type, and validation is done to
ensure these fields aren't mixed up between types.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2016/04/27 03:25:35
Showing 30 changed files
... ...
@@ -17,6 +17,7 @@ import (
17 17
 
18 18
 	"github.com/Sirupsen/logrus"
19 19
 	containertypes "github.com/docker/docker/api/types/container"
20
+	mounttypes "github.com/docker/docker/api/types/mount"
20 21
 	networktypes "github.com/docker/docker/api/types/network"
21 22
 	"github.com/docker/docker/daemon/exec"
22 23
 	"github.com/docker/docker/daemon/logger"
... ...
@@ -551,6 +552,7 @@ func (container *Container) ShouldRestart() bool {
551 551
 // AddMountPointWithVolume adds a new mount point configured with a volume to the container.
552 552
 func (container *Container) AddMountPointWithVolume(destination string, vol volume.Volume, rw bool) {
553 553
 	container.MountPoints[destination] = &volume.MountPoint{
554
+		Type:        mounttypes.TypeVolume,
554 555
 		Name:        vol.Name(),
555 556
 		Driver:      vol.DriverName(),
556 557
 		Destination: destination,
... ...
@@ -9,6 +9,7 @@ import (
9 9
 
10 10
 	"github.com/Sirupsen/logrus"
11 11
 	containertypes "github.com/docker/docker/api/types/container"
12
+	mounttypes "github.com/docker/docker/api/types/mount"
12 13
 	"github.com/docker/docker/container"
13 14
 	"github.com/docker/docker/pkg/stringid"
14 15
 	"github.com/opencontainers/runc/libcontainer/label"
... ...
@@ -63,7 +64,11 @@ func (daemon *Daemon) createContainerPlatformSpecificSettings(container *contain
63 63
 // this is only called when the container is created.
64 64
 func (daemon *Daemon) populateVolumes(c *container.Container) error {
65 65
 	for _, mnt := range c.MountPoints {
66
-		if !mnt.CopyData || mnt.Volume == nil {
66
+		if mnt.Volume == nil {
67
+			continue
68
+		}
69
+
70
+		if mnt.Type != mounttypes.TypeVolume || !mnt.CopyData {
67 71
 			continue
68 72
 		}
69 73
 
... ...
@@ -18,7 +18,7 @@ func (daemon *Daemon) createContainerPlatformSpecificSettings(container *contain
18 18
 
19 19
 	for spec := range config.Volumes {
20 20
 
21
-		mp, err := volume.ParseMountSpec(spec, hostConfig.VolumeDriver)
21
+		mp, err := volume.ParseMountRaw(spec, hostConfig.VolumeDriver)
22 22
 		if err != nil {
23 23
 			return fmt.Errorf("Unrecognised volume spec: %v", err)
24 24
 		}
... ...
@@ -68,6 +68,7 @@ func addMountPoints(container *container.Container) []types.MountPoint {
68 68
 	mountPoints := make([]types.MountPoint, 0, len(container.MountPoints))
69 69
 	for _, m := range container.MountPoints {
70 70
 		mountPoints = append(mountPoints, types.MountPoint{
71
+			Type:        m.Type,
71 72
 			Name:        m.Name,
72 73
 			Source:      m.Path(),
73 74
 			Destination: m.Destination,
... ...
@@ -16,6 +16,7 @@ func addMountPoints(container *container.Container) []types.MountPoint {
16 16
 	mountPoints := make([]types.MountPoint, 0, len(container.MountPoints))
17 17
 	for _, m := range container.MountPoints {
18 18
 		mountPoints = append(mountPoints, types.MountPoint{
19
+			Type:        m.Type,
19 20
 			Name:        m.Name,
20 21
 			Source:      m.Path(),
21 22
 			Destination: m.Destination,
... ...
@@ -27,7 +27,7 @@ func (daemon *Daemon) removeMountPoints(container *container.Container, rm bool)
27 27
 		if rm {
28 28
 			// Do not remove named mountpoints
29 29
 			// these are mountpoints specified like `docker run -v <name>:/foo`
30
-			if m.Named {
30
+			if m.Spec.Source != "" {
31 31
 				continue
32 32
 			}
33 33
 			err := daemon.volumes.Remove(m.Volume)
... ...
@@ -9,8 +9,11 @@ import (
9 9
 
10 10
 	"github.com/docker/docker/api/types"
11 11
 	containertypes "github.com/docker/docker/api/types/container"
12
+	mounttypes "github.com/docker/docker/api/types/mount"
12 13
 	"github.com/docker/docker/container"
14
+	dockererrors "github.com/docker/docker/errors"
13 15
 	"github.com/docker/docker/volume"
16
+	"github.com/opencontainers/runc/libcontainer/label"
14 17
 )
15 18
 
16 19
 var (
... ...
@@ -106,7 +109,8 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
106 106
 				Driver:      m.Driver,
107 107
 				Destination: m.Destination,
108 108
 				Propagation: m.Propagation,
109
-				Named:       m.Named,
109
+				Spec:        m.Spec,
110
+				CopyData:    false,
110 111
 			}
111 112
 
112 113
 			if len(cp.Source) == 0 {
... ...
@@ -123,18 +127,18 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
123 123
 
124 124
 	// 3. Read bind mounts
125 125
 	for _, b := range hostConfig.Binds {
126
-		// #10618
127
-		bind, err := volume.ParseMountSpec(b, hostConfig.VolumeDriver)
126
+		bind, err := volume.ParseMountRaw(b, hostConfig.VolumeDriver)
128 127
 		if err != nil {
129 128
 			return err
130 129
 		}
131 130
 
131
+		// #10618
132 132
 		_, tmpfsExists := hostConfig.Tmpfs[bind.Destination]
133 133
 		if binds[bind.Destination] || tmpfsExists {
134 134
 			return fmt.Errorf("Duplicate mount point '%s'", bind.Destination)
135 135
 		}
136 136
 
137
-		if len(bind.Name) > 0 {
137
+		if bind.Type == mounttypes.TypeVolume {
138 138
 			// create the volume
139 139
 			v, err := daemon.volumes.CreateWithRef(bind.Name, bind.Driver, container.ID, nil, nil)
140 140
 			if err != nil {
... ...
@@ -144,9 +148,8 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
144 144
 			bind.Source = v.Path()
145 145
 			// bind.Name is an already existing volume, we need to use that here
146 146
 			bind.Driver = v.DriverName()
147
-			bind.Named = true
148
-			if bind.Driver == "local" {
149
-				bind = setBindModeIfNull(bind)
147
+			if bind.Driver == volume.DefaultDriverName {
148
+				setBindModeIfNull(bind)
150 149
 			}
151 150
 		}
152 151
 
... ...
@@ -154,6 +157,50 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo
154 154
 		mountPoints[bind.Destination] = bind
155 155
 	}
156 156
 
157
+	for _, cfg := range hostConfig.Mounts {
158
+		mp, err := volume.ParseMountSpec(cfg)
159
+		if err != nil {
160
+			return dockererrors.NewBadRequestError(err)
161
+		}
162
+
163
+		if binds[mp.Destination] {
164
+			return fmt.Errorf("Duplicate mount point '%s'", cfg.Target)
165
+		}
166
+
167
+		if mp.Type == mounttypes.TypeVolume {
168
+			var v volume.Volume
169
+			if cfg.VolumeOptions != nil {
170
+				var driverOpts map[string]string
171
+				if cfg.VolumeOptions.DriverConfig != nil {
172
+					driverOpts = cfg.VolumeOptions.DriverConfig.Options
173
+				}
174
+				v, err = daemon.volumes.CreateWithRef(mp.Name, mp.Driver, container.ID, driverOpts, cfg.VolumeOptions.Labels)
175
+			} else {
176
+				v, err = daemon.volumes.CreateWithRef(mp.Name, mp.Driver, container.ID, nil, nil)
177
+			}
178
+			if err != nil {
179
+				return err
180
+			}
181
+
182
+			if err := label.Relabel(mp.Source, container.MountLabel, false); err != nil {
183
+				return err
184
+			}
185
+			mp.Volume = v
186
+			mp.Name = v.Name()
187
+			mp.Driver = v.DriverName()
188
+
189
+			// only use the cached path here since getting the path is not neccessary right now and calling `Path()` may be slow
190
+			if cv, ok := v.(interface {
191
+				CachedPath() string
192
+			}); ok {
193
+				mp.Source = cv.CachedPath()
194
+			}
195
+		}
196
+
197
+		binds[mp.Destination] = true
198
+		mountPoints[mp.Destination] = mp
199
+	}
200
+
157 201
 	container.Lock()
158 202
 
159 203
 	// 4. Cleanup old volumes that are about to be reassigned.
... ...
@@ -85,11 +85,10 @@ func sortMounts(m []container.Mount) []container.Mount {
85 85
 // setBindModeIfNull is platform specific processing to ensure the
86 86
 // shared mode is set to 'z' if it is null. This is called in the case
87 87
 // of processing a named volume and not a typical bind.
88
-func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint {
88
+func setBindModeIfNull(bind *volume.MountPoint) {
89 89
 	if bind.Mode == "" {
90 90
 		bind.Mode = "z"
91 91
 	}
92
-	return bind
93 92
 }
94 93
 
95 94
 // migrateVolume links the contents of a volume created pre Docker 1.7
... ...
@@ -46,6 +46,6 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er
46 46
 
47 47
 // setBindModeIfNull is platform specific processing which is a no-op on
48 48
 // Windows.
49
-func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint {
50
-	return bind
49
+func setBindModeIfNull(bind *volume.MountPoint) {
50
+	return
51 51
 }
... ...
@@ -122,6 +122,7 @@ This section lists each version from latest to oldest.  Each listing includes a
122 122
 * `DELETE /volumes/(name)` now accepts a `force` query parameter to force removal of volumes that were already removed out of band by the volume driver plugin.
123 123
 * `POST /containers/create/` and `POST /containers/(name)/update` now validates restart policies.
124 124
 * `POST /containers/create` now validates IPAMConfig in NetworkingConfig, and returns error for invalid IPv4 and IPv6 addresses (`--ip` and `--ip6` in `docker create/run`).
125
+* `POST /containers/create` now takes a `Mounts` field in `HostConfig` which replaces `Binds` and `Volumes`. *note*: `Binds` and `Volumes` are still available but are exclusive with `Mounts`
125 126
 
126 127
 ### v1.24 API changes
127 128
 
... ...
@@ -334,7 +334,8 @@ Create a container
334 334
              "StorageOpt": {},
335 335
              "CgroupParent": "",
336 336
              "VolumeDriver": "",
337
-             "ShmSize": 67108864
337
+             "ShmSize": 67108864,
338
+             "Mounts": []
338 339
           },
339 340
           "NetworkingConfig": {
340 341
               "EndpointsConfig": {
... ...
@@ -610,7 +611,8 @@ Return low-level information on the container `id`
610 610
 			"VolumesFrom": null,
611 611
 			"Ulimits": [{}],
612 612
 			"VolumeDriver": "",
613
-			"ShmSize": 67108864
613
+			"ShmSize": 67108864,
614
+			"Mounts": []
614 615
 		},
615 616
 		"HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname",
616 617
 		"HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts",
... ...
@@ -486,6 +486,24 @@ Create a container
486 486
     -   **CgroupParent** - Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist.
487 487
     -   **VolumeDriver** - Driver that this container users to mount volumes.
488 488
     -   **ShmSize** - Size of `/dev/shm` in bytes. The size must be greater than 0.  If omitted the system uses 64MB.
489
+    -   **Mounts** – Specification for mounts to be added to the container.
490
+        - **Target** – Container path.
491
+        - **Source** – Mount source (e.g. a volume name, a host path).
492
+        - **Type** – The mount type (`bind`, or `volume`).
493
+          Available types (for the `Type` field):
494
+          - **bind** - Mounts a file or directory from the host into the container. Must exist prior to creating the container.
495
+          - **volume** - Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed.
496
+        - **ReadOnly** – A boolean indicating whether the mount should be read-only.
497
+        - **BindOptions** - Optional configuration for the `bind` type.
498
+          - **Propagation** – A propagation mode with the value `[r]private`, `[r]shared`, or `[r]slave`.
499
+        - **VolumeOptions** – Optional configuration for the `volume` type.
500
+            - **NoCopy** – A boolean indicating if volume should be
501
+              populated with the data from the target. (Default false)
502
+            - **Labels** – User-defined name and labels for the volume as key/value pairs: `{"name": "value"}`
503
+            - **DriverConfig** – Map of driver-specific options.
504
+              - **Name** - Name of the driver to use to create the volume.
505
+              - **Options** - key/value map of driver specific options.
506
+
489 507
 
490 508
 **Query parameters**:
491 509
 
... ...
@@ -6,10 +6,12 @@ import (
6 6
 	"encoding/json"
7 7
 	"fmt"
8 8
 	"io"
9
+	"io/ioutil"
9 10
 	"net/http"
10 11
 	"net/http/httputil"
11 12
 	"net/url"
12 13
 	"os"
14
+	"path/filepath"
13 15
 	"regexp"
14 16
 	"strconv"
15 17
 	"strings"
... ...
@@ -17,10 +19,14 @@ import (
17 17
 
18 18
 	"github.com/docker/docker/api/types"
19 19
 	containertypes "github.com/docker/docker/api/types/container"
20
+	mounttypes "github.com/docker/docker/api/types/mount"
20 21
 	networktypes "github.com/docker/docker/api/types/network"
21 22
 	"github.com/docker/docker/pkg/integration"
22 23
 	"github.com/docker/docker/pkg/integration/checker"
24
+	"github.com/docker/docker/pkg/ioutils"
25
+	"github.com/docker/docker/pkg/mount"
23 26
 	"github.com/docker/docker/pkg/stringid"
27
+	"github.com/docker/docker/volume"
24 28
 	"github.com/go-check/check"
25 29
 )
26 30
 
... ...
@@ -1525,3 +1531,212 @@ func (s *DockerSuite) TestContainerApiStatsWithNetworkDisabled(c *check.C) {
1525 1525
 		c.Assert(dec.Decode(&s), checker.IsNil)
1526 1526
 	}
1527 1527
 }
1528
+
1529
+func (s *DockerSuite) TestContainersApiCreateMountsValidation(c *check.C) {
1530
+	type m mounttypes.Mount
1531
+	type hc struct{ Mounts []m }
1532
+	type cfg struct {
1533
+		Image      string
1534
+		HostConfig hc
1535
+	}
1536
+	type testCase struct {
1537
+		config cfg
1538
+		status int
1539
+		msg    string
1540
+	}
1541
+
1542
+	prefix, slash := getPrefixAndSlashFromDaemonPlatform()
1543
+	destPath := prefix + slash + "foo"
1544
+	notExistPath := prefix + slash + "notexist"
1545
+
1546
+	cases := []testCase{
1547
+		{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "notreal", Target: destPath}}}}, http.StatusBadRequest, "mount type unknown"},
1548
+		{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind"}}}}, http.StatusBadRequest, "Target must not be empty"},
1549
+		{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind", Target: destPath}}}}, http.StatusBadRequest, "Source must not be empty"},
1550
+		{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind", Source: notExistPath, Target: destPath}}}}, http.StatusBadRequest, "bind source path does not exist"},
1551
+		{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "volume"}}}}, http.StatusBadRequest, "Target must not be empty"},
1552
+		{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "volume", Source: "hello", Target: destPath}}}}, http.StatusCreated, ""},
1553
+		{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "volume", Source: "hello2", Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{DriverConfig: &mounttypes.Driver{Name: "local"}}}}}}, http.StatusCreated, ""},
1554
+	}
1555
+
1556
+	if SameHostDaemon.Condition() {
1557
+		tmpDir, err := ioutils.TempDir("", "test-mounts-api")
1558
+		c.Assert(err, checker.IsNil)
1559
+		defer os.RemoveAll(tmpDir)
1560
+		cases = append(cases, []testCase{
1561
+			{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind", Source: tmpDir, Target: destPath}}}}, http.StatusCreated, ""},
1562
+			{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind", Source: tmpDir, Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{}}}}}, http.StatusBadRequest, "VolumeOptions must not be specified"},
1563
+		}...)
1564
+	}
1565
+
1566
+	if DaemonIsLinux.Condition() {
1567
+		cases = append(cases, []testCase{
1568
+			{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "volume", Source: "hello3", Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{DriverConfig: &mounttypes.Driver{Name: "local", Options: map[string]string{"o": "size=1"}}}}}}}, http.StatusCreated, ""},
1569
+		}...)
1570
+
1571
+	}
1572
+
1573
+	for i, x := range cases {
1574
+		c.Logf("case %d", i)
1575
+		status, b, err := sockRequest("POST", "/containers/create", x.config)
1576
+		c.Assert(err, checker.IsNil)
1577
+		c.Assert(status, checker.Equals, x.status, check.Commentf("%s\n%v", string(b), cases[i].config))
1578
+		if len(x.msg) > 0 {
1579
+			c.Assert(string(b), checker.Contains, x.msg, check.Commentf("%v", cases[i].config))
1580
+		}
1581
+	}
1582
+}
1583
+
1584
+func (s *DockerSuite) TestContainerApiCreateMountsBindRead(c *check.C) {
1585
+	testRequires(c, NotUserNamespace, SameHostDaemon)
1586
+	// also with data in the host side
1587
+	prefix, slash := getPrefixAndSlashFromDaemonPlatform()
1588
+	destPath := prefix + slash + "foo"
1589
+	tmpDir, err := ioutil.TempDir("", "test-mounts-api-bind")
1590
+	c.Assert(err, checker.IsNil)
1591
+	defer os.RemoveAll(tmpDir)
1592
+	err = ioutil.WriteFile(filepath.Join(tmpDir, "bar"), []byte("hello"), 666)
1593
+	c.Assert(err, checker.IsNil)
1594
+
1595
+	data := map[string]interface{}{
1596
+		"Image":      "busybox",
1597
+		"Cmd":        []string{"/bin/sh", "-c", "cat /foo/bar"},
1598
+		"HostConfig": map[string]interface{}{"Mounts": []map[string]interface{}{{"Type": "bind", "Source": tmpDir, "Target": destPath}}},
1599
+	}
1600
+	status, resp, err := sockRequest("POST", "/containers/create?name=test", data)
1601
+	c.Assert(err, checker.IsNil, check.Commentf(string(resp)))
1602
+	c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(resp)))
1603
+
1604
+	out, _ := dockerCmd(c, "start", "-a", "test")
1605
+	c.Assert(out, checker.Equals, "hello")
1606
+}
1607
+
1608
+// Test Mounts comes out as expected for the MountPoint
1609
+func (s *DockerSuite) TestContainersApiCreateMountsCreate(c *check.C) {
1610
+	prefix, slash := getPrefixAndSlashFromDaemonPlatform()
1611
+	destPath := prefix + slash + "foo"
1612
+
1613
+	var (
1614
+		err     error
1615
+		testImg string
1616
+	)
1617
+	if daemonPlatform != "windows" {
1618
+		testImg, err = buildImage("test-mount-config", `
1619
+	FROM busybox
1620
+	RUN mkdir `+destPath+` && touch `+destPath+slash+`bar
1621
+	CMD cat `+destPath+slash+`bar
1622
+	`, true)
1623
+	} else {
1624
+		testImg = "busybox"
1625
+	}
1626
+	c.Assert(err, checker.IsNil)
1627
+
1628
+	type testCase struct {
1629
+		cfg      mounttypes.Mount
1630
+		expected types.MountPoint
1631
+	}
1632
+
1633
+	cases := []testCase{
1634
+		// use literal strings here for `Type` instead of the defined constants in the volume package to keep this honest
1635
+		// Validation of the actual `Mount` struct is done in another test is not needed here
1636
+		{mounttypes.Mount{Type: "volume", Target: destPath}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", RW: true, Destination: destPath}},
1637
+		{mounttypes.Mount{Type: "volume", Target: destPath + slash}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", RW: true, Destination: destPath}},
1638
+		{mounttypes.Mount{Type: "volume", Target: destPath, Source: "test1"}, types.MountPoint{Type: "volume", Name: "test1", RW: true, Destination: destPath}},
1639
+		{mounttypes.Mount{Type: "volume", Target: destPath, ReadOnly: true, Source: "test2"}, types.MountPoint{Type: "volume", Name: "test2", RW: false, Destination: destPath}},
1640
+		{mounttypes.Mount{Type: "volume", Target: destPath, Source: "test3", VolumeOptions: &mounttypes.VolumeOptions{DriverConfig: &mounttypes.Driver{Name: volume.DefaultDriverName}}}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", Name: "test3", RW: true, Destination: destPath}},
1641
+	}
1642
+
1643
+	if SameHostDaemon.Condition() {
1644
+		// setup temp dir for testing binds
1645
+		tmpDir1, err := ioutil.TempDir("", "test-mounts-api-1")
1646
+		c.Assert(err, checker.IsNil)
1647
+		defer os.RemoveAll(tmpDir1)
1648
+		cases = append(cases, []testCase{
1649
+			{mounttypes.Mount{Type: "bind", Source: tmpDir1, Target: destPath}, types.MountPoint{Type: "bind", RW: true, Destination: destPath, Source: tmpDir1}},
1650
+			{mounttypes.Mount{Type: "bind", Source: tmpDir1, Target: destPath, ReadOnly: true}, types.MountPoint{Type: "bind", RW: false, Destination: destPath, Source: tmpDir1}},
1651
+		}...)
1652
+
1653
+		// for modes only supported on Linux
1654
+		if DaemonIsLinux.Condition() {
1655
+			tmpDir3, err := ioutils.TempDir("", "test-mounts-api-3")
1656
+			c.Assert(err, checker.IsNil)
1657
+			defer os.RemoveAll(tmpDir3)
1658
+
1659
+			c.Assert(mount.Mount(tmpDir3, tmpDir3, "none", "bind,rw"), checker.IsNil)
1660
+			c.Assert(mount.ForceMount("", tmpDir3, "none", "shared"), checker.IsNil)
1661
+
1662
+			cases = append(cases, []testCase{
1663
+				{mounttypes.Mount{Type: "bind", Source: tmpDir3, Target: destPath}, types.MountPoint{Type: "bind", RW: true, Destination: destPath, Source: tmpDir3}},
1664
+				{mounttypes.Mount{Type: "bind", Source: tmpDir3, Target: destPath, ReadOnly: true}, types.MountPoint{Type: "bind", RW: false, Destination: destPath, Source: tmpDir3}},
1665
+				{mounttypes.Mount{Type: "bind", Source: tmpDir3, Target: destPath, ReadOnly: true, BindOptions: &mounttypes.BindOptions{Propagation: "shared"}}, types.MountPoint{Type: "bind", RW: false, Destination: destPath, Source: tmpDir3, Propagation: "shared"}},
1666
+			}...)
1667
+		}
1668
+	}
1669
+
1670
+	if daemonPlatform != "windows" { // Windows does not support volume populate
1671
+		cases = append(cases, []testCase{
1672
+			{mounttypes.Mount{Type: "volume", Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", RW: true, Destination: destPath}},
1673
+			{mounttypes.Mount{Type: "volume", Target: destPath + slash, VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", RW: true, Destination: destPath}},
1674
+			{mounttypes.Mount{Type: "volume", Target: destPath, Source: "test4", VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, types.MountPoint{Type: "volume", Name: "test4", RW: true, Destination: destPath}},
1675
+			{mounttypes.Mount{Type: "volume", Target: destPath, Source: "test5", ReadOnly: true, VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, types.MountPoint{Type: "volume", Name: "test5", RW: false, Destination: destPath}},
1676
+		}...)
1677
+	}
1678
+
1679
+	type wrapper struct {
1680
+		containertypes.Config
1681
+		HostConfig containertypes.HostConfig
1682
+	}
1683
+	type createResp struct {
1684
+		ID string `json:"Id"`
1685
+	}
1686
+	for i, x := range cases {
1687
+		c.Logf("case %d - config: %v", i, x.cfg)
1688
+		status, data, err := sockRequest("POST", "/containers/create", wrapper{containertypes.Config{Image: testImg}, containertypes.HostConfig{Mounts: []mounttypes.Mount{x.cfg}}})
1689
+		c.Assert(err, checker.IsNil, check.Commentf(string(data)))
1690
+		c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(data)))
1691
+
1692
+		var resp createResp
1693
+		err = json.Unmarshal(data, &resp)
1694
+		c.Assert(err, checker.IsNil, check.Commentf(string(data)))
1695
+		id := resp.ID
1696
+
1697
+		var mps []types.MountPoint
1698
+		err = json.NewDecoder(strings.NewReader(inspectFieldJSON(c, id, "Mounts"))).Decode(&mps)
1699
+		c.Assert(err, checker.IsNil)
1700
+		c.Assert(mps, checker.HasLen, 1)
1701
+		c.Assert(mps[0].Destination, checker.Equals, x.expected.Destination)
1702
+
1703
+		if len(x.expected.Source) > 0 {
1704
+			c.Assert(mps[0].Source, checker.Equals, x.expected.Source)
1705
+		}
1706
+		if len(x.expected.Name) > 0 {
1707
+			c.Assert(mps[0].Name, checker.Equals, x.expected.Name)
1708
+		}
1709
+		if len(x.expected.Driver) > 0 {
1710
+			c.Assert(mps[0].Driver, checker.Equals, x.expected.Driver)
1711
+		}
1712
+		c.Assert(mps[0].RW, checker.Equals, x.expected.RW)
1713
+		c.Assert(mps[0].Type, checker.Equals, x.expected.Type)
1714
+		c.Assert(mps[0].Mode, checker.Equals, x.expected.Mode)
1715
+		if len(x.expected.Propagation) > 0 {
1716
+			c.Assert(mps[0].Propagation, checker.Equals, x.expected.Propagation)
1717
+		}
1718
+
1719
+		out, _, err := dockerCmdWithError("start", "-a", id)
1720
+		if (x.cfg.Type != "volume" || (x.cfg.VolumeOptions != nil && x.cfg.VolumeOptions.NoCopy)) && daemonPlatform != "windows" {
1721
+			c.Assert(err, checker.NotNil, check.Commentf("%s\n%v", out, mps[0]))
1722
+		} else {
1723
+			c.Assert(err, checker.IsNil, check.Commentf("%s\n%v", out, mps[0]))
1724
+		}
1725
+
1726
+		dockerCmd(c, "rm", "-fv", id)
1727
+		if x.cfg.Type == "volume" && len(x.cfg.Source) > 0 {
1728
+			// This should still exist even though we removed the container
1729
+			dockerCmd(c, "volume", "inspect", mps[0].Name)
1730
+		} else {
1731
+			// This should be removed automatically when we removed the container
1732
+			out, _, err := dockerCmdWithError("volume", "inspect", mps[0].Name)
1733
+			c.Assert(err, checker.NotNil, check.Commentf(out))
1734
+		}
1735
+	}
1736
+}
... ...
@@ -4272,15 +4272,9 @@ func (s *DockerSuite) TestRunVolumesMountedAsSlave(c *check.C) {
4272 4272
 
4273 4273
 func (s *DockerSuite) TestRunNamedVolumesMountedAsShared(c *check.C) {
4274 4274
 	testRequires(c, DaemonIsLinux, NotUserNamespace)
4275
-	out, exitcode, _ := dockerCmdWithError("run", "-v", "foo:/test:shared", "busybox", "touch", "/test/somefile")
4276
-
4277
-	if exitcode == 0 {
4278
-		c.Fatalf("expected non-zero exit code; received %d", exitcode)
4279
-	}
4280
-
4281
-	if expected := "Invalid volume specification"; !strings.Contains(out, expected) {
4282
-		c.Fatalf(`Expected %q in output; got: %s`, expected, out)
4283
-	}
4275
+	out, exitCode, _ := dockerCmdWithError("run", "-v", "foo:/test:shared", "busybox", "touch", "/test/somefile")
4276
+	c.Assert(exitCode, checker.Not(checker.Equals), 0)
4277
+	c.Assert(out, checker.Contains, "invalid mount config")
4284 4278
 }
4285 4279
 
4286 4280
 func (s *DockerSuite) TestRunNamedVolumeCopyImageData(c *check.C) {
... ...
@@ -73,16 +73,29 @@ func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostCon
73 73
 // validateVolumesAndBindSettings validates each of the volumes and bind settings
74 74
 // passed by the caller to ensure they are valid.
75 75
 func validateVolumesAndBindSettings(c *container.Config, hc *container.HostConfig) error {
76
+	if len(hc.Mounts) > 0 {
77
+		if len(hc.Binds) > 0 {
78
+			return conflictError(fmt.Errorf("must not specify both Binds and Mounts"))
79
+		}
80
+
81
+		if len(c.Volumes) > 0 {
82
+			return conflictError(fmt.Errorf("must not specify both Volumes and Mounts"))
83
+		}
84
+
85
+		if len(hc.VolumeDriver) > 0 {
86
+			return conflictError(fmt.Errorf("must not specify both VolumeDriver and Mounts"))
87
+		}
88
+	}
76 89
 
77 90
 	// Ensure all volumes and binds are valid.
78 91
 	for spec := range c.Volumes {
79
-		if _, err := volume.ParseMountSpec(spec, hc.VolumeDriver); err != nil {
80
-			return fmt.Errorf("Invalid volume spec %q: %v", spec, err)
92
+		if _, err := volume.ParseMountRaw(spec, hc.VolumeDriver); err != nil {
93
+			return fmt.Errorf("invalid volume spec %q: %v", spec, err)
81 94
 		}
82 95
 	}
83 96
 	for _, spec := range hc.Binds {
84
-		if _, err := volume.ParseMountSpec(spec, hc.VolumeDriver); err != nil {
85
-			return fmt.Errorf("Invalid bind mount spec %q: %v", spec, err)
97
+		if _, err := volume.ParseMountRaw(spec, hc.VolumeDriver); err != nil {
98
+			return fmt.Errorf("invalid bind mount spec %q: %v", spec, err)
86 99
 		}
87 100
 	}
88 101
 
... ...
@@ -2,6 +2,8 @@ package runconfig
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+
6
+	"github.com/docker/docker/errors"
5 7
 )
6 8
 
7 9
 var (
... ...
@@ -38,3 +40,7 @@ var (
38 38
 	// ErrConflictUTSHostname conflict between the hostname and the UTS mode
39 39
 	ErrConflictUTSHostname = fmt.Errorf("Conflicting options: hostname and the UTS mode")
40 40
 )
41
+
42
+func conflictError(err error) error {
43
+	return errors.NewRequestConflictError(err)
44
+}
41 45
new file mode 100644
... ...
@@ -0,0 +1,118 @@
0
+package volume
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"os"
6
+	"path/filepath"
7
+
8
+	"github.com/docker/docker/api/types/mount"
9
+)
10
+
11
+var errBindNotExist = errors.New("bind source path does not exist")
12
+
13
+type validateOpts struct {
14
+	skipBindSourceCheck   bool
15
+	skipAbsolutePathCheck bool
16
+}
17
+
18
+func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error {
19
+	opts := validateOpts{}
20
+	for _, o := range options {
21
+		o(&opts)
22
+	}
23
+
24
+	if len(mnt.Target) == 0 {
25
+		return &errMountConfig{mnt, errMissingField("Target")}
26
+	}
27
+
28
+	if err := validateNotRoot(mnt.Target); err != nil {
29
+		return &errMountConfig{mnt, err}
30
+	}
31
+
32
+	if !opts.skipAbsolutePathCheck {
33
+		if err := validateAbsolute(mnt.Target); err != nil {
34
+			return &errMountConfig{mnt, err}
35
+		}
36
+	}
37
+
38
+	switch mnt.Type {
39
+	case mount.TypeBind:
40
+		if len(mnt.Source) == 0 {
41
+			return &errMountConfig{mnt, errMissingField("Source")}
42
+		}
43
+		// Don't error out just because the propagation mode is not supported on the platform
44
+		if opts := mnt.BindOptions; opts != nil {
45
+			if len(opts.Propagation) > 0 && len(propagationModes) > 0 {
46
+				if _, ok := propagationModes[opts.Propagation]; !ok {
47
+					return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
48
+				}
49
+			}
50
+		}
51
+		if mnt.VolumeOptions != nil {
52
+			return &errMountConfig{mnt, errExtraField("VolumeOptions")}
53
+		}
54
+
55
+		if err := validateAbsolute(mnt.Source); err != nil {
56
+			return &errMountConfig{mnt, err}
57
+		}
58
+
59
+		// Do not allow binding to non-existent path
60
+		if !opts.skipBindSourceCheck {
61
+			fi, err := os.Stat(mnt.Source)
62
+			if err != nil {
63
+				if !os.IsNotExist(err) {
64
+					return &errMountConfig{mnt, err}
65
+				}
66
+				return &errMountConfig{mnt, errBindNotExist}
67
+			}
68
+			if err := validateStat(fi); err != nil {
69
+				return &errMountConfig{mnt, err}
70
+			}
71
+		}
72
+	case mount.TypeVolume:
73
+		if mnt.BindOptions != nil {
74
+			return &errMountConfig{mnt, errExtraField("BindOptions")}
75
+		}
76
+
77
+		if len(mnt.Source) == 0 && mnt.ReadOnly {
78
+			return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
79
+		}
80
+
81
+		if len(mnt.Source) != 0 {
82
+			if valid, err := IsVolumeNameValid(mnt.Source); !valid {
83
+				if err == nil {
84
+					err = errors.New("invalid volume name")
85
+				}
86
+				return &errMountConfig{mnt, err}
87
+			}
88
+		}
89
+	default:
90
+		return &errMountConfig{mnt, errors.New("mount type unknown")}
91
+	}
92
+	return nil
93
+}
94
+
95
+type errMountConfig struct {
96
+	mount *mount.Mount
97
+	err   error
98
+}
99
+
100
+func (e *errMountConfig) Error() string {
101
+	return fmt.Sprintf("invalid mount config for type %q: %v", e.mount.Type, e.err.Error())
102
+}
103
+
104
+func errExtraField(name string) error {
105
+	return fmt.Errorf("field %s must not be specified", name)
106
+}
107
+func errMissingField(name string) error {
108
+	return fmt.Errorf("field %s must not be empty", name)
109
+}
110
+
111
+func validateAbsolute(p string) error {
112
+	p = convertSlash(p)
113
+	if filepath.IsAbs(p) {
114
+		return nil
115
+	}
116
+	return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
117
+}
0 118
new file mode 100644
... ...
@@ -0,0 +1,43 @@
0
+package volume
1
+
2
+import (
3
+	"errors"
4
+	"io/ioutil"
5
+	"os"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/docker/docker/api/types/mount"
10
+)
11
+
12
+func TestValidateMount(t *testing.T) {
13
+	testDir, err := ioutil.TempDir("", "test-validate-mount")
14
+	if err != nil {
15
+		t.Fatal(err)
16
+	}
17
+	defer os.RemoveAll(testDir)
18
+
19
+	cases := []struct {
20
+		input    mount.Mount
21
+		expected error
22
+	}{
23
+		{mount.Mount{Type: mount.TypeVolume}, errMissingField("Target")},
24
+		{mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath, Source: "hello"}, nil},
25
+		{mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, nil},
26
+		{mount.Mount{Type: mount.TypeBind}, errMissingField("Target")},
27
+		{mount.Mount{Type: mount.TypeBind, Target: testDestinationPath}, errMissingField("Source")},
28
+		{mount.Mount{Type: mount.TypeBind, Target: testDestinationPath, Source: testSourcePath, VolumeOptions: &mount.VolumeOptions{}}, errExtraField("VolumeOptions")},
29
+		{mount.Mount{Type: mount.TypeBind, Source: testSourcePath, Target: testDestinationPath}, errBindNotExist},
30
+		{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, nil},
31
+		{mount.Mount{Type: "invalid", Target: testDestinationPath}, errors.New("mount type unknown")},
32
+	}
33
+	for i, x := range cases {
34
+		err := validateMountConfig(&x.input)
35
+		if err == nil && x.expected == nil {
36
+			continue
37
+		}
38
+		if (err == nil && x.expected != nil) || (x.expected == nil && err != nil) || !strings.Contains(err.Error(), x.expected.Error()) {
39
+			t.Fatalf("expected %q, got %q, case: %d", x.expected, err, i)
40
+		}
41
+	}
42
+}
0 43
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+// +build !windows
1
+
2
+package volume
3
+
4
+var (
5
+	testDestinationPath = "/foo"
6
+	testSourcePath      = "/foo"
7
+)
0 8
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+package volume
1
+
2
+var (
3
+	testDestinationPath = `c:\foo`
4
+	testSourcePath      = `c:\foo`
5
+)
... ...
@@ -3,6 +3,7 @@ package volume
3 3
 import (
4 4
 	"fmt"
5 5
 	"os"
6
+	"path/filepath"
6 7
 	"strings"
7 8
 	"syscall"
8 9
 
... ...
@@ -82,19 +83,19 @@ type ScopedVolume interface {
82 82
 // specifies which volume is to be used and where inside a container it should
83 83
 // be mounted.
84 84
 type MountPoint struct {
85
-	Source      string // Container host directory
86
-	Destination string // Inside the container
87
-	RW          bool   // True if writable
88
-	Name        string // Name set by user
89
-	Driver      string // Volume driver to use
90
-	Volume      Volume `json:"-"`
85
+	Source      string          // Container host directory
86
+	Destination string          // Inside the container
87
+	RW          bool            // True if writable
88
+	Name        string          // Name set by user
89
+	Driver      string          // Volume driver to use
90
+	Type        mounttypes.Type `json:",omitempty"` // Type of mount to use, see `Type<foo>` definitions
91
+	Volume      Volume          `json:"-"`
91 92
 
92 93
 	// Note Mode is not used on Windows
93
-	Mode string `json:"Relabel"` // Originally field was `Relabel`"
94
+	Mode string `json:"Relabel,omitempty"` // Originally field was `Relabel`"
94 95
 
95 96
 	// Note Propagation is not used on Windows
96
-	Propagation mounttypes.Propagation // Mount propagation string
97
-	Named       bool                   // specifies if the mountpoint was specified by name
97
+	Propagation mounttypes.Propagation `json:",omitempty"` // Mount propagation string
98 98
 
99 99
 	// Specifies if data should be copied from the container before the first mount
100 100
 	// Use a pointer here so we can tell if the user set this value explicitly
... ...
@@ -102,7 +103,8 @@ type MountPoint struct {
102 102
 	CopyData bool `json:"-"`
103 103
 	// ID is the opaque ID used to pass to the volume driver.
104 104
 	// This should be set by calls to `Mount` and unset by calls to `Unmount`
105
-	ID string
105
+	ID   string `json:",omitempty"`
106
+	Spec mounttypes.Mount
106 107
 }
107 108
 
108 109
 // Setup sets up a mount point by either mounting the volume if it is
... ...
@@ -117,12 +119,15 @@ func (m *MountPoint) Setup(mountLabel string, rootUID, rootGID int) (string, err
117 117
 	if len(m.Source) == 0 {
118 118
 		return "", fmt.Errorf("Unable to setup mount point, neither source nor volume defined")
119 119
 	}
120
-	// idtools.MkdirAllNewAs() produces an error if m.Source exists and is a file (not a directory)
121
-	// also, makes sure that if the directory is created, the correct remapped rootUID/rootGID will own it
122
-	if err := idtools.MkdirAllNewAs(m.Source, 0755, rootUID, rootGID); err != nil {
123
-		if perr, ok := err.(*os.PathError); ok {
124
-			if perr.Err != syscall.ENOTDIR {
125
-				return "", err
120
+	// system.MkdirAll() produces an error if m.Source exists and is a file (not a directory),
121
+	if m.Type == mounttypes.TypeBind {
122
+		// idtools.MkdirAllNewAs() produces an error if m.Source exists and is a file (not a directory)
123
+		// also, makes sure that if the directory is created, the correct remapped rootUID/rootGID will own it
124
+		if err := idtools.MkdirAllNewAs(m.Source, 0755, rootUID, rootGID); err != nil {
125
+			if perr, ok := err.(*os.PathError); ok {
126
+				if perr.Err != syscall.ENOTDIR {
127
+					return "", err
128
+				}
126 129
 			}
127 130
 		}
128 131
 	}
... ...
@@ -142,17 +147,6 @@ func (m *MountPoint) Path() string {
142 142
 	return m.Source
143 143
 }
144 144
 
145
-// Type returns the type of mount point
146
-func (m *MountPoint) Type() string {
147
-	if m.Name != "" {
148
-		return "volume"
149
-	}
150
-	if m.Source != "" {
151
-		return "bind"
152
-	}
153
-	return "ephemeral"
154
-}
155
-
156 145
 // ParseVolumesFrom ensures that the supplied volumes-from is valid.
157 146
 func ParseVolumesFrom(spec string) (string, string, error) {
158 147
 	if len(spec) == 0 {
... ...
@@ -183,10 +177,125 @@ func ParseVolumesFrom(spec string) (string, string, error) {
183 183
 	return id, mode, nil
184 184
 }
185 185
 
186
+// ParseMountRaw parses a raw volume spec (e.g. `-v /foo:/bar:shared`) into a
187
+// structured spec. Once the raw spec is parsed it relies on `ParseMountSpec` to
188
+// validate the spec and create a MountPoint
189
+func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
190
+	arr, err := splitRawSpec(convertSlash(raw))
191
+	if err != nil {
192
+		return nil, err
193
+	}
194
+
195
+	var spec mounttypes.Mount
196
+	var mode string
197
+	switch len(arr) {
198
+	case 1:
199
+		// Just a destination path in the container
200
+		spec.Target = arr[0]
201
+	case 2:
202
+		if ValidMountMode(arr[1]) {
203
+			// Destination + Mode is not a valid volume - volumes
204
+			// cannot include a mode. eg /foo:rw
205
+			return nil, errInvalidSpec(raw)
206
+		}
207
+		// Host Source Path or Name + Destination
208
+		spec.Source = arr[0]
209
+		spec.Target = arr[1]
210
+	case 3:
211
+		// HostSourcePath+DestinationPath+Mode
212
+		spec.Source = arr[0]
213
+		spec.Target = arr[1]
214
+		mode = arr[2]
215
+	default:
216
+		return nil, errInvalidSpec(raw)
217
+	}
218
+
219
+	if !ValidMountMode(mode) {
220
+		return nil, errInvalidMode(mode)
221
+	}
222
+
223
+	if filepath.IsAbs(spec.Source) {
224
+		spec.Type = mounttypes.TypeBind
225
+	} else {
226
+		spec.Type = mounttypes.TypeVolume
227
+	}
228
+
229
+	spec.ReadOnly = !ReadWrite(mode)
230
+
231
+	// cannot assume that if a volume driver is passed in that we should set it
232
+	if volumeDriver != "" && spec.Type == mounttypes.TypeVolume {
233
+		spec.VolumeOptions = &mounttypes.VolumeOptions{
234
+			DriverConfig: &mounttypes.Driver{Name: volumeDriver},
235
+		}
236
+	}
237
+
238
+	if copyData, isSet := getCopyMode(mode); isSet {
239
+		if spec.VolumeOptions == nil {
240
+			spec.VolumeOptions = &mounttypes.VolumeOptions{}
241
+		}
242
+		spec.VolumeOptions.NoCopy = !copyData
243
+	}
244
+	if HasPropagation(mode) {
245
+		spec.BindOptions = &mounttypes.BindOptions{
246
+			Propagation: GetPropagation(mode),
247
+		}
248
+	}
249
+
250
+	mp, err := ParseMountSpec(spec, platformRawValidationOpts...)
251
+	if mp != nil {
252
+		mp.Mode = mode
253
+	}
254
+	if err != nil {
255
+		err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
256
+	}
257
+	return mp, err
258
+}
259
+
260
+// ParseMountSpec reads a mount config, validates it, and configures a mountpoint from it.
261
+func ParseMountSpec(cfg mounttypes.Mount, options ...func(*validateOpts)) (*MountPoint, error) {
262
+	if err := validateMountConfig(&cfg, options...); err != nil {
263
+		return nil, err
264
+	}
265
+	mp := &MountPoint{
266
+		RW:          !cfg.ReadOnly,
267
+		Destination: clean(convertSlash(cfg.Target)),
268
+		Type:        cfg.Type,
269
+		Spec:        cfg,
270
+	}
271
+
272
+	switch cfg.Type {
273
+	case mounttypes.TypeVolume:
274
+		if cfg.Source == "" {
275
+			mp.Name = stringid.GenerateNonCryptoID()
276
+		} else {
277
+			mp.Name = cfg.Source
278
+		}
279
+		mp.CopyData = DefaultCopyMode
280
+
281
+		mp.Driver = DefaultDriverName
282
+		if cfg.VolumeOptions != nil {
283
+			if cfg.VolumeOptions.DriverConfig != nil {
284
+				mp.Driver = cfg.VolumeOptions.DriverConfig.Name
285
+			}
286
+			if cfg.VolumeOptions.NoCopy {
287
+				mp.CopyData = false
288
+			}
289
+		}
290
+	case mounttypes.TypeBind:
291
+		mp.Source = clean(convertSlash(cfg.Source))
292
+		if cfg.BindOptions != nil {
293
+			if len(cfg.BindOptions.Propagation) > 0 {
294
+				mp.Propagation = cfg.BindOptions.Propagation
295
+			}
296
+		}
297
+	}
298
+	return mp, nil
299
+}
300
+
186 301
 func errInvalidMode(mode string) error {
187 302
 	return fmt.Errorf("invalid mode: %v", mode)
188 303
 }
189 304
 
190 305
 func errInvalidSpec(spec string) error {
191
-	return fmt.Errorf("Invalid volume specification: '%s'", spec)
306
+	return fmt.Errorf("invalid volume specification: '%s'", spec)
192 307
 }
... ...
@@ -2,11 +2,6 @@ package volume
2 2
 
3 3
 import "strings"
4 4
 
5
-const (
6
-	// DefaultCopyMode is the copy mode used by default for normal/named volumes
7
-	DefaultCopyMode = true
8
-)
9
-
10 5
 // {<copy mode>=isEnabled}
11 6
 var copyModes = map[string]bool{
12 7
 	"nocopy": false,
13 8
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+// +build !windows
1
+
2
+package volume
3
+
4
+const (
5
+	// DefaultCopyMode is the copy mode used by default for normal/named volumes
6
+	DefaultCopyMode = true
7
+)
0 8
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+package volume
1
+
2
+const (
3
+	// DefaultCopyMode is the copy mode used by default for normal/named volumes
4
+	DefaultCopyMode = false
5
+)
... ...
@@ -10,9 +10,9 @@ import (
10 10
 
11 11
 // DefaultPropagationMode defines what propagation mode should be used by
12 12
 // default if user has not specified one explicitly.
13
-const DefaultPropagationMode mounttypes.Propagation = "rprivate"
14
-
15 13
 // propagation modes
14
+const DefaultPropagationMode = mounttypes.PropagationRPrivate
15
+
16 16
 var propagationModes = map[mounttypes.Propagation]bool{
17 17
 	mounttypes.PropagationPrivate:  true,
18 18
 	mounttypes.PropagationRPrivate: true,
... ...
@@ -7,7 +7,7 @@ import (
7 7
 	"testing"
8 8
 )
9 9
 
10
-func TestParseMountSpecPropagation(t *testing.T) {
10
+func TestParseMountRawPropagation(t *testing.T) {
11 11
 	var (
12 12
 		valid   []string
13 13
 		invalid map[string]string
... ...
@@ -34,31 +34,31 @@ func TestParseMountSpecPropagation(t *testing.T) {
34 34
 		"/hostPath:/containerPath:ro,Z,rprivate",
35 35
 	}
36 36
 	invalid = map[string]string{
37
-		"/path:/path:ro,rshared,rslave":   `invalid mode: ro,rshared,rslave`,
38
-		"/path:/path:ro,z,rshared,rslave": `invalid mode: ro,z,rshared,rslave`,
39
-		"/path:shared":                    "Invalid volume specification",
40
-		"/path:slave":                     "Invalid volume specification",
41
-		"/path:private":                   "Invalid volume specification",
42
-		"name:/absolute-path:shared":      "Invalid volume specification",
43
-		"name:/absolute-path:rshared":     "Invalid volume specification",
44
-		"name:/absolute-path:slave":       "Invalid volume specification",
45
-		"name:/absolute-path:rslave":      "Invalid volume specification",
46
-		"name:/absolute-path:private":     "Invalid volume specification",
47
-		"name:/absolute-path:rprivate":    "Invalid volume specification",
37
+		"/path:/path:ro,rshared,rslave":   `invalid mode`,
38
+		"/path:/path:ro,z,rshared,rslave": `invalid mode`,
39
+		"/path:shared":                    "invalid volume specification",
40
+		"/path:slave":                     "invalid volume specification",
41
+		"/path:private":                   "invalid volume specification",
42
+		"name:/absolute-path:shared":      "invalid volume specification",
43
+		"name:/absolute-path:rshared":     "invalid volume specification",
44
+		"name:/absolute-path:slave":       "invalid volume specification",
45
+		"name:/absolute-path:rslave":      "invalid volume specification",
46
+		"name:/absolute-path:private":     "invalid volume specification",
47
+		"name:/absolute-path:rprivate":    "invalid volume specification",
48 48
 	}
49 49
 
50 50
 	for _, path := range valid {
51
-		if _, err := ParseMountSpec(path, "local"); err != nil {
52
-			t.Fatalf("ParseMountSpec(`%q`) should succeed: error %q", path, err)
51
+		if _, err := ParseMountRaw(path, "local"); err != nil {
52
+			t.Fatalf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
53 53
 		}
54 54
 	}
55 55
 
56 56
 	for path, expectedError := range invalid {
57
-		if _, err := ParseMountSpec(path, "local"); err == nil {
58
-			t.Fatalf("ParseMountSpec(`%q`) should have failed validation. Err %v", path, err)
57
+		if _, err := ParseMountRaw(path, "local"); err == nil {
58
+			t.Fatalf("ParseMountRaw(`%q`) should have failed validation. Err %v", path, err)
59 59
 		} else {
60 60
 			if !strings.Contains(err.Error(), expectedError) {
61
-				t.Fatalf("ParseMountSpec(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
61
+				t.Fatalf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
62 62
 			}
63 63
 		}
64 64
 	}
... ...
@@ -9,7 +9,7 @@ import mounttypes "github.com/docker/docker/api/types/mount"
9 9
 const DefaultPropagationMode mounttypes.Propagation = ""
10 10
 
11 11
 // propagation modes not supported on this platform.
12
-var propagationModes = map[string]bool{}
12
+var propagationModes = map[mounttypes.Propagation]bool{}
13 13
 
14 14
 // GetPropagation is not supported. Return empty string.
15 15
 func GetPropagation(mode string) mounttypes.Propagation {
... ...
@@ -1,12 +1,16 @@
1 1
 package volume
2 2
 
3 3
 import (
4
+	"io/ioutil"
5
+	"os"
4 6
 	"runtime"
5 7
 	"strings"
6 8
 	"testing"
9
+
10
+	"github.com/docker/docker/api/types/mount"
7 11
 )
8 12
 
9
-func TestParseMountSpec(t *testing.T) {
13
+func TestParseMountRaw(t *testing.T) {
10 14
 	var (
11 15
 		valid   []string
12 16
 		invalid map[string]string
... ...
@@ -36,25 +40,25 @@ func TestParseMountSpec(t *testing.T) {
36 36
 			`c:\Program Files (x86)`, // With capitals and brackets
37 37
 		}
38 38
 		invalid = map[string]string{
39
-			``:                                 "Invalid volume specification: ",
40
-			`.`:                                "Invalid volume specification: ",
41
-			`..\`:                              "Invalid volume specification: ",
42
-			`c:\:..\`:                          "Invalid volume specification: ",
43
-			`c:\:d:\:xyzzy`:                    "Invalid volume specification: ",
44
-			`c:`:                               "cannot be c:",
45
-			`c:\`:                              `cannot be c:\`,
46
-			`c:\notexist:d:`:                   `The system cannot find the file specified`,
47
-			`c:\windows\system32\ntdll.dll:d:`: `Source 'c:\windows\system32\ntdll.dll' is not a directory`,
48
-			`name<:d:`:                         `Invalid volume specification`,
49
-			`name>:d:`:                         `Invalid volume specification`,
50
-			`name::d:`:                         `Invalid volume specification`,
51
-			`name":d:`:                         `Invalid volume specification`,
52
-			`name\:d:`:                         `Invalid volume specification`,
53
-			`name*:d:`:                         `Invalid volume specification`,
54
-			`name|:d:`:                         `Invalid volume specification`,
55
-			`name?:d:`:                         `Invalid volume specification`,
56
-			`name/:d:`:                         `Invalid volume specification`,
57
-			`d:\pathandmode:rw`:                `Invalid volume specification`,
39
+			``:                                 "invalid volume specification: ",
40
+			`.`:                                "invalid volume specification: ",
41
+			`..\`:                              "invalid volume specification: ",
42
+			`c:\:..\`:                          "invalid volume specification: ",
43
+			`c:\:d:\:xyzzy`:                    "invalid volume specification: ",
44
+			`c:`:                               "cannot be `c:`",
45
+			`c:\`:                              "cannot be `c:`",
46
+			`c:\notexist:d:`:                   `source path does not exist`,
47
+			`c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`,
48
+			`name<:d:`:                         `invalid volume specification`,
49
+			`name>:d:`:                         `invalid volume specification`,
50
+			`name::d:`:                         `invalid volume specification`,
51
+			`name":d:`:                         `invalid volume specification`,
52
+			`name\:d:`:                         `invalid volume specification`,
53
+			`name*:d:`:                         `invalid volume specification`,
54
+			`name|:d:`:                         `invalid volume specification`,
55
+			`name?:d:`:                         `invalid volume specification`,
56
+			`name/:d:`:                         `invalid volume specification`,
57
+			`d:\pathandmode:rw`:                `invalid volume specification`,
58 58
 			`con:d:`:                           `cannot be a reserved word for Windows filenames`,
59 59
 			`PRN:d:`:                           `cannot be a reserved word for Windows filenames`,
60 60
 			`aUx:d:`:                           `cannot be a reserved word for Windows filenames`,
... ...
@@ -93,50 +97,50 @@ func TestParseMountSpec(t *testing.T) {
93 93
 			"/rw:/ro",
94 94
 		}
95 95
 		invalid = map[string]string{
96
-			"":                "Invalid volume specification",
97
-			"./":              "Invalid volume destination",
98
-			"../":             "Invalid volume destination",
99
-			"/:../":           "Invalid volume destination",
100
-			"/:path":          "Invalid volume destination",
101
-			":":               "Invalid volume specification",
102
-			"/tmp:":           "Invalid volume destination",
103
-			":test":           "Invalid volume specification",
104
-			":/test":          "Invalid volume specification",
105
-			"tmp:":            "Invalid volume destination",
106
-			":test:":          "Invalid volume specification",
107
-			"::":              "Invalid volume specification",
108
-			":::":             "Invalid volume specification",
109
-			"/tmp:::":         "Invalid volume specification",
110
-			":/tmp::":         "Invalid volume specification",
111
-			"/path:rw":        "Invalid volume specification",
112
-			"/path:ro":        "Invalid volume specification",
113
-			"/rw:rw":          "Invalid volume specification",
114
-			"path:ro":         "Invalid volume specification",
115
-			"/path:/path:sw":  `invalid mode: sw`,
116
-			"/path:/path:rwz": `invalid mode: rwz`,
96
+			"":                "invalid volume specification",
97
+			"./":              "mount path must be absolute",
98
+			"../":             "mount path must be absolute",
99
+			"/:../":           "mount path must be absolute",
100
+			"/:path":          "mount path must be absolute",
101
+			":":               "invalid volume specification",
102
+			"/tmp:":           "invalid volume specification",
103
+			":test":           "invalid volume specification",
104
+			":/test":          "invalid volume specification",
105
+			"tmp:":            "invalid volume specification",
106
+			":test:":          "invalid volume specification",
107
+			"::":              "invalid volume specification",
108
+			":::":             "invalid volume specification",
109
+			"/tmp:::":         "invalid volume specification",
110
+			":/tmp::":         "invalid volume specification",
111
+			"/path:rw":        "invalid volume specification",
112
+			"/path:ro":        "invalid volume specification",
113
+			"/rw:rw":          "invalid volume specification",
114
+			"path:ro":         "invalid volume specification",
115
+			"/path:/path:sw":  `invalid mode`,
116
+			"/path:/path:rwz": `invalid mode`,
117 117
 		}
118 118
 	}
119 119
 
120 120
 	for _, path := range valid {
121
-		if _, err := ParseMountSpec(path, "local"); err != nil {
122
-			t.Fatalf("ParseMountSpec(`%q`) should succeed: error %q", path, err)
121
+		if _, err := ParseMountRaw(path, "local"); err != nil {
122
+			t.Fatalf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
123 123
 		}
124 124
 	}
125 125
 
126 126
 	for path, expectedError := range invalid {
127
-		if _, err := ParseMountSpec(path, "local"); err == nil {
128
-			t.Fatalf("ParseMountSpec(`%q`) should have failed validation. Err %v", path, err)
127
+		if mp, err := ParseMountRaw(path, "local"); err == nil {
128
+			t.Fatalf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
129 129
 		} else {
130 130
 			if !strings.Contains(err.Error(), expectedError) {
131
-				t.Fatalf("ParseMountSpec(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
131
+				t.Fatalf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
132 132
 			}
133 133
 		}
134 134
 	}
135 135
 }
136 136
 
137
-// testParseMountSpec is a structure used by TestParseMountSpecSplit for
138
-// specifying test cases for the ParseMountSpec() function.
139
-type testParseMountSpec struct {
137
+// testParseMountRaw is a structure used by TestParseMountRawSplit for
138
+// specifying test cases for the ParseMountRaw() function.
139
+type testParseMountRaw struct {
140 140
 	bind      string
141 141
 	driver    string
142 142
 	expDest   string
... ...
@@ -147,10 +151,10 @@ type testParseMountSpec struct {
147 147
 	fail      bool
148 148
 }
149 149
 
150
-func TestParseMountSpecSplit(t *testing.T) {
151
-	var cases []testParseMountSpec
150
+func TestParseMountRawSplit(t *testing.T) {
151
+	var cases []testParseMountRaw
152 152
 	if runtime.GOOS == "windows" {
153
-		cases = []testParseMountSpec{
153
+		cases = []testParseMountRaw{
154 154
 			{`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false},
155 155
 			{`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false},
156 156
 			// TODO Windows post TP5 - Add readonly support {`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false},
... ...
@@ -159,25 +163,26 @@ func TestParseMountSpecSplit(t *testing.T) {
159 159
 			{`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false},
160 160
 			{`name:d:`, "local", `d:`, ``, `name`, "local", true, false},
161 161
 			// TODO Windows post TP5 - Add readonly support {`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false},
162
-			{`name:c:`, "", ``, ``, ``, "", true, true},
163
-			{`driver/name:c:`, "", ``, ``, ``, "", true, true},
162
+			{`name:c:`, "", ``, ``, ``, DefaultDriverName, true, true},
163
+			{`driver/name:c:`, "", ``, ``, ``, DefaultDriverName, true, true},
164 164
 		}
165 165
 	} else {
166
-		cases = []testParseMountSpec{
166
+		cases = []testParseMountRaw{
167 167
 			{"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false},
168 168
 			{"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false},
169 169
 			{"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false},
170 170
 			{"/tmp:/tmp4:foo", "", "", "", "", "", false, true},
171
-			{"name:/named1", "", "/named1", "", "name", "", true, false},
171
+			{"name:/named1", "", "/named1", "", "name", DefaultDriverName, true, false},
172 172
 			{"name:/named2", "external", "/named2", "", "name", "external", true, false},
173 173
 			{"name:/named3:ro", "local", "/named3", "", "name", "local", false, false},
174
-			{"local/name:/tmp:rw", "", "/tmp", "", "local/name", "", true, false},
174
+			{"local/name:/tmp:rw", "", "/tmp", "", "local/name", DefaultDriverName, true, false},
175 175
 			{"/tmp:tmp", "", "", "", "", "", true, true},
176 176
 		}
177 177
 	}
178 178
 
179
-	for _, c := range cases {
180
-		m, err := ParseMountSpec(c.bind, c.driver)
179
+	for i, c := range cases {
180
+		t.Logf("case %d", i)
181
+		m, err := ParseMountRaw(c.bind, c.driver)
181 182
 		if c.fail {
182 183
 			if err == nil {
183 184
 				t.Fatalf("Expected error, was nil, for spec %s\n", c.bind)
... ...
@@ -186,28 +191,79 @@ func TestParseMountSpecSplit(t *testing.T) {
186 186
 		}
187 187
 
188 188
 		if m == nil || err != nil {
189
-			t.Fatalf("ParseMountSpec failed for spec %s driver %s error %v\n", c.bind, c.driver, err.Error())
189
+			t.Fatalf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err.Error())
190 190
 			continue
191 191
 		}
192 192
 
193 193
 		if m.Destination != c.expDest {
194
-			t.Fatalf("Expected destination %s, was %s, for spec %s\n", c.expDest, m.Destination, c.bind)
194
+			t.Fatalf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
195 195
 		}
196 196
 
197 197
 		if m.Source != c.expSource {
198
-			t.Fatalf("Expected source %s, was %s, for spec %s\n", c.expSource, m.Source, c.bind)
198
+			t.Fatalf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind)
199 199
 		}
200 200
 
201 201
 		if m.Name != c.expName {
202
-			t.Fatalf("Expected name %s, was %s for spec %s\n", c.expName, m.Name, c.bind)
202
+			t.Fatalf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind)
203 203
 		}
204 204
 
205
-		if m.Driver != c.expDriver {
206
-			t.Fatalf("Expected driver %s, was %s, for spec %s\n", c.expDriver, m.Driver, c.bind)
205
+		if (m.Driver != c.expDriver) || (m.Driver == DefaultDriverName && c.expDriver == "") {
206
+			t.Fatalf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind)
207 207
 		}
208 208
 
209 209
 		if m.RW != c.expRW {
210
-			t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind)
210
+			t.Fatalf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind)
211
+		}
212
+	}
213
+}
214
+
215
+func TestParseMountSpec(t *testing.T) {
216
+	type c struct {
217
+		input    mount.Mount
218
+		expected MountPoint
219
+	}
220
+	testDir, err := ioutil.TempDir("", "test-mount-config")
221
+	if err != nil {
222
+		t.Fatal(err)
223
+	}
224
+	defer os.RemoveAll(testDir)
225
+
226
+	cases := []c{
227
+		{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath}},
228
+		{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true}},
229
+		{mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath}},
230
+		{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath + string(os.PathSeparator), ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath}},
231
+		{mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, Driver: DefaultDriverName, CopyData: DefaultCopyMode}},
232
+		{mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath + string(os.PathSeparator)}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, Driver: DefaultDriverName, CopyData: DefaultCopyMode}},
233
+	}
234
+
235
+	for i, c := range cases {
236
+		t.Logf("case %d", i)
237
+		mp, err := ParseMountSpec(c.input)
238
+		if err != nil {
239
+			t.Fatal(err)
240
+		}
241
+
242
+		if c.expected.Type != mp.Type {
243
+			t.Fatalf("Expected mount types to match. Expected: '%s', Actual: '%s'", c.expected.Type, mp.Type)
244
+		}
245
+		if c.expected.Destination != mp.Destination {
246
+			t.Fatalf("Expected mount destination to match. Expected: '%s', Actual: '%s'", c.expected.Destination, mp.Destination)
247
+		}
248
+		if c.expected.Source != mp.Source {
249
+			t.Fatalf("Expected mount source to match. Expected: '%s', Actual: '%s'", c.expected.Source, mp.Source)
250
+		}
251
+		if c.expected.RW != mp.RW {
252
+			t.Fatalf("Expected mount writable to match. Expected: '%v', Actual: '%s'", c.expected.RW, mp.RW)
253
+		}
254
+		if c.expected.Propagation != mp.Propagation {
255
+			t.Fatalf("Expected mount propagation to match. Expected: '%v', Actual: '%s'", c.expected.Propagation, mp.Propagation)
256
+		}
257
+		if c.expected.Driver != mp.Driver {
258
+			t.Fatalf("Expected mount driver to match. Expected: '%v', Actual: '%s'", c.expected.Driver, mp.Driver)
259
+		}
260
+		if c.expected.CopyData != mp.CopyData {
261
+			t.Fatalf("Expected mount copy data to match. Expected: '%v', Actual: '%v'", c.expected.CopyData, mp.CopyData)
211 262
 		}
212 263
 	}
213 264
 }
... ...
@@ -4,12 +4,20 @@ package volume
4 4
 
5 5
 import (
6 6
 	"fmt"
7
+	"os"
7 8
 	"path/filepath"
8 9
 	"strings"
9 10
 
10 11
 	mounttypes "github.com/docker/docker/api/types/mount"
11 12
 )
12 13
 
14
+var platformRawValidationOpts = []func(o *validateOpts){
15
+	// need to make sure to not error out if the bind source does not exist on unix
16
+	// this is supported for historical reasons, the path will be automatically
17
+	// created later.
18
+	func(o *validateOpts) { o.skipBindSourceCheck = true },
19
+}
20
+
13 21
 // read-write modes
14 22
 var rwModes = map[string]bool{
15 23
 	"rw": true,
... ...
@@ -38,103 +46,6 @@ func (m *MountPoint) HasResource(absolutePath string) bool {
38 38
 	return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
39 39
 }
40 40
 
41
-// ParseMountSpec validates the configuration of mount information is valid.
42
-func ParseMountSpec(spec, volumeDriver string) (*MountPoint, error) {
43
-	spec = filepath.ToSlash(spec)
44
-
45
-	mp := &MountPoint{
46
-		RW:          true,
47
-		Propagation: DefaultPropagationMode,
48
-	}
49
-	if strings.Count(spec, ":") > 2 {
50
-		return nil, errInvalidSpec(spec)
51
-	}
52
-
53
-	arr := strings.SplitN(spec, ":", 3)
54
-	if arr[0] == "" {
55
-		return nil, errInvalidSpec(spec)
56
-	}
57
-
58
-	switch len(arr) {
59
-	case 1:
60
-		// Just a destination path in the container
61
-		mp.Destination = filepath.Clean(arr[0])
62
-	case 2:
63
-		if isValid := ValidMountMode(arr[1]); isValid {
64
-			// Destination + Mode is not a valid volume - volumes
65
-			// cannot include a mode. eg /foo:rw
66
-			return nil, errInvalidSpec(spec)
67
-		}
68
-		// Host Source Path or Name + Destination
69
-		mp.Source = arr[0]
70
-		mp.Destination = arr[1]
71
-	case 3:
72
-		// HostSourcePath+DestinationPath+Mode
73
-		mp.Source = arr[0]
74
-		mp.Destination = arr[1]
75
-		mp.Mode = arr[2] // Mode field is used by SELinux to decide whether to apply label
76
-		if !ValidMountMode(mp.Mode) {
77
-			return nil, errInvalidMode(mp.Mode)
78
-		}
79
-		mp.RW = ReadWrite(mp.Mode)
80
-		mp.Propagation = GetPropagation(mp.Mode)
81
-	default:
82
-		return nil, errInvalidSpec(spec)
83
-	}
84
-
85
-	//validate the volumes destination path
86
-	mp.Destination = filepath.Clean(mp.Destination)
87
-	if !filepath.IsAbs(mp.Destination) {
88
-		return nil, fmt.Errorf("Invalid volume destination path: '%s' mount path must be absolute.", mp.Destination)
89
-	}
90
-
91
-	// Destination cannot be "/"
92
-	if mp.Destination == "/" {
93
-		return nil, fmt.Errorf("Invalid specification: destination can't be '/' in '%s'", spec)
94
-	}
95
-
96
-	name, source := ParseVolumeSource(mp.Source)
97
-	if len(source) == 0 {
98
-		mp.Source = "" // Clear it out as we previously assumed it was not a name
99
-		mp.Driver = volumeDriver
100
-		// Named volumes can't have propagation properties specified.
101
-		// Their defaults will be decided by docker. This is just a
102
-		// safeguard. Don't want to get into situations where named
103
-		// volumes were mounted as '[r]shared' inside container and
104
-		// container does further mounts under that volume and these
105
-		// mounts become visible on  host and later original volume
106
-		// cleanup becomes an issue if container does not unmount
107
-		// submounts explicitly.
108
-		if HasPropagation(mp.Mode) {
109
-			return nil, errInvalidSpec(spec)
110
-		}
111
-	} else {
112
-		mp.Source = filepath.Clean(source)
113
-	}
114
-
115
-	copyData, isSet := getCopyMode(mp.Mode)
116
-	// do not allow copy modes on binds
117
-	if len(name) == 0 && isSet {
118
-		return nil, errInvalidMode(mp.Mode)
119
-	}
120
-
121
-	mp.CopyData = copyData
122
-	mp.Name = name
123
-
124
-	return mp, nil
125
-}
126
-
127
-// ParseVolumeSource parses the origin sources that's mounted into the container.
128
-// It returns a name and a source. It looks to see if the spec passed in
129
-// is an absolute file. If it is, it assumes the spec is a source. If not,
130
-// it assumes the spec is a name.
131
-func ParseVolumeSource(spec string) (string, string) {
132
-	if !filepath.IsAbs(spec) {
133
-		return spec, ""
134
-	}
135
-	return "", spec
136
-}
137
-
138 41
 // IsVolumeNameValid checks a volume name in a platform specific manner.
139 42
 func IsVolumeNameValid(name string) (bool, error) {
140 43
 	return true, nil
... ...
@@ -143,6 +54,10 @@ func IsVolumeNameValid(name string) (bool, error) {
143 143
 // ValidMountMode will make sure the mount mode is valid.
144 144
 // returns if it's a valid mount mode or not.
145 145
 func ValidMountMode(mode string) bool {
146
+	if mode == "" {
147
+		return true
148
+	}
149
+
146 150
 	rwModeCount := 0
147 151
 	labelModeCount := 0
148 152
 	propagationModeCount := 0
... ...
@@ -183,6 +98,41 @@ func ReadWrite(mode string) bool {
183 183
 			return false
184 184
 		}
185 185
 	}
186
-
187 186
 	return true
188 187
 }
188
+
189
+func validateNotRoot(p string) error {
190
+	p = filepath.Clean(convertSlash(p))
191
+	if p == "/" {
192
+		return fmt.Errorf("invalid specification: destination can't be '/'")
193
+	}
194
+	return nil
195
+}
196
+
197
+func validateCopyMode(mode bool) error {
198
+	return nil
199
+}
200
+
201
+func convertSlash(p string) string {
202
+	return filepath.ToSlash(p)
203
+}
204
+
205
+func splitRawSpec(raw string) ([]string, error) {
206
+	if strings.Count(raw, ":") > 2 {
207
+		return nil, errInvalidSpec(raw)
208
+	}
209
+
210
+	arr := strings.SplitN(raw, ":", 3)
211
+	if arr[0] == "" {
212
+		return nil, errInvalidSpec(raw)
213
+	}
214
+	return arr, nil
215
+}
216
+
217
+func clean(p string) string {
218
+	return filepath.Clean(p)
219
+}
220
+
221
+func validateStat(fi os.FileInfo) error {
222
+	return nil
223
+}
... ...
@@ -7,7 +7,6 @@ import (
7 7
 	"regexp"
8 8
 	"strings"
9 9
 
10
-	"github.com/Sirupsen/logrus"
11 10
 	"github.com/docker/docker/pkg/system"
12 11
 )
13 12
 
... ...
@@ -21,6 +20,15 @@ var roModes = map[string]bool{
21 21
 	"ro": true,
22 22
 }
23 23
 
24
+var platformRawValidationOpts = []func(*validateOpts){
25
+	// filepath.IsAbs is weird on Windows:
26
+	//	`c:` is not considered an absolute path
27
+	//	`c:\` is considered an absolute path
28
+	// In any case, the regex matching below ensures absolute paths
29
+	// TODO: consider this a bug with filepath.IsAbs (?)
30
+	func(o *validateOpts) { o.skipAbsolutePathCheck = true },
31
+}
32
+
24 33
 const (
25 34
 	// Spec should be in the format [source:]destination[:mode]
26 35
 	//
... ...
@@ -94,109 +102,54 @@ func (m *MountPoint) BackwardsCompatible() bool {
94 94
 	return false
95 95
 }
96 96
 
97
-// ParseMountSpec validates the configuration of mount information is valid.
98
-func ParseMountSpec(spec string, volumeDriver string) (*MountPoint, error) {
99
-	var specExp = regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`)
100
-
101
-	// Ensure in platform semantics for matching. The CLI will send in Unix semantics.
102
-	match := specExp.FindStringSubmatch(filepath.FromSlash(strings.ToLower(spec)))
97
+func splitRawSpec(raw string) ([]string, error) {
98
+	specExp := regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`)
99
+	match := specExp.FindStringSubmatch(strings.ToLower(raw))
103 100
 
104 101
 	// Must have something back
105 102
 	if len(match) == 0 {
106
-		return nil, errInvalidSpec(spec)
103
+		return nil, errInvalidSpec(raw)
107 104
 	}
108 105
 
109
-	// Pull out the sub expressions from the named capture groups
106
+	var split []string
110 107
 	matchgroups := make(map[string]string)
108
+	// Pull out the sub expressions from the named capture groups
111 109
 	for i, name := range specExp.SubexpNames() {
112 110
 		matchgroups[name] = strings.ToLower(match[i])
113 111
 	}
114
-
115
-	mp := &MountPoint{
116
-		Source:      matchgroups["source"],
117
-		Destination: matchgroups["destination"],
118
-		RW:          true,
119
-	}
120
-	if strings.ToLower(matchgroups["mode"]) == "ro" {
121
-		mp.RW = false
122
-	}
123
-
124
-	// Volumes cannot include an explicitly supplied mode eg c:\path:rw
125
-	if mp.Source == "" && mp.Destination != "" && matchgroups["mode"] != "" {
126
-		return nil, errInvalidSpec(spec)
127
-	}
128
-
129
-	// Note: No need to check if destination is absolute as it must be by
130
-	// definition of matching the regex.
131
-
132
-	if filepath.VolumeName(mp.Destination) == mp.Destination {
133
-		// Ensure the destination path, if a drive letter, is not the c drive
134
-		if strings.ToLower(mp.Destination) == "c:" {
135
-			return nil, fmt.Errorf("Destination drive letter in '%s' cannot be c:", spec)
136
-		}
137
-	} else {
138
-		// So we know the destination is a path, not drive letter. Clean it up.
139
-		mp.Destination = filepath.Clean(mp.Destination)
140
-		// Ensure the destination path, if a path, is not the c root directory
141
-		if strings.ToLower(mp.Destination) == `c:\` {
142
-			return nil, fmt.Errorf(`Destination path in '%s' cannot be c:\`, spec)
112
+	if source, exists := matchgroups["source"]; exists {
113
+		if source != "" {
114
+			split = append(split, source)
143 115
 		}
144 116
 	}
145
-
146
-	// See if the source is a name instead of a host directory
147
-	if len(mp.Source) > 0 {
148
-		validName, err := IsVolumeNameValid(mp.Source)
149
-		if err != nil {
150
-			return nil, err
151
-		}
152
-		if validName {
153
-			// OK, so the source is a name.
154
-			mp.Name = mp.Source
155
-			mp.Source = ""
156
-
157
-			// Set the driver accordingly
158
-			mp.Driver = volumeDriver
159
-			if len(mp.Driver) == 0 {
160
-				mp.Driver = DefaultDriverName
161
-			}
162
-		} else {
163
-			// OK, so the source must be a host directory. Make sure it's clean.
164
-			mp.Source = filepath.Clean(mp.Source)
117
+	if destination, exists := matchgroups["destination"]; exists {
118
+		if destination != "" {
119
+			split = append(split, destination)
165 120
 		}
166 121
 	}
167
-
168
-	// Ensure the host path source, if supplied, exists and is a directory
169
-	if len(mp.Source) > 0 {
170
-		var fi os.FileInfo
171
-		var err error
172
-		if fi, err = os.Stat(mp.Source); err != nil {
173
-			return nil, fmt.Errorf("Source directory '%s' could not be found: %s", mp.Source, err)
174
-		}
175
-		if !fi.IsDir() {
176
-			return nil, fmt.Errorf("Source '%s' is not a directory", mp.Source)
122
+	if mode, exists := matchgroups["mode"]; exists {
123
+		if mode != "" {
124
+			split = append(split, mode)
177 125
 		}
178 126
 	}
179
-
180 127
 	// Fix #26329. If the destination appears to be a file, and the source is null,
181 128
 	// it may be because we've fallen through the possible naming regex and hit a
182 129
 	// situation where the user intention was to map a file into a container through
183 130
 	// a local volume, but this is not supported by the platform.
184
-	if len(mp.Source) == 0 && len(mp.Destination) > 0 {
185
-		var fi os.FileInfo
186
-		var err error
187
-		if fi, err = os.Stat(mp.Destination); err == nil {
188
-			validName, err := IsVolumeNameValid(mp.Destination)
189
-			if err != nil {
190
-				return nil, err
191
-			}
192
-			if !validName && !fi.IsDir() {
193
-				return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", mp.Destination)
131
+	if matchgroups["source"] == "" && matchgroups["destination"] != "" {
132
+		validName, err := IsVolumeNameValid(matchgroups["destination"])
133
+		if err != nil {
134
+			return nil, err
135
+		}
136
+		if !validName {
137
+			if fi, err := os.Stat(matchgroups["destination"]); err == nil {
138
+				if !fi.IsDir() {
139
+					return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
140
+				}
194 141
 			}
195 142
 		}
196 143
 	}
197
-
198
-	logrus.Debugf("MP: Source '%s', Dest '%s', RW %t, Name '%s', Driver '%s'", mp.Source, mp.Destination, mp.RW, mp.Name, mp.Driver)
199
-	return mp, nil
144
+	return split, nil
200 145
 }
201 146
 
202 147
 // IsVolumeNameValid checks a volume name in a platform specific manner.
... ...
@@ -207,7 +160,7 @@ func IsVolumeNameValid(name string) (bool, error) {
207 207
 	}
208 208
 	nameExp = regexp.MustCompile(`^` + RXReservedNames + `$`)
209 209
 	if nameExp.MatchString(name) {
210
-		return false, fmt.Errorf("Volume name %q cannot be a reserved word for Windows filenames", name)
210
+		return false, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
211 211
 	}
212 212
 	return true, nil
213 213
 }
... ...
@@ -215,10 +168,46 @@ func IsVolumeNameValid(name string) (bool, error) {
215 215
 // ValidMountMode will make sure the mount mode is valid.
216 216
 // returns if it's a valid mount mode or not.
217 217
 func ValidMountMode(mode string) bool {
218
+	if mode == "" {
219
+		return true
220
+	}
218 221
 	return roModes[strings.ToLower(mode)] || rwModes[strings.ToLower(mode)]
219 222
 }
220 223
 
221 224
 // ReadWrite tells you if a mode string is a valid read-write mode or not.
222 225
 func ReadWrite(mode string) bool {
223
-	return rwModes[strings.ToLower(mode)]
226
+	return rwModes[strings.ToLower(mode)] || mode == ""
227
+}
228
+
229
+func validateNotRoot(p string) error {
230
+	p = strings.ToLower(convertSlash(p))
231
+	if p == "c:" || p == `c:\` {
232
+		return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
233
+	}
234
+	return nil
235
+}
236
+
237
+func validateCopyMode(mode bool) error {
238
+	if mode {
239
+		return fmt.Errorf("Windows does not support copying image path content")
240
+	}
241
+	return nil
242
+}
243
+
244
+func convertSlash(p string) string {
245
+	return filepath.FromSlash(p)
246
+}
247
+
248
+func clean(p string) string {
249
+	if match, _ := regexp.MatchString("^[a-z]:$", p); match {
250
+		return p
251
+	}
252
+	return filepath.Clean(p)
253
+}
254
+
255
+func validateStat(fi os.FileInfo) error {
256
+	if !fi.IsDir() {
257
+		return fmt.Errorf("source path must be a directory")
258
+	}
259
+	return nil
224 260
 }