mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #33852 from jstarks/win_named_pipes
Windows: named pipe mounts
This commit is contained in:
commit
202cf001dd
10 changed files with 209 additions and 62 deletions
|
@ -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
integration-cli/docker_api_containers_windows_test.go
Normal file
71
integration-cli/docker_api_containers_windows_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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$")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
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" {
|
||||
mds[i].ReadOnly = true
|
||||
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 {
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
)
|
||||
|
@ -13,7 +13,6 @@ var errBindNotExist = errors.New("bind source path does not exist")
|
|||
|
||||
type validateOpts struct {
|
||||
skipBindSourceCheck bool
|
||||
skipAbsolutePathCheck bool
|
||||
}
|
||||
|
||||
func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error {
|
||||
|
@ -30,11 +29,9 @@ 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}
|
||||
}
|
||||
}
|
||||
|
||||
switch mnt.Type {
|
||||
case mount.TypeBind:
|
||||
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue