diff --git a/api/types/mount/mount.go b/api/types/mount/mount.go index 2744f85d6d..71368643dd 100644 --- a/api/types/mount/mount.go +++ b/api/types/mount/mount.go @@ -15,6 +15,8 @@ const ( TypeVolume Type = "volume" // TypeTmpfs is the type for mounting tmpfs TypeTmpfs Type = "tmpfs" + // TypeNamedPipe is the type for mounting Windows named pipes + TypeNamedPipe Type = "npipe" ) // Mount represents a mount (volume). diff --git a/integration-cli/docker_api_containers_windows_test.go b/integration-cli/docker_api_containers_windows_test.go new file mode 100644 index 0000000000..4cbe067cd5 --- /dev/null +++ b/integration-cli/docker_api_containers_windows_test.go @@ -0,0 +1,71 @@ +// +build windows + +package main + +import ( + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "strings" + + winio "github.com/Microsoft/go-winio" + "github.com/docker/docker/integration-cli/checker" + "github.com/docker/docker/integration-cli/request" + "github.com/go-check/check" +) + +func (s *DockerSuite) TestContainersAPICreateMountsBindNamedPipe(c *check.C) { + testRequires(c, SameHostDaemon, DaemonIsWindowsAtLeastBuild(16210)) // Named pipe support was added in RS3 + + // Create a host pipe to map into the container + hostPipeName := fmt.Sprintf(`\\.\pipe\docker-cli-test-pipe-%x`, rand.Uint64()) + pc := &winio.PipeConfig{ + SecurityDescriptor: "D:P(A;;GA;;;AU)", // Allow all users access to the pipe + } + l, err := winio.ListenPipe(hostPipeName, pc) + if err != nil { + c.Fatal(err) + } + defer l.Close() + + // Asynchronously read data that the container writes to the mapped pipe. + var b []byte + ch := make(chan error) + go func() { + conn, err := l.Accept() + if err == nil { + b, err = ioutil.ReadAll(conn) + conn.Close() + } + ch <- err + }() + + containerPipeName := `\\.\pipe\docker-cli-test-pipe` + text := "hello from a pipe" + cmd := fmt.Sprintf("echo %s > %s", text, containerPipeName) + + name := "test-bind-npipe" + data := map[string]interface{}{ + "Image": testEnv.MinimalBaseImage(), + "Cmd": []string{"cmd", "/c", cmd}, + "HostConfig": map[string]interface{}{"Mounts": []map[string]interface{}{{"Type": "npipe", "Source": hostPipeName, "Target": containerPipeName}}}, + } + + status, resp, err := request.SockRequest("POST", "/containers/create?name="+name, data, daemonHost()) + c.Assert(err, checker.IsNil, check.Commentf(string(resp))) + c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(resp))) + + status, _, err = request.SockRequest("POST", "/containers/"+name+"/start", nil, daemonHost()) + c.Assert(err, checker.IsNil) + c.Assert(status, checker.Equals, http.StatusNoContent) + + err = <-ch + if err != nil { + c.Fatal(err) + } + result := strings.TrimSpace(string(b)) + if result != text { + c.Errorf("expected pipe to contain %s, got %s", text, result) + } +} diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 544cfdf9a8..6032de4d93 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -4610,10 +4610,7 @@ func (s *DockerSuite) TestRunAddDeviceCgroupRule(c *check.C) { // Verifies that running as local system is operating correctly on Windows func (s *DockerSuite) TestWindowsRunAsSystem(c *check.C) { - testRequires(c, DaemonIsWindows) - if testEnv.DaemonKernelVersionNumeric() < 15000 { - c.Skip("Requires build 15000 or later") - } + testRequires(c, DaemonIsWindowsAtLeastBuild(15000)) out, _ := dockerCmd(c, "run", "--net=none", `--user=nt authority\system`, "--hostname=XYZZY", minimalBaseImage(), "cmd", "/c", `@echo %USERNAME%`) c.Assert(strings.TrimSpace(out), checker.Equals, "XYZZY$") } diff --git a/integration-cli/requirements_test.go b/integration-cli/requirements_test.go index 15b3df2265..d6cc27b1d0 100644 --- a/integration-cli/requirements_test.go +++ b/integration-cli/requirements_test.go @@ -37,6 +37,12 @@ func DaemonIsWindows() bool { return PlatformIs("windows") } +func DaemonIsWindowsAtLeastBuild(buildNumber int) func() bool { + return func() bool { + return DaemonIsWindows() && testEnv.DaemonKernelVersionNumeric() >= buildNumber + } +} + func DaemonIsLinux() bool { return PlatformIs("linux") } diff --git a/libcontainerd/client_windows.go b/libcontainerd/client_windows.go index a12948a96d..3c17803615 100644 --- a/libcontainerd/client_windows.go +++ b/libcontainerd/client_windows.go @@ -16,6 +16,7 @@ import ( "github.com/Microsoft/hcsshim" "github.com/docker/docker/pkg/sysinfo" + "github.com/docker/docker/pkg/system" opengcs "github.com/jhowardmsft/opengcs/gogcs/client" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" @@ -230,20 +231,35 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo } // Add the mounts (volumes, bind mounts etc) to the structure - mds := make([]hcsshim.MappedDir, len(spec.Mounts)) - for i, mount := range spec.Mounts { - mds[i] = hcsshim.MappedDir{ - HostPath: mount.Source, - ContainerPath: mount.Destination, - ReadOnly: false, - } - for _, o := range mount.Options { - if strings.ToLower(o) == "ro" { - mds[i].ReadOnly = true + var mds []hcsshim.MappedDir + var mps []hcsshim.MappedPipe + for _, mount := range spec.Mounts { + const pipePrefix = `\\.\pipe\` + if strings.HasPrefix(mount.Destination, pipePrefix) { + mp := hcsshim.MappedPipe{ + HostPath: mount.Source, + ContainerPipeName: mount.Destination[len(pipePrefix):], } + mps = append(mps, mp) + } else { + md := hcsshim.MappedDir{ + HostPath: mount.Source, + ContainerPath: mount.Destination, + ReadOnly: false, + } + for _, o := range mount.Options { + if strings.ToLower(o) == "ro" { + md.ReadOnly = true + } + } + mds = append(mds, md) } } configuration.MappedDirectories = mds + if len(mps) > 0 && system.GetOSVersion().Build < 16210 { // replace with Win10 RS3 build number at RTM + return errors.New("named pipe mounts are not supported on this version of Windows") + } + configuration.MappedPipes = mps hcsContainer, err := hcsshim.CreateContainer(containerID, configuration) if err != nil { diff --git a/volume/validate.go b/volume/validate.go index 42396a0dad..5de46198f6 100644 --- a/volume/validate.go +++ b/volume/validate.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "os" - "path/filepath" + "runtime" "github.com/docker/docker/api/types/mount" ) @@ -12,8 +12,7 @@ import ( var errBindNotExist = errors.New("bind source path does not exist") type validateOpts struct { - skipBindSourceCheck bool - skipAbsolutePathCheck bool + skipBindSourceCheck bool } func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error { @@ -30,10 +29,8 @@ func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error return &errMountConfig{mnt, err} } - if !opts.skipAbsolutePathCheck { - if err := validateAbsolute(mnt.Target); err != nil { - return &errMountConfig{mnt, err} - } + if err := validateAbsolute(mnt.Target); err != nil { + return &errMountConfig{mnt, err} } switch mnt.Type { @@ -97,6 +94,31 @@ func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error if _, err := ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil { return &errMountConfig{mnt, err} } + case mount.TypeNamedPipe: + if runtime.GOOS != "windows" { + return &errMountConfig{mnt, errors.New("named pipe bind mounts are not supported on this OS")} + } + + if len(mnt.Source) == 0 { + return &errMountConfig{mnt, errMissingField("Source")} + } + + if mnt.BindOptions != nil { + return &errMountConfig{mnt, errExtraField("BindOptions")} + } + + if mnt.ReadOnly { + return &errMountConfig{mnt, errExtraField("ReadOnly")} + } + + if detectMountType(mnt.Source) != mount.TypeNamedPipe { + return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)} + } + + if detectMountType(mnt.Target) != mount.TypeNamedPipe { + return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Target)} + } + default: return &errMountConfig{mnt, errors.New("mount type unknown")} } @@ -121,7 +143,7 @@ func errMissingField(name string) error { func validateAbsolute(p string) error { p = convertSlash(p) - if filepath.IsAbs(p) { + if isAbsPath(p) { return nil } return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p) diff --git a/volume/volume.go b/volume/volume.go index 8598d4cb8f..7e8d16cc68 100644 --- a/volume/volume.go +++ b/volume/volume.go @@ -3,7 +3,6 @@ package volume import ( "fmt" "os" - "path/filepath" "strings" "syscall" "time" @@ -284,12 +283,7 @@ func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) { return nil, errInvalidMode(mode) } - if filepath.IsAbs(spec.Source) { - spec.Type = mounttypes.TypeBind - } else { - spec.Type = mounttypes.TypeVolume - } - + spec.Type = detectMountType(spec.Source) spec.ReadOnly = !ReadWrite(mode) // 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 mp.CopyData = false } } - case mounttypes.TypeBind: + case mounttypes.TypeBind, mounttypes.TypeNamedPipe: mp.Source = clean(convertSlash(cfg.Source)) if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 { mp.Propagation = cfg.BindOptions.Propagation diff --git a/volume/volume_test.go b/volume/volume_test.go index 5c3e0e381b..395f374ff0 100644 --- a/volume/volume_test.go +++ b/volume/volume_test.go @@ -143,6 +143,7 @@ func TestParseMountRaw(t *testing.T) { type testParseMountRaw struct { bind string driver string + expType mount.Type expDest string expSource string expName string @@ -155,28 +156,31 @@ func TestParseMountRawSplit(t *testing.T) { var cases []testParseMountRaw if runtime.GOOS == "windows" { cases = []testParseMountRaw{ - {`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false}, - {`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false}, - {`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false}, - {`c:\:d:\:rw`, "local", `d:\`, `c:\`, ``, "", true, false}, - {`c:\:d:\:foo`, "local", `d:\`, `c:\`, ``, "", false, true}, - {`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false}, - {`name:d:`, "local", `d:`, ``, `name`, "local", true, false}, - {`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false}, - {`name:c:`, "", ``, ``, ``, "", true, true}, - {`driver/name:c:`, "", ``, ``, ``, "", true, true}, + {`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false}, + {`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, + {`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false}, + {`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, + {`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true}, + {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false}, + {`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, + {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, + {`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, + {`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, + {`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false}, + {`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, + {`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, } } else { cases = []testParseMountRaw{ - {"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false}, - {"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false}, - {"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false}, - {"/tmp:/tmp4:foo", "", "", "", "", "", false, true}, - {"name:/named1", "", "/named1", "", "name", "", true, false}, - {"name:/named2", "external", "/named2", "", "name", "external", true, false}, - {"name:/named3:ro", "local", "/named3", "", "name", "local", false, false}, - {"local/name:/tmp:rw", "", "/tmp", "", "local/name", "", true, false}, - {"/tmp:tmp", "", "", "", "", "", true, true}, + {"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false}, + {"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false}, + {"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false}, + {"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true}, + {"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false}, + {"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false}, + {"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false}, + {"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false}, + {"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true}, } } @@ -195,8 +199,12 @@ func TestParseMountRawSplit(t *testing.T) { continue } + if m.Type != c.expType { + t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind) + } + if m.Destination != c.expDest { - t.Fatalf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind) + t.Fatalf("Expected destination '%s', was '%s', for spec '%s'", c.expDest, m.Destination, c.bind) } if m.Source != c.expSource { diff --git a/volume/volume_unix.go b/volume/volume_unix.go index e35b70c03b..5dde82147f 100644 --- a/volume/volume_unix.go +++ b/volume/volume_unix.go @@ -124,7 +124,12 @@ func validateCopyMode(mode bool) error { } func convertSlash(p string) string { - return filepath.ToSlash(p) + return p +} + +// isAbsPath reports whether the path is absolute. +func isAbsPath(p string) bool { + return filepath.IsAbs(p) } func splitRawSpec(raw string) ([]string, error) { @@ -139,6 +144,13 @@ func splitRawSpec(raw string) ([]string, error) { return arr, nil } +func detectMountType(p string) mounttypes.Type { + if filepath.IsAbs(p) { + return mounttypes.TypeBind + } + return mounttypes.TypeVolume +} + func clean(p string) string { return filepath.Clean(p) } diff --git a/volume/volume_windows.go b/volume/volume_windows.go index 22f6fc7a14..d792b385f8 100644 --- a/volume/volume_windows.go +++ b/volume/volume_windows.go @@ -6,6 +6,8 @@ import ( "path/filepath" "regexp" "strings" + + mounttypes "github.com/docker/docker/api/types/mount" ) // read-write modes @@ -18,14 +20,7 @@ var roModes = map[string]bool{ "ro": true, } -var platformRawValidationOpts = []func(*validateOpts){ - // filepath.IsAbs is weird on Windows: - // `c:` is not considered an absolute path - // `c:\` is considered an absolute path - // In any case, the regex matching below ensures absolute paths - // TODO: consider this a bug with filepath.IsAbs (?) - func(o *validateOpts) { o.skipAbsolutePathCheck = true }, -} +var platformRawValidationOpts = []func(*validateOpts){} const ( // Spec should be in the format [source:]destination[:mode] @@ -49,11 +44,13 @@ const ( RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*` // RXName is the second option of a source RXName = `[^\\/:*?"<>|\r\n]+` + // RXPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \) + RXPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+` // RXReservedNames are reserved names not possible on Windows RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])` // RXSource is the combined possibilities for a source - RXSource = `((?P((` + RXHostDir + `)|(` + RXName + `))):)?` + RXSource = `((?P((` + RXHostDir + `)|(` + RXName + `)|(` + RXPipe + `))):)?` // Source. Can be either a host directory, a name, or omitted: // HostDir: @@ -69,8 +66,10 @@ const ( // - And then followed by a colon which is not in the capture group // - And can be optional + // RXDestinationDir is the file path option for the mount destination + RXDestinationDir = `([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?)` // RXDestination is the regex expression for the mount destination - RXDestination = `(?P([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))` + RXDestination = `(?P(` + RXDestinationDir + `)|(` + RXPipe + `))` // Destination (aka container path): // - Variation on hostdir but can be a drive followed by colon as well // - If a path, must be absolute. Can include spaces @@ -140,6 +139,15 @@ func splitRawSpec(raw string) ([]string, error) { return split, nil } +func detectMountType(p string) mounttypes.Type { + if strings.HasPrefix(filepath.FromSlash(p), `\\.\pipe\`) { + return mounttypes.TypeNamedPipe + } else if filepath.IsAbs(p) { + return mounttypes.TypeBind + } + return mounttypes.TypeVolume +} + // IsVolumeNameValid checks a volume name in a platform specific manner. func IsVolumeNameValid(name string) (bool, error) { nameExp := regexp.MustCompile(`^` + RXName + `$`) @@ -186,8 +194,19 @@ func convertSlash(p string) string { return filepath.FromSlash(p) } +// isAbsPath returns whether a path is absolute for the purposes of mounting into a container +// (absolute paths, drive letter paths such as X:, and paths starting with `\\.\` to support named pipes). +func isAbsPath(p string) bool { + return filepath.IsAbs(p) || + strings.HasPrefix(p, `\\.\`) || + (len(p) == 2 && p[1] == ':' && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= 'A' && p[0] <= 'Z'))) +} + +// Do not clean plain drive letters or paths starting with `\\.\`. +var cleanRegexp = regexp.MustCompile(`^([a-z]:|[/\\]{2}\.[/\\].*)$`) + func clean(p string) string { - if match, _ := regexp.MatchString("^[a-z]:$", p); match { + if match := cleanRegexp.MatchString(p); match { return p } return filepath.Clean(p)