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>
This commit is contained in:
John Starks 2017-07-31 14:23:52 -07:00
parent 6f1907898f
commit 54354db850
10 changed files with 209 additions and 62 deletions

View file

@ -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).

View file

@ -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)
}
}

View file

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

View file

@ -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")
}

View file

@ -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 {

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)