Skip to content

Commit

Permalink
Windows: Add named pipe mount support
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
jstarks committed Aug 7, 2017
1 parent 6f19078 commit 54354db
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 62 deletions.
2 changes: 2 additions & 0 deletions api/types/mount/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
71 changes: 71 additions & 0 deletions integration-cli/docker_api_containers_windows_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
5 changes: 1 addition & 4 deletions integration-cli/docker_cli_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 6 additions & 0 deletions integration-cli/requirements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
36 changes: 26 additions & 10 deletions libcontainerd/client_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
38 changes: 30 additions & 8 deletions volume/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"

"github.com/docker/docker/api/types/mount"
)

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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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")}
}
Expand All @@ -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)
Expand Down
10 changes: 2 additions & 8 deletions volume/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package volume
import (
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
48 changes: 28 additions & 20 deletions volume/volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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},
}
}

Expand All @@ -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 {
Expand Down
14 changes: 13 additions & 1 deletion volume/volume_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down
Loading

0 comments on commit 54354db

Please sign in to comment.