Przeglądaj źródła

Merge pull request #33852 from jstarks/win_named_pipes

Windows: named pipe mounts
Yong Tang 8 lat temu
rodzic
commit
202cf001dd

+ 2 - 0
api/types/mount/mount.go

@@ -15,6 +15,8 @@ const (
 	TypeVolume Type = "volume"
 	TypeVolume Type = "volume"
 	// TypeTmpfs is the type for mounting tmpfs
 	// TypeTmpfs is the type for mounting tmpfs
 	TypeTmpfs Type = "tmpfs"
 	TypeTmpfs Type = "tmpfs"
+	// TypeNamedPipe is the type for mounting Windows named pipes
+	TypeNamedPipe Type = "npipe"
 )
 )
 
 
 // Mount represents a mount (volume).
 // Mount represents a mount (volume).

+ 71 - 0
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)
+	}
+}

+ 1 - 4
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
 // Verifies that running as local system is operating correctly on Windows
 func (s *DockerSuite) TestWindowsRunAsSystem(c *check.C) {
 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%`)
 	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$")
 	c.Assert(strings.TrimSpace(out), checker.Equals, "XYZZY$")
 }
 }

+ 6 - 0
integration-cli/requirements_test.go

@@ -37,6 +37,12 @@ func DaemonIsWindows() bool {
 	return PlatformIs("windows")
 	return PlatformIs("windows")
 }
 }
 
 
+func DaemonIsWindowsAtLeastBuild(buildNumber int) func() bool {
+	return func() bool {
+		return DaemonIsWindows() && testEnv.DaemonKernelVersionNumeric() >= buildNumber
+	}
+}
+
 func DaemonIsLinux() bool {
 func DaemonIsLinux() bool {
 	return PlatformIs("linux")
 	return PlatformIs("linux")
 }
 }

+ 26 - 10
libcontainerd/client_windows.go

@@ -16,6 +16,7 @@ import (
 
 
 	"github.com/Microsoft/hcsshim"
 	"github.com/Microsoft/hcsshim"
 	"github.com/docker/docker/pkg/sysinfo"
 	"github.com/docker/docker/pkg/sysinfo"
+	"github.com/docker/docker/pkg/system"
 	opengcs "github.com/jhowardmsft/opengcs/gogcs/client"
 	opengcs "github.com/jhowardmsft/opengcs/gogcs/client"
 	specs "github.com/opencontainers/runtime-spec/specs-go"
 	specs "github.com/opencontainers/runtime-spec/specs-go"
 	"github.com/sirupsen/logrus"
 	"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
 	// 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
 	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)
 	hcsContainer, err := hcsshim.CreateContainer(containerID, configuration)
 	if err != nil {
 	if err != nil {

+ 30 - 8
volume/validate.go

@@ -4,7 +4,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
-	"path/filepath"
+	"runtime"
 
 
 	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types/mount"
 )
 )
@@ -12,8 +12,7 @@ import (
 var errBindNotExist = errors.New("bind source path does not exist")
 var errBindNotExist = errors.New("bind source path does not exist")
 
 
 type validateOpts struct {
 type validateOpts struct {
-	skipBindSourceCheck   bool
-	skipAbsolutePathCheck bool
+	skipBindSourceCheck bool
 }
 }
 
 
 func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error {
 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}
 		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 {
 	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 {
 		if _, err := ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
 			return &errMountConfig{mnt, err}
 			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:
 	default:
 		return &errMountConfig{mnt, errors.New("mount type unknown")}
 		return &errMountConfig{mnt, errors.New("mount type unknown")}
 	}
 	}
@@ -121,7 +143,7 @@ func errMissingField(name string) error {
 
 
 func validateAbsolute(p string) error {
 func validateAbsolute(p string) error {
 	p = convertSlash(p)
 	p = convertSlash(p)
-	if filepath.IsAbs(p) {
+	if isAbsPath(p) {
 		return nil
 		return nil
 	}
 	}
 	return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
 	return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)

+ 2 - 8
volume/volume.go

@@ -3,7 +3,6 @@ package volume
 import (
 import (
 	"fmt"
 	"fmt"
 	"os"
 	"os"
-	"path/filepath"
 	"strings"
 	"strings"
 	"syscall"
 	"syscall"
 	"time"
 	"time"
@@ -284,12 +283,7 @@ func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
 		return nil, errInvalidMode(mode)
 		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)
 	spec.ReadOnly = !ReadWrite(mode)
 
 
 	// cannot assume that if a volume driver is passed in that we should set it
 	// 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
 				mp.CopyData = false
 			}
 			}
 		}
 		}
-	case mounttypes.TypeBind:
+	case mounttypes.TypeBind, mounttypes.TypeNamedPipe:
 		mp.Source = clean(convertSlash(cfg.Source))
 		mp.Source = clean(convertSlash(cfg.Source))
 		if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
 		if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
 			mp.Propagation = cfg.BindOptions.Propagation
 			mp.Propagation = cfg.BindOptions.Propagation

+ 28 - 20
volume/volume_test.go

@@ -143,6 +143,7 @@ func TestParseMountRaw(t *testing.T) {
 type testParseMountRaw struct {
 type testParseMountRaw struct {
 	bind      string
 	bind      string
 	driver    string
 	driver    string
+	expType   mount.Type
 	expDest   string
 	expDest   string
 	expSource string
 	expSource string
 	expName   string
 	expName   string
@@ -155,28 +156,31 @@ func TestParseMountRawSplit(t *testing.T) {
 	var cases []testParseMountRaw
 	var cases []testParseMountRaw
 	if runtime.GOOS == "windows" {
 	if runtime.GOOS == "windows" {
 		cases = []testParseMountRaw{
 		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 {
 	} else {
 		cases = []testParseMountRaw{
 		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
 			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 {
 		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 {
 		if m.Source != c.expSource {

+ 13 - 1
volume/volume_unix.go

@@ -124,7 +124,12 @@ func validateCopyMode(mode bool) error {
 }
 }
 
 
 func convertSlash(p string) string {
 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) {
 func splitRawSpec(raw string) ([]string, error) {
@@ -139,6 +144,13 @@ func splitRawSpec(raw string) ([]string, error) {
 	return arr, nil
 	return arr, nil
 }
 }
 
 
+func detectMountType(p string) mounttypes.Type {
+	if filepath.IsAbs(p) {
+		return mounttypes.TypeBind
+	}
+	return mounttypes.TypeVolume
+}
+
 func clean(p string) string {
 func clean(p string) string {
 	return filepath.Clean(p)
 	return filepath.Clean(p)
 }
 }

+ 30 - 11
volume/volume_windows.go

@@ -6,6 +6,8 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
+
+	mounttypes "github.com/docker/docker/api/types/mount"
 )
 )
 
 
 // read-write modes
 // read-write modes
@@ -18,14 +20,7 @@ var roModes = map[string]bool{
 	"ro": true,
 	"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 (
 const (
 	// Spec should be in the format [source:]destination[:mode]
 	// Spec should be in the format [source:]destination[:mode]
@@ -49,11 +44,13 @@ const (
 	RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*`
 	RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*`
 	// RXName is the second option of a source
 	// RXName is the second option of a source
 	RXName = `[^\\/:*?"<>|\r\n]+`
 	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 are reserved names not possible on Windows
 	RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])`
 	RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])`
 
 
 	// RXSource is the combined possibilities for a source
 	// RXSource is the combined possibilities for a source
