Browse Source

Windows: Add named pipe mount support

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>
John Starks 8 năm trước cách đây
mục cha
commit
54354db850

+ 2 - 0
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).

+ 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
 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$")
 }

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

+ 26 - 10
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 {

+ 30 - 8
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)

+ 2 - 8
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

+ 28 - 20
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 {

+ 13 - 1
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)
 }

+ 30 - 11
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<source>((` + RXHostDir + `)|(` + RXName + `))):)?`
+	RXSource = `((?P<source>((` + 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<destination>([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))`
+	RXDestination = `(?P<destination>(` + 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)