Merge pull request #33852 from jstarks/win_named_pipes

Windows: named pipe mounts
This commit is contained in:
Yong Tang 2017-08-07 16:54:05 -07:00 committed by GitHub
commit 202cf001dd
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)