-	RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `))):)?`
+	RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `)|(` + RXPipe + `))):)?`
 
 
 	// Source. Can be either a host directory, a name, or omitted:
 	// Source. Can be either a host directory, a name, or omitted:
 	//  HostDir:
 	//  HostDir:
@@ -69,8 +66,10 @@ const (
 	//    -  And then followed by a colon which is not in the capture group
 	//    -  And then followed by a colon which is not in the capture group
 	//    -  And can be optional
 	//    -  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 is the regex expression for the mount destination
-	RXDestination = `(?P<destination>([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))`
+	RXDestination = `(?P<destination>(` + RXDestinationDir + `)|(` + RXPipe + `))`
 	// Destination (aka container path):
 	// Destination (aka container path):
 	//    -  Variation on hostdir but can be a drive followed by colon as well
 	//    -  Variation on hostdir but can be a drive followed by colon as well
 	//    -  If a path, must be absolute. Can include spaces
 	//    -  If a path, must be absolute. Can include spaces
@@ -140,6 +139,15 @@ func splitRawSpec(raw string) ([]string, error) {
 	return split, nil
 	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.
 // IsVolumeNameValid checks a volume name in a platform specific manner.
 func IsVolumeNameValid(name string) (bool, error) {
 func IsVolumeNameValid(name string) (bool, error) {
 	nameExp := regexp.MustCompile(`^` + RXName + `$`)
 	nameExp := regexp.MustCompile(`^` + RXName + `$`)
@@ -186,8 +194,19 @@ func convertSlash(p string) string {
 	return filepath.FromSlash(p)
 	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 {
 func clean(p string) string {
-	if match, _ := regexp.MatchString("^[a-z]:$", p); match {
+	if match := cleanRegexp.MatchString(p); match {
 		return p
 		return p
 	}
 	}
 	return filepath.Clean(p)
 	return filepath.Clean(p)