`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>
| ... | ... |
@@ -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 |
+} |
| ... | ... |
@@ -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 |
} |
| ... | ... |
@@ -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 |
} |