Current insider builds of Windows have support for mounting individual
named pipe servers from the host to the guest. This allows, for example,
exposing the docker engine's named pipe to a container.
This change allows the user to request such a mount via the normal bind
mount syntax in the CLI:
docker run -v \\.\pipe\docker_engine:\\.\pipe\docker_engine <args>
Signed-off-by: John Starks <jostarks@microsoft.com>
| ... | ... |
@@ -15,6 +15,8 @@ const ( |
| 15 | 15 |
TypeVolume Type = "volume" |
| 16 | 16 |
// TypeTmpfs is the type for mounting tmpfs |
| 17 | 17 |
TypeTmpfs Type = "tmpfs" |
| 18 |
+ // TypeNamedPipe is the type for mounting Windows named pipes |
|
| 19 |
+ TypeNamedPipe Type = "npipe" |
|
| 18 | 20 |
) |
| 19 | 21 |
|
| 20 | 22 |
// Mount represents a mount (volume). |
| 21 | 23 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,71 @@ |
| 0 |
+// +build windows |
|
| 1 |
+ |
|
| 2 |
+package main |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "math/rand" |
|
| 8 |
+ "net/http" |
|
| 9 |
+ "strings" |
|
| 10 |
+ |
|
| 11 |
+ winio "github.com/Microsoft/go-winio" |
|
| 12 |
+ "github.com/docker/docker/integration-cli/checker" |
|
| 13 |
+ "github.com/docker/docker/integration-cli/request" |
|
| 14 |
+ "github.com/go-check/check" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+func (s *DockerSuite) TestContainersAPICreateMountsBindNamedPipe(c *check.C) {
|
|
| 18 |
+ testRequires(c, SameHostDaemon, DaemonIsWindowsAtLeastBuild(16210)) // Named pipe support was added in RS3 |
|
| 19 |
+ |
|
| 20 |
+ // Create a host pipe to map into the container |
|
| 21 |
+ hostPipeName := fmt.Sprintf(`\\.\pipe\docker-cli-test-pipe-%x`, rand.Uint64()) |
|
| 22 |
+ pc := &winio.PipeConfig{
|
|
| 23 |
+ SecurityDescriptor: "D:P(A;;GA;;;AU)", // Allow all users access to the pipe |
|
| 24 |
+ } |
|
| 25 |
+ l, err := winio.ListenPipe(hostPipeName, pc) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ c.Fatal(err) |
|
| 28 |
+ } |
|
| 29 |
+ defer l.Close() |
|
| 30 |
+ |
|
| 31 |
+ // Asynchronously read data that the container writes to the mapped pipe. |
|
| 32 |
+ var b []byte |
|
| 33 |
+ ch := make(chan error) |
|
| 34 |
+ go func() {
|
|
| 35 |
+ conn, err := l.Accept() |
|
| 36 |
+ if err == nil {
|
|
| 37 |
+ b, err = ioutil.ReadAll(conn) |
|
| 38 |
+ conn.Close() |
|
| 39 |
+ } |
|
| 40 |
+ ch <- err |
|
| 41 |
+ }() |
|
| 42 |
+ |
|
| 43 |
+ containerPipeName := `\\.\pipe\docker-cli-test-pipe` |
|
| 44 |
+ text := "hello from a pipe" |
|
| 45 |
+ cmd := fmt.Sprintf("echo %s > %s", text, containerPipeName)
|
|
| 46 |
+ |
|
| 47 |
+ name := "test-bind-npipe" |
|
| 48 |
+ data := map[string]interface{}{
|
|
| 49 |
+ "Image": testEnv.MinimalBaseImage(), |
|
| 50 |
+ "Cmd": []string{"cmd", "/c", cmd},
|
|
| 51 |
+ "HostConfig": map[string]interface{}{"Mounts": []map[string]interface{}{{"Type": "npipe", "Source": hostPipeName, "Target": containerPipeName}}},
|
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ status, resp, err := request.SockRequest("POST", "/containers/create?name="+name, data, daemonHost())
|
|
| 55 |
+ c.Assert(err, checker.IsNil, check.Commentf(string(resp))) |
|
| 56 |
+ c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(resp))) |
|
| 57 |
+ |
|
| 58 |
+ status, _, err = request.SockRequest("POST", "/containers/"+name+"/start", nil, daemonHost())
|
|
| 59 |
+ c.Assert(err, checker.IsNil) |
|
| 60 |
+ c.Assert(status, checker.Equals, http.StatusNoContent) |
|
| 61 |
+ |
|
| 62 |
+ err = <-ch |
|
| 63 |
+ if err != nil {
|
|
| 64 |
+ c.Fatal(err) |
|
| 65 |
+ } |
|
| 66 |
+ result := strings.TrimSpace(string(b)) |
|
| 67 |
+ if result != text {
|
|
| 68 |
+ c.Errorf("expected pipe to contain %s, got %s", text, result)
|
|
| 69 |
+ } |
|
| 70 |
+} |
| ... | ... |
@@ -4610,10 +4610,7 @@ func (s *DockerSuite) TestRunAddDeviceCgroupRule(c *check.C) {
|
| 4610 | 4610 |
|
| 4611 | 4611 |
// Verifies that running as local system is operating correctly on Windows |
| 4612 | 4612 |
func (s *DockerSuite) TestWindowsRunAsSystem(c *check.C) {
|
| 4613 |
- testRequires(c, DaemonIsWindows) |
|
| 4614 |
- if testEnv.DaemonKernelVersionNumeric() < 15000 {
|
|
| 4615 |
- c.Skip("Requires build 15000 or later")
|
|
| 4616 |
- } |
|
| 4613 |
+ testRequires(c, DaemonIsWindowsAtLeastBuild(15000)) |
|
| 4617 | 4614 |
out, _ := dockerCmd(c, "run", "--net=none", `--user=nt authority\system`, "--hostname=XYZZY", minimalBaseImage(), "cmd", "/c", `@echo %USERNAME%`) |
| 4618 | 4615 |
c.Assert(strings.TrimSpace(out), checker.Equals, "XYZZY$") |
| 4619 | 4616 |
} |
| ... | ... |
@@ -37,6 +37,12 @@ func DaemonIsWindows() bool {
|
| 37 | 37 |
return PlatformIs("windows")
|
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 |
+func DaemonIsWindowsAtLeastBuild(buildNumber int) func() bool {
|
|
| 41 |
+ return func() bool {
|
|
| 42 |
+ return DaemonIsWindows() && testEnv.DaemonKernelVersionNumeric() >= buildNumber |
|
| 43 |
+ } |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 40 | 46 |
func DaemonIsLinux() bool {
|
| 41 | 47 |
return PlatformIs("linux")
|
| 42 | 48 |
} |
| ... | ... |
@@ -16,6 +16,7 @@ import ( |
| 16 | 16 |
|
| 17 | 17 |
"github.com/Microsoft/hcsshim" |
| 18 | 18 |
"github.com/docker/docker/pkg/sysinfo" |
| 19 |
+ "github.com/docker/docker/pkg/system" |
|
| 19 | 20 |
opengcs "github.com/jhowardmsft/opengcs/gogcs/client" |
| 20 | 21 |
specs "github.com/opencontainers/runtime-spec/specs-go" |
| 21 | 22 |
"github.com/sirupsen/logrus" |
| ... | ... |
@@ -230,20 +231,35 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo |
| 230 | 230 |
} |
| 231 | 231 |
|
| 232 | 232 |
// Add the mounts (volumes, bind mounts etc) to the structure |
| 233 |
- mds := make([]hcsshim.MappedDir, len(spec.Mounts)) |
|
| 234 |
- for i, mount := range spec.Mounts {
|
|
| 235 |
- mds[i] = hcsshim.MappedDir{
|
|
| 236 |
- HostPath: mount.Source, |
|
| 237 |
- ContainerPath: mount.Destination, |
|
| 238 |
- ReadOnly: false, |
|
| 239 |
- } |
|
| 240 |
- for _, o := range mount.Options {
|
|
| 241 |
- if strings.ToLower(o) == "ro" {
|
|
| 242 |
- mds[i].ReadOnly = true |
|
| 233 |
+ var mds []hcsshim.MappedDir |
|
| 234 |
+ var mps []hcsshim.MappedPipe |
|
| 235 |
+ for _, mount := range spec.Mounts {
|
|
| 236 |
+ const pipePrefix = `\\.\pipe\` |
|
| 237 |
+ if strings.HasPrefix(mount.Destination, pipePrefix) {
|
|
| 238 |
+ mp := hcsshim.MappedPipe{
|
|
| 239 |
+ HostPath: mount.Source, |
|
| 240 |
+ ContainerPipeName: mount.Destination[len(pipePrefix):], |
|
| 241 |
+ } |
|
| 242 |
+ mps = append(mps, mp) |
|
| 243 |
+ } else {
|
|
| 244 |
+ md := hcsshim.MappedDir{
|
|
| 245 |
+ HostPath: mount.Source, |
|
| 246 |
+ ContainerPath: mount.Destination, |
|
| 247 |
+ ReadOnly: false, |
|
| 243 | 248 |
} |
| 249 |
+ for _, o := range mount.Options {
|
|
| 250 |
+ if strings.ToLower(o) == "ro" {
|
|
| 251 |
+ md.ReadOnly = true |
|
| 252 |
+ } |
|
| 253 |
+ } |
|
| 254 |
+ mds = append(mds, md) |
|
| 244 | 255 |
} |
| 245 | 256 |
} |
| 246 | 257 |
configuration.MappedDirectories = mds |
| 258 |
+ if len(mps) > 0 && system.GetOSVersion().Build < 16210 { // replace with Win10 RS3 build number at RTM
|
|
| 259 |
+ return errors.New("named pipe mounts are not supported on this version of Windows")
|
|
| 260 |
+ } |
|
| 261 |
+ configuration.MappedPipes = mps |
|
| 247 | 262 |
|
| 248 | 263 |
hcsContainer, err := hcsshim.CreateContainer(containerID, configuration) |
| 249 | 264 |
if err != nil {
|
| ... | ... |
@@ -4,7 +4,7 @@ import ( |
| 4 | 4 |
"errors" |
| 5 | 5 |
"fmt" |
| 6 | 6 |
"os" |
| 7 |
- "path/filepath" |
|
| 7 |
+ "runtime" |
|
| 8 | 8 |
|
| 9 | 9 |
"github.com/docker/docker/api/types/mount" |
| 10 | 10 |
) |
| ... | ... |
@@ -12,8 +12,7 @@ import ( |
| 12 | 12 |
var errBindNotExist = errors.New("bind source path does not exist")
|
| 13 | 13 |
|
| 14 | 14 |
type validateOpts struct {
|
| 15 |
- skipBindSourceCheck bool |
|
| 16 |
- skipAbsolutePathCheck bool |
|
| 15 |
+ skipBindSourceCheck bool |
|
| 17 | 16 |
} |
| 18 | 17 |
|
| 19 | 18 |
func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error {
|
| ... | ... |
@@ -30,10 +29,8 @@ func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error |
| 30 | 30 |
return &errMountConfig{mnt, err}
|
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 |
- if !opts.skipAbsolutePathCheck {
|
|
| 34 |
- if err := validateAbsolute(mnt.Target); err != nil {
|
|
| 35 |
- return &errMountConfig{mnt, err}
|
|
| 36 |
- } |
|
| 33 |
+ if err := validateAbsolute(mnt.Target); err != nil {
|
|
| 34 |
+ return &errMountConfig{mnt, err}
|
|
| 37 | 35 |
} |
| 38 | 36 |
|
| 39 | 37 |
switch mnt.Type {
|
| ... | ... |
@@ -97,6 +94,31 @@ func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error |
| 97 | 97 |
if _, err := ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
|
| 98 | 98 |
return &errMountConfig{mnt, err}
|
| 99 | 99 |
} |
| 100 |
+ case mount.TypeNamedPipe: |
|
| 101 |
+ if runtime.GOOS != "windows" {
|
|
| 102 |
+ return &errMountConfig{mnt, errors.New("named pipe bind mounts are not supported on this OS")}
|
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ if len(mnt.Source) == 0 {
|
|
| 106 |
+ return &errMountConfig{mnt, errMissingField("Source")}
|
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ if mnt.BindOptions != nil {
|
|
| 110 |
+ return &errMountConfig{mnt, errExtraField("BindOptions")}
|
|
| 111 |
+ } |
|
| 112 |
+ |
|
| 113 |
+ if mnt.ReadOnly {
|
|
| 114 |
+ return &errMountConfig{mnt, errExtraField("ReadOnly")}
|
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ if detectMountType(mnt.Source) != mount.TypeNamedPipe {
|
|
| 118 |
+ return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)}
|
|
| 119 |
+ } |
|
| 120 |
+ |
|
| 121 |
+ if detectMountType(mnt.Target) != mount.TypeNamedPipe {
|
|
| 122 |
+ return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Target)}
|
|
| 123 |
+ } |
|
| 124 |
+ |
|
| 100 | 125 |
default: |
| 101 | 126 |
return &errMountConfig{mnt, errors.New("mount type unknown")}
|
| 102 | 127 |
} |
| ... | ... |
@@ -121,7 +143,7 @@ func errMissingField(name string) error {
|
| 121 | 121 |
|
| 122 | 122 |
func validateAbsolute(p string) error {
|
| 123 | 123 |
p = convertSlash(p) |
| 124 |
- if filepath.IsAbs(p) {
|
|
| 124 |
+ if isAbsPath(p) {
|
|
| 125 | 125 |
return nil |
| 126 | 126 |
} |
| 127 | 127 |
return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
|
| ... | ... |
@@ -3,7 +3,6 @@ package volume |
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"os" |
| 6 |
- "path/filepath" |
|
| 7 | 6 |
"strings" |
| 8 | 7 |
"syscall" |
| 9 | 8 |
"time" |
| ... | ... |
@@ -284,12 +283,7 @@ func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
|
| 284 | 284 |
return nil, errInvalidMode(mode) |
| 285 | 285 |
} |
| 286 | 286 |
|
| 287 |
- if filepath.IsAbs(spec.Source) {
|
|
| 288 |
- spec.Type = mounttypes.TypeBind |
|
| 289 |
- } else {
|
|
| 290 |
- spec.Type = mounttypes.TypeVolume |
|
| 291 |
- } |
|
| 292 |
- |
|
| 287 |
+ spec.Type = detectMountType(spec.Source) |
|
| 293 | 288 |
spec.ReadOnly = !ReadWrite(mode) |
| 294 | 289 |
|
| 295 | 290 |
// cannot assume that if a volume driver is passed in that we should set it |
| ... | ... |
@@ -350,7 +344,7 @@ func ParseMountSpec(cfg mounttypes.Mount, options ...func(*validateOpts)) (*Moun |
| 350 | 350 |
mp.CopyData = false |
| 351 | 351 |
} |
| 352 | 352 |
} |
| 353 |
- case mounttypes.TypeBind: |
|
| 353 |
+ case mounttypes.TypeBind, mounttypes.TypeNamedPipe: |
|
| 354 | 354 |
mp.Source = clean(convertSlash(cfg.Source)) |
| 355 | 355 |
if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
|
| 356 | 356 |
mp.Propagation = cfg.BindOptions.Propagation |
| ... | ... |
@@ -143,6 +143,7 @@ func TestParseMountRaw(t *testing.T) {
|
| 143 | 143 |
type testParseMountRaw struct {
|
| 144 | 144 |
bind string |
| 145 | 145 |
driver string |
| 146 |
+ expType mount.Type |
|
| 146 | 147 |
expDest string |
| 147 | 148 |
expSource string |
| 148 | 149 |
expName string |
| ... | ... |
@@ -155,28 +156,31 @@ func TestParseMountRawSplit(t *testing.T) {
|
| 155 | 155 |
var cases []testParseMountRaw |
| 156 | 156 |
if runtime.GOOS == "windows" {
|
| 157 | 157 |
cases = []testParseMountRaw{
|
| 158 |
- {`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false},
|
|
| 159 |
- {`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false},
|
|
| 160 |
- {`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false},
|
|
| 161 |
- {`c:\:d:\:rw`, "local", `d:\`, `c:\`, ``, "", true, false},
|
|
| 162 |
- {`c:\:d:\:foo`, "local", `d:\`, `c:\`, ``, "", false, true},
|
|
| 163 |
- {`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false},
|
|
| 164 |
- {`name:d:`, "local", `d:`, ``, `name`, "local", true, false},
|
|
| 165 |
- {`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false},
|
|
| 166 |
- {`name:c:`, "", ``, ``, ``, "", true, true},
|
|
| 167 |
- {`driver/name:c:`, "", ``, ``, ``, "", true, true},
|
|
| 158 |
+ {`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false},
|
|
| 159 |
+ {`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
|
|
| 160 |
+ {`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false},
|
|
| 161 |
+ {`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
|
|
| 162 |
+ {`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true},
|
|
| 163 |
+ {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false},
|
|
| 164 |
+ {`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
|
|
| 165 |
+ {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
|
|
| 166 |
+ {`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
|
|
| 167 |
+ {`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
|
|
| 168 |
+ {`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false},
|
|
| 169 |
+ {`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
|
|
| 170 |
+ {`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
|
|
| 168 | 171 |
} |
| 169 | 172 |
} else {
|
| 170 | 173 |
cases = []testParseMountRaw{
|
| 171 |
- {"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false},
|
|
| 172 |
- {"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false},
|
|
| 173 |
- {"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false},
|
|
| 174 |
- {"/tmp:/tmp4:foo", "", "", "", "", "", false, true},
|
|
| 175 |
- {"name:/named1", "", "/named1", "", "name", "", true, false},
|
|
| 176 |
- {"name:/named2", "external", "/named2", "", "name", "external", true, false},
|
|
| 177 |
- {"name:/named3:ro", "local", "/named3", "", "name", "local", false, false},
|
|
| 178 |
- {"local/name:/tmp:rw", "", "/tmp", "", "local/name", "", true, false},
|
|
| 179 |
- {"/tmp:tmp", "", "", "", "", "", true, true},
|
|
| 174 |
+ {"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false},
|
|
| 175 |
+ {"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false},
|
|
| 176 |
+ {"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false},
|
|
| 177 |
+ {"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true},
|
|
| 178 |
+ {"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false},
|
|
| 179 |
+ {"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false},
|
|
| 180 |
+ {"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false},
|
|
| 181 |
+ {"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false},
|
|
| 182 |
+ {"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true},
|
|
| 180 | 183 |
} |
| 181 | 184 |
} |
| 182 | 185 |
|
| ... | ... |
@@ -195,8 +199,12 @@ func TestParseMountRawSplit(t *testing.T) {
|
| 195 | 195 |
continue |
| 196 | 196 |
} |
| 197 | 197 |
|
| 198 |
+ if m.Type != c.expType {
|
|
| 199 |
+ t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind)
|
|
| 200 |
+ } |
|
| 201 |
+ |
|
| 198 | 202 |
if m.Destination != c.expDest {
|
| 199 |
- t.Fatalf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
|
|
| 203 |
+ t.Fatalf("Expected destination '%s', was '%s', for spec '%s'", c.expDest, m.Destination, c.bind)
|
|
| 200 | 204 |
} |
| 201 | 205 |
|
| 202 | 206 |
if m.Source != c.expSource {
|
| ... | ... |
@@ -124,7 +124,12 @@ func validateCopyMode(mode bool) error {
|
| 124 | 124 |
} |
| 125 | 125 |
|
| 126 | 126 |
func convertSlash(p string) string {
|
| 127 |
- return filepath.ToSlash(p) |
|
| 127 |
+ return p |
|
| 128 |
+} |
|
| 129 |
+ |
|
| 130 |
+// isAbsPath reports whether the path is absolute. |
|
| 131 |
+func isAbsPath(p string) bool {
|
|
| 132 |
+ return filepath.IsAbs(p) |
|
| 128 | 133 |
} |
| 129 | 134 |
|
| 130 | 135 |
func splitRawSpec(raw string) ([]string, error) {
|
| ... | ... |
@@ -139,6 +144,13 @@ func splitRawSpec(raw string) ([]string, error) {
|
| 139 | 139 |
return arr, nil |
| 140 | 140 |
} |
| 141 | 141 |
|
| 142 |
+func detectMountType(p string) mounttypes.Type {
|
|
| 143 |
+ if filepath.IsAbs(p) {
|
|
| 144 |
+ return mounttypes.TypeBind |
|
| 145 |
+ } |
|
| 146 |
+ return mounttypes.TypeVolume |
|
| 147 |
+} |
|
| 148 |
+ |
|
| 142 | 149 |
func clean(p string) string {
|
| 143 | 150 |
return filepath.Clean(p) |
| 144 | 151 |
} |
| ... | ... |
@@ -6,6 +6,8 @@ import ( |
| 6 | 6 |
"path/filepath" |
| 7 | 7 |
"regexp" |
| 8 | 8 |
"strings" |
| 9 |
+ |
|
| 10 |
+ mounttypes "github.com/docker/docker/api/types/mount" |
|
| 9 | 11 |
) |
| 10 | 12 |
|
| 11 | 13 |
// read-write modes |
| ... | ... |
@@ -18,14 +20,7 @@ var roModes = map[string]bool{
|
| 18 | 18 |
"ro": true, |
| 19 | 19 |
} |
| 20 | 20 |
|
| 21 |
-var platformRawValidationOpts = []func(*validateOpts){
|
|
| 22 |
- // filepath.IsAbs is weird on Windows: |
|
| 23 |
- // `c:` is not considered an absolute path |
|
| 24 |
- // `c:\` is considered an absolute path |
|
| 25 |
- // In any case, the regex matching below ensures absolute paths |
|
| 26 |
- // TODO: consider this a bug with filepath.IsAbs (?) |
|
| 27 |
- func(o *validateOpts) { o.skipAbsolutePathCheck = true },
|
|
| 28 |
-} |
|
| 21 |
+var platformRawValidationOpts = []func(*validateOpts){}
|
|
| 29 | 22 |
|
| 30 | 23 |
const ( |
| 31 | 24 |
// Spec should be in the format [source:]destination[:mode] |
| ... | ... |
@@ -49,11 +44,13 @@ const ( |
| 49 | 49 |
RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*` |
| 50 | 50 |
// RXName is the second option of a source |
| 51 | 51 |
RXName = `[^\\/:*?"<>|\r\n]+` |
| 52 |
+ // RXPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \) |
|
| 53 |
+ RXPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+`
|
|
| 52 | 54 |
// RXReservedNames are reserved names not possible on Windows |
| 53 | 55 |
RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])` |
| 54 | 56 |
|
| 55 | 57 |
// RXSource is the combined possibilities for a source |
| 56 |
- RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `))):)?` |
|
| 58 |
+ RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `)|(` + RXPipe + `))):)?` |
|
| 57 | 59 |
|
| 58 | 60 |
// Source. Can be either a host directory, a name, or omitted: |
| 59 | 61 |
// HostDir: |
| ... | ... |
@@ -69,8 +66,10 @@ const ( |
| 69 | 69 |
// - And then followed by a colon which is not in the capture group |
| 70 | 70 |
// - And can be optional |
| 71 | 71 |
|
| 72 |
+ // RXDestinationDir is the file path option for the mount destination |
|
| 73 |
+ RXDestinationDir = `([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?)` |
|
| 72 | 74 |
// RXDestination is the regex expression for the mount destination |
| 73 |
- RXDestination = `(?P<destination>([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))` |
|
| 75 |
+ RXDestination = `(?P<destination>(` + RXDestinationDir + `)|(` + RXPipe + `))` |
|
| 74 | 76 |
// Destination (aka container path): |
| 75 | 77 |
// - Variation on hostdir but can be a drive followed by colon as well |
| 76 | 78 |
// - If a path, must be absolute. Can include spaces |
| ... | ... |
@@ -140,6 +139,15 @@ func splitRawSpec(raw string) ([]string, error) {
|
| 140 | 140 |
return split, nil |
| 141 | 141 |
} |
| 142 | 142 |
|
| 143 |
+func detectMountType(p string) mounttypes.Type {
|
|
| 144 |
+ if strings.HasPrefix(filepath.FromSlash(p), `\\.\pipe\`) {
|
|
| 145 |
+ return mounttypes.TypeNamedPipe |
|
| 146 |
+ } else if filepath.IsAbs(p) {
|
|
| 147 |
+ return mounttypes.TypeBind |
|
| 148 |
+ } |
|
| 149 |
+ return mounttypes.TypeVolume |
|
| 150 |
+} |
|
| 151 |
+ |
|
| 143 | 152 |
// IsVolumeNameValid checks a volume name in a platform specific manner. |
| 144 | 153 |
func IsVolumeNameValid(name string) (bool, error) {
|
| 145 | 154 |
nameExp := regexp.MustCompile(`^` + RXName + `$`) |
| ... | ... |
@@ -186,8 +194,19 @@ func convertSlash(p string) string {
|
| 186 | 186 |
return filepath.FromSlash(p) |
| 187 | 187 |
} |
| 188 | 188 |
|
| 189 |
+// isAbsPath returns whether a path is absolute for the purposes of mounting into a container |
|
| 190 |
+// (absolute paths, drive letter paths such as X:, and paths starting with `\\.\` to support named pipes). |
|
| 191 |
+func isAbsPath(p string) bool {
|
|
| 192 |
+ return filepath.IsAbs(p) || |
|
| 193 |
+ strings.HasPrefix(p, `\\.\`) || |
|
| 194 |
+ (len(p) == 2 && p[1] == ':' && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= 'A' && p[0] <= 'Z'))) |
|
| 195 |
+} |
|
| 196 |
+ |
|
| 197 |
+// Do not clean plain drive letters or paths starting with `\\.\`. |
|
| 198 |
+var cleanRegexp = regexp.MustCompile(`^([a-z]:|[/\\]{2}\.[/\\].*)$`)
|
|
| 199 |
+ |
|
| 189 | 200 |
func clean(p string) string {
|
| 190 |
- if match, _ := regexp.MatchString("^[a-z]:$", p); match {
|
|
| 201 |
+ if match := cleanRegexp.MatchString(p); match {
|
|
| 191 | 202 |
return p |
| 192 | 203 |
} |
| 193 | 204 |
return filepath.Clean(p) |