summaryrefslogtreecommitdiff
path: root/volume/mounts
diff options
context:
space:
mode:
authorBrian Goff <cpuguy83@gmail.com>2018-04-17 16:50:28 -0400
committerBrian Goff <cpuguy83@gmail.com>2018-04-19 06:35:54 -0400
commit6a70fd222b95643a8a6b88e2634d5f085ae4122a (patch)
tree90f370ce7033dc470ce97c4c26769bf8fb6e56bb /volume/mounts
parent69a5611dde5b46785c8f071a744f5b4b7547d5b9 (diff)
downloaddocker-6a70fd222b95643a8a6b88e2634d5f085ae4122a.tar.gz
Move mount parsing to separate package.
This moves the platform specific stuff in a separate package and keeps the `volume` package and the defined interfaces light to import. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
Diffstat (limited to 'volume/mounts')
-rw-r--r--volume/mounts/lcow_parser.go34
-rw-r--r--volume/mounts/linux_parser.go417
-rw-r--r--volume/mounts/mounts.go170
-rw-r--r--volume/mounts/parser.go47
-rw-r--r--volume/mounts/parser_test.go480
-rw-r--r--volume/mounts/validate.go28
-rw-r--r--volume/mounts/validate_test.go73
-rw-r--r--volume/mounts/validate_unix_test.go8
-rw-r--r--volume/mounts/validate_windows_test.go6
-rw-r--r--volume/mounts/volume_copy.go23
-rw-r--r--volume/mounts/volume_unix.go18
-rw-r--r--volume/mounts/volume_windows.go8
-rw-r--r--volume/mounts/windows_parser.go456
13 files changed, 1768 insertions, 0 deletions
diff --git a/volume/mounts/lcow_parser.go b/volume/mounts/lcow_parser.go
new file mode 100644
index 0000000000..bafb7b07f8
--- /dev/null
+++ b/volume/mounts/lcow_parser.go
@@ -0,0 +1,34 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "errors"
+ "path"
+
+ "github.com/docker/docker/api/types/mount"
+)
+
+var lcowSpecificValidators mountValidator = func(m *mount.Mount) error {
+ if path.Clean(m.Target) == "/" {
+ return ErrVolumeTargetIsRoot
+ }
+ if m.Type == mount.TypeNamedPipe {
+ return errors.New("Linux containers on Windows do not support named pipe mounts")
+ }
+ return nil
+}
+
+type lcowParser struct {
+ windowsParser
+}
+
+func (p *lcowParser) ValidateMountConfig(mnt *mount.Mount) error {
+ return p.validateMountConfigReg(mnt, rxLCOWDestination, lcowSpecificValidators)
+}
+
+func (p *lcowParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
+ return p.parseMountRaw(raw, volumeDriver, rxLCOWDestination, false, lcowSpecificValidators)
+}
+
+func (p *lcowParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
+ return p.parseMountSpec(cfg, rxLCOWDestination, false, lcowSpecificValidators)
+}
diff --git a/volume/mounts/linux_parser.go b/volume/mounts/linux_parser.go
new file mode 100644
index 0000000000..8e436aec0e
--- /dev/null
+++ b/volume/mounts/linux_parser.go
@@ -0,0 +1,417 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "errors"
+ "fmt"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/docker/docker/api/types/mount"
+ "github.com/docker/docker/pkg/stringid"
+ "github.com/docker/docker/volume"
+)
+
+type linuxParser struct {
+}
+
+func linuxSplitRawSpec(raw string) ([]string, error) {
+ if strings.Count(raw, ":") > 2 {
+ return nil, errInvalidSpec(raw)
+ }
+
+ arr := strings.SplitN(raw, ":", 3)
+ if arr[0] == "" {
+ return nil, errInvalidSpec(raw)
+ }
+ return arr, nil
+}
+
+func linuxValidateNotRoot(p string) error {
+ p = path.Clean(strings.Replace(p, `\`, `/`, -1))
+ if p == "/" {
+ return ErrVolumeTargetIsRoot
+ }
+ return nil
+}
+func linuxValidateAbsolute(p string) error {
+ p = strings.Replace(p, `\`, `/`, -1)
+ if path.IsAbs(p) {
+ return nil
+ }
+ return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
+}
+func (p *linuxParser) ValidateMountConfig(mnt *mount.Mount) error {
+ // there was something looking like a bug in existing codebase:
+ // - validateMountConfig on linux was called with options skipping bind source existence when calling ParseMountRaw
+ // - but not when calling ParseMountSpec directly... nor when the unit test called it directly
+ return p.validateMountConfigImpl(mnt, true)
+}
+func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSourceExists bool) error {
+ if len(mnt.Target) == 0 {
+ return &errMountConfig{mnt, errMissingField("Target")}
+ }
+
+ if err := linuxValidateNotRoot(mnt.Target); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+
+ if err := linuxValidateAbsolute(mnt.Target); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+
+ switch mnt.Type {
+ case mount.TypeBind:
+ if len(mnt.Source) == 0 {
+ return &errMountConfig{mnt, errMissingField("Source")}
+ }
+ // Don't error out just because the propagation mode is not supported on the platform
+ if opts := mnt.BindOptions; opts != nil {
+ if len(opts.Propagation) > 0 && len(linuxPropagationModes) > 0 {
+ if _, ok := linuxPropagationModes[opts.Propagation]; !ok {
+ return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
+ }
+ }
+ }
+ if mnt.VolumeOptions != nil {
+ return &errMountConfig{mnt, errExtraField("VolumeOptions")}
+ }
+
+ if err := linuxValidateAbsolute(mnt.Source); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+
+ if validateBindSourceExists {
+ exists, _, _ := currentFileInfoProvider.fileInfo(mnt.Source)
+ if !exists {
+ return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)}
+ }
+ }
+
+ case mount.TypeVolume:
+ if mnt.BindOptions != nil {
+ return &errMountConfig{mnt, errExtraField("BindOptions")}
+ }
+
+ if len(mnt.Source) == 0 && mnt.ReadOnly {
+ return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
+ }
+ case mount.TypeTmpfs:
+ if len(mnt.Source) != 0 {
+ return &errMountConfig{mnt, errExtraField("Source")}
+ }
+ if _, err := p.ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+ default:
+ return &errMountConfig{mnt, errors.New("mount type unknown")}
+ }
+ return nil
+}
+
+// read-write modes
+var rwModes = map[string]bool{
+ "rw": true,
+ "ro": true,
+}
+
+// label modes
+var linuxLabelModes = map[string]bool{
+ "Z": true,
+ "z": true,
+}
+
+// consistency modes
+var linuxConsistencyModes = map[mount.Consistency]bool{
+ mount.ConsistencyFull: true,
+ mount.ConsistencyCached: true,
+ mount.ConsistencyDelegated: true,
+}
+var linuxPropagationModes = map[mount.Propagation]bool{
+ mount.PropagationPrivate: true,
+ mount.PropagationRPrivate: true,
+ mount.PropagationSlave: true,
+ mount.PropagationRSlave: true,
+ mount.PropagationShared: true,
+ mount.PropagationRShared: true,
+}
+
+const linuxDefaultPropagationMode = mount.PropagationRPrivate
+
+func linuxGetPropagation(mode string) mount.Propagation {
+ for _, o := range strings.Split(mode, ",") {
+ prop := mount.Propagation(o)
+ if linuxPropagationModes[prop] {
+ return prop
+ }
+ }
+ return linuxDefaultPropagationMode
+}
+
+func linuxHasPropagation(mode string) bool {
+ for _, o := range strings.Split(mode, ",") {
+ if linuxPropagationModes[mount.Propagation(o)] {
+ return true
+ }
+ }
+ return false
+}
+
+func linuxValidMountMode(mode string) bool {
+ if mode == "" {
+ return true
+ }
+
+ rwModeCount := 0
+ labelModeCount := 0
+ propagationModeCount := 0
+ copyModeCount := 0
+ consistencyModeCount := 0
+
+ for _, o := range strings.Split(mode, ",") {
+ switch {
+ case rwModes[o]:
+ rwModeCount++
+ case linuxLabelModes[o]:
+ labelModeCount++
+ case linuxPropagationModes[mount.Propagation(o)]:
+ propagationModeCount++
+ case copyModeExists(o):
+ copyModeCount++
+ case linuxConsistencyModes[mount.Consistency(o)]:
+ consistencyModeCount++
+ default:
+ return false
+ }
+ }
+
+ // Only one string for each mode is allowed.
+ if rwModeCount > 1 || labelModeCount > 1 || propagationModeCount > 1 || copyModeCount > 1 || consistencyModeCount > 1 {
+ return false
+ }
+ return true
+}
+
+func (p *linuxParser) ReadWrite(mode string) bool {
+ if !linuxValidMountMode(mode) {
+ return false
+ }
+
+ for _, o := range strings.Split(mode, ",") {
+ if o == "ro" {
+ return false
+ }
+ }
+ return true
+}
+
+func (p *linuxParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
+ arr, err := linuxSplitRawSpec(raw)
+ if err != nil {
+ return nil, err
+ }
+
+ var spec mount.Mount
+ var mode string
+ switch len(arr) {
+ case 1:
+ // Just a destination path in the container
+ spec.Target = arr[0]
+ case 2:
+ if linuxValidMountMode(arr[1]) {
+ // Destination + Mode is not a valid volume - volumes
+ // cannot include a mode. e.g. /foo:rw
+ return nil, errInvalidSpec(raw)
+ }
+ // Host Source Path or Name + Destination
+ spec.Source = arr[0]
+ spec.Target = arr[1]
+ case 3:
+ // HostSourcePath+DestinationPath+Mode
+ spec.Source = arr[0]
+ spec.Target = arr[1]
+ mode = arr[2]
+ default:
+ return nil, errInvalidSpec(raw)
+ }
+
+ if !linuxValidMountMode(mode) {
+ return nil, errInvalidMode(mode)
+ }
+
+ if path.IsAbs(spec.Source) {
+ spec.Type = mount.TypeBind
+ } else {
+ spec.Type = mount.TypeVolume
+ }
+
+ spec.ReadOnly = !p.ReadWrite(mode)
+
+ // cannot assume that if a volume driver is passed in that we should set it
+ if volumeDriver != "" && spec.Type == mount.TypeVolume {
+ spec.VolumeOptions = &mount.VolumeOptions{
+ DriverConfig: &mount.Driver{Name: volumeDriver},
+ }
+ }
+
+ if copyData, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
+ if spec.VolumeOptions == nil {
+ spec.VolumeOptions = &mount.VolumeOptions{}
+ }
+ spec.VolumeOptions.NoCopy = !copyData
+ }
+ if linuxHasPropagation(mode) {
+ spec.BindOptions = &mount.BindOptions{
+ Propagation: linuxGetPropagation(mode),
+ }
+ }
+
+ mp, err := p.parseMountSpec(spec, false)
+ if mp != nil {
+ mp.Mode = mode
+ }
+ if err != nil {
+ err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
+ }
+ return mp, err
+}
+func (p *linuxParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
+ return p.parseMountSpec(cfg, true)
+}
+func (p *linuxParser) parseMountSpec(cfg mount.Mount, validateBindSourceExists bool) (*MountPoint, error) {
+ if err := p.validateMountConfigImpl(&cfg, validateBindSourceExists); err != nil {
+ return nil, err
+ }
+ mp := &MountPoint{
+ RW: !cfg.ReadOnly,
+ Destination: path.Clean(filepath.ToSlash(cfg.Target)),
+ Type: cfg.Type,
+ Spec: cfg,
+ }
+
+ switch cfg.Type {
+ case mount.TypeVolume:
+ if cfg.Source == "" {
+ mp.Name = stringid.GenerateNonCryptoID()
+ } else {
+ mp.Name = cfg.Source
+ }
+ mp.CopyData = p.DefaultCopyMode()
+
+ if cfg.VolumeOptions != nil {
+ if cfg.VolumeOptions.DriverConfig != nil {
+ mp.Driver = cfg.VolumeOptions.DriverConfig.Name
+ }
+ if cfg.VolumeOptions.NoCopy {
+ mp.CopyData = false
+ }
+ }
+ case mount.TypeBind:
+ mp.Source = path.Clean(filepath.ToSlash(cfg.Source))
+ if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
+ mp.Propagation = cfg.BindOptions.Propagation
+ } else {
+ // If user did not specify a propagation mode, get
+ // default propagation mode.
+ mp.Propagation = linuxDefaultPropagationMode
+ }
+ case mount.TypeTmpfs:
+ // NOP
+ }
+ return mp, nil
+}
+
+func (p *linuxParser) ParseVolumesFrom(spec string) (string, string, error) {
+ if len(spec) == 0 {
+ return "", "", fmt.Errorf("volumes-from specification cannot be an empty string")
+ }
+
+ specParts := strings.SplitN(spec, ":", 2)
+ id := specParts[0]
+ mode := "rw"
+
+ if len(specParts) == 2 {
+ mode = specParts[1]
+ if !linuxValidMountMode(mode) {
+ return "", "", errInvalidMode(mode)
+ }
+ // For now don't allow propagation properties while importing
+ // volumes from data container. These volumes will inherit
+ // the same propagation property as of the original volume
+ // in data container. This probably can be relaxed in future.
+ if linuxHasPropagation(mode) {
+ return "", "", errInvalidMode(mode)
+ }
+ // Do not allow copy modes on volumes-from
+ if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
+ return "", "", errInvalidMode(mode)
+ }
+ }
+ return id, mode, nil
+}
+
+func (p *linuxParser) DefaultPropagationMode() mount.Propagation {
+ return linuxDefaultPropagationMode
+}
+
+func (p *linuxParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) {
+ var rawOpts []string
+ if readOnly {
+ rawOpts = append(rawOpts, "ro")
+ }
+
+ if opt != nil && opt.Mode != 0 {
+ rawOpts = append(rawOpts, fmt.Sprintf("mode=%o", opt.Mode))
+ }
+
+ if opt != nil && opt.SizeBytes != 0 {
+ // calculate suffix here, making this linux specific, but that is
+ // okay, since API is that way anyways.
+
+ // we do this by finding the suffix that divides evenly into the
+ // value, returning the value itself, with no suffix, if it fails.
+ //
+ // For the most part, we don't enforce any semantic to this values.
+ // The operating system will usually align this and enforce minimum
+ // and maximums.
+ var (
+ size = opt.SizeBytes
+ suffix string
+ )
+ for _, r := range []struct {
+ suffix string
+ divisor int64
+ }{
+ {"g", 1 << 30},
+ {"m", 1 << 20},
+ {"k", 1 << 10},
+ } {
+ if size%r.divisor == 0 {
+ size = size / r.divisor
+ suffix = r.suffix
+ break
+ }
+ }
+
+ rawOpts = append(rawOpts, fmt.Sprintf("size=%d%s", size, suffix))
+ }
+ return strings.Join(rawOpts, ","), nil
+}
+
+func (p *linuxParser) DefaultCopyMode() bool {
+ return true
+}
+func (p *linuxParser) ValidateVolumeName(name string) error {
+ return nil
+}
+
+func (p *linuxParser) IsBackwardCompatible(m *MountPoint) bool {
+ return len(m.Source) > 0 || m.Driver == volume.DefaultDriverName
+}
+
+func (p *linuxParser) ValidateTmpfsMountDestination(dest string) error {
+ if err := linuxValidateNotRoot(dest); err != nil {
+ return err
+ }
+ return linuxValidateAbsolute(dest)
+}
diff --git a/volume/mounts/mounts.go b/volume/mounts/mounts.go
new file mode 100644
index 0000000000..8f255a5482
--- /dev/null
+++ b/volume/mounts/mounts.go
@@ -0,0 +1,170 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "syscall"
+
+ mounttypes "github.com/docker/docker/api/types/mount"
+ "github.com/docker/docker/pkg/idtools"
+ "github.com/docker/docker/pkg/stringid"
+ "github.com/docker/docker/volume"
+ "github.com/opencontainers/selinux/go-selinux/label"
+ "github.com/pkg/errors"
+)
+
+// MountPoint is the intersection point between a volume and a container. It
+// specifies which volume is to be used and where inside a container it should
+// be mounted.
+//
+// Note that this type is embedded in `container.Container` object and persisted to disk.
+// Changes to this struct need to by synced with on disk state.
+type MountPoint struct {
+ // Source is the source path of the mount.
+ // E.g. `mount --bind /foo /bar`, `/foo` is the `Source`.
+ Source string
+ // Destination is the path relative to the container root (`/`) to the mount point
+ // It is where the `Source` is mounted to
+ Destination string
+ // RW is set to true when the mountpoint should be mounted as read-write
+ RW bool
+ // Name is the name reference to the underlying data defined by `Source`
+ // e.g., the volume name
+ Name string
+ // Driver is the volume driver used to create the volume (if it is a volume)
+ Driver string
+ // Type of mount to use, see `Type<foo>` definitions in github.com/docker/docker/api/types/mount
+ Type mounttypes.Type `json:",omitempty"`
+ // Volume is the volume providing data to this mountpoint.
+ // This is nil unless `Type` is set to `TypeVolume`
+ Volume volume.Volume `json:"-"`
+
+ // Mode is the comma separated list of options supplied by the user when creating
+ // the bind/volume mount.
+ // Note Mode is not used on Windows
+ Mode string `json:"Relabel,omitempty"` // Originally field was `Relabel`"
+
+ // Propagation describes how the mounts are propagated from the host into the
+ // mount point, and vice-versa.
+ // See https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
+ // Note Propagation is not used on Windows
+ Propagation mounttypes.Propagation `json:",omitempty"` // Mount propagation string
+
+ // Specifies if data should be copied from the container before the first mount
+ // Use a pointer here so we can tell if the user set this value explicitly
+ // This allows us to error out when the user explicitly enabled copy but we can't copy due to the volume being populated
+ CopyData bool `json:"-"`
+ // ID is the opaque ID used to pass to the volume driver.
+ // This should be set by calls to `Mount` and unset by calls to `Unmount`
+ ID string `json:",omitempty"`
+
+ // Sepc is a copy of the API request that created this mount.
+ Spec mounttypes.Mount
+
+ // Track usage of this mountpoint
+ // Specifically needed for containers which are running and calls to `docker cp`
+ // because both these actions require mounting the volumes.
+ active int
+}
+
+// Cleanup frees resources used by the mountpoint
+func (m *MountPoint) Cleanup() error {
+ if m.Volume == nil || m.ID == "" {
+ return nil
+ }
+
+ if err := m.Volume.Unmount(m.ID); err != nil {
+ return errors.Wrapf(err, "error unmounting volume %s", m.Volume.Name())
+ }
+
+ m.active--
+ if m.active == 0 {
+ m.ID = ""
+ }
+ return nil
+}
+
+// Setup sets up a mount point by either mounting the volume if it is
+// configured, or creating the source directory if supplied.
+// The, optional, checkFun parameter allows doing additional checking
+// before creating the source directory on the host.
+func (m *MountPoint) Setup(mountLabel string, rootIDs idtools.IDPair, checkFun func(m *MountPoint) error) (path string, err error) {
+ defer func() {
+ if err != nil || !label.RelabelNeeded(m.Mode) {
+ return
+ }
+
+ var sourcePath string
+ sourcePath, err = filepath.EvalSymlinks(m.Source)
+ if err != nil {
+ path = ""
+ err = errors.Wrapf(err, "error evaluating symlinks from mount source %q", m.Source)
+ return
+ }
+ err = label.Relabel(sourcePath, mountLabel, label.IsShared(m.Mode))
+ if err == syscall.ENOTSUP {
+ err = nil
+ }
+ if err != nil {
+ path = ""
+ err = errors.Wrapf(err, "error setting label on mount source '%s'", sourcePath)
+ }
+ }()
+
+ if m.Volume != nil {
+ id := m.ID
+ if id == "" {
+ id = stringid.GenerateNonCryptoID()
+ }
+ path, err := m.Volume.Mount(id)
+ if err != nil {
+ return "", errors.Wrapf(err, "error while mounting volume '%s'", m.Source)
+ }
+
+ m.ID = id
+ m.active++
+ return path, nil
+ }
+
+ if len(m.Source) == 0 {
+ return "", fmt.Errorf("Unable to setup mount point, neither source nor volume defined")
+ }
+
+ if m.Type == mounttypes.TypeBind {
+ // Before creating the source directory on the host, invoke checkFun if it's not nil. One of
+ // the use case is to forbid creating the daemon socket as a directory if the daemon is in
+ // the process of shutting down.
+ if checkFun != nil {
+ if err := checkFun(m); err != nil {
+ return "", err
+ }
+ }
+ // idtools.MkdirAllNewAs() produces an error if m.Source exists and is a file (not a directory)
+ // also, makes sure that if the directory is created, the correct remapped rootUID/rootGID will own it
+ if err := idtools.MkdirAllAndChownNew(m.Source, 0755, rootIDs); err != nil {
+ if perr, ok := err.(*os.PathError); ok {
+ if perr.Err != syscall.ENOTDIR {
+ return "", errors.Wrapf(err, "error while creating mount source path '%s'", m.Source)
+ }
+ }
+ }
+ }
+ return m.Source, nil
+}
+
+// Path returns the path of a volume in a mount point.
+func (m *MountPoint) Path() string {
+ if m.Volume != nil {
+ return m.Volume.Path()
+ }
+ return m.Source
+}
+
+func errInvalidMode(mode string) error {
+ return errors.Errorf("invalid mode: %v", mode)
+}
+
+func errInvalidSpec(spec string) error {
+ return errors.Errorf("invalid volume specification: '%s'", spec)
+}
diff --git a/volume/mounts/parser.go b/volume/mounts/parser.go
new file mode 100644
index 0000000000..73681750ea
--- /dev/null
+++ b/volume/mounts/parser.go
@@ -0,0 +1,47 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "errors"
+ "runtime"
+
+ "github.com/docker/docker/api/types/mount"
+)
+
+const (
+ // OSLinux is the same as runtime.GOOS on linux
+ OSLinux = "linux"
+ // OSWindows is the same as runtime.GOOS on windows
+ OSWindows = "windows"
+)
+
+// ErrVolumeTargetIsRoot is returned when the target destination is root.
+// It's used by both LCOW and Linux parsers.
+var ErrVolumeTargetIsRoot = errors.New("invalid specification: destination can't be '/'")
+
+// Parser represents a platform specific parser for mount expressions
+type Parser interface {
+ ParseMountRaw(raw, volumeDriver string) (*MountPoint, error)
+ ParseMountSpec(cfg mount.Mount) (*MountPoint, error)
+ ParseVolumesFrom(spec string) (string, string, error)
+ DefaultPropagationMode() mount.Propagation
+ ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error)
+ DefaultCopyMode() bool
+ ValidateVolumeName(name string) error
+ ReadWrite(mode string) bool
+ IsBackwardCompatible(m *MountPoint) bool
+ HasResource(m *MountPoint, absPath string) bool
+ ValidateTmpfsMountDestination(dest string) error
+ ValidateMountConfig(mt *mount.Mount) error
+}
+
+// NewParser creates a parser for a given container OS, depending on the current host OS (linux on a windows host will resolve to an lcowParser)
+func NewParser(containerOS string) Parser {
+ switch containerOS {
+ case OSWindows:
+ return &windowsParser{}
+ }
+ if runtime.GOOS == OSWindows {
+ return &lcowParser{}
+ }
+ return &linuxParser{}
+}
diff --git a/volume/mounts/parser_test.go b/volume/mounts/parser_test.go
new file mode 100644
index 0000000000..347f7d9c4d
--- /dev/null
+++ b/volume/mounts/parser_test.go
@@ -0,0 +1,480 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "io/ioutil"
+ "os"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/api/types/mount"
+)
+
+type parseMountRawTestSet struct {
+ valid []string
+ invalid map[string]string
+}
+
+func TestConvertTmpfsOptions(t *testing.T) {
+ type testCase struct {
+ opt mount.TmpfsOptions
+ readOnly bool
+ expectedSubstrings []string
+ unexpectedSubstrings []string
+ }
+ cases := []testCase{
+ {
+ opt: mount.TmpfsOptions{SizeBytes: 1024 * 1024, Mode: 0700},
+ readOnly: false,
+ expectedSubstrings: []string{"size=1m", "mode=700"},
+ unexpectedSubstrings: []string{"ro"},
+ },
+ {
+ opt: mount.TmpfsOptions{},
+ readOnly: true,
+ expectedSubstrings: []string{"ro"},
+ unexpectedSubstrings: []string{},
+ },
+ }
+ p := &linuxParser{}
+ for _, c := range cases {
+ data, err := p.ConvertTmpfsOptions(&c.opt, c.readOnly)
+ if err != nil {
+ t.Fatalf("could not convert %+v (readOnly: %v) to string: %v",
+ c.opt, c.readOnly, err)
+ }
+ t.Logf("data=%q", data)
+ for _, s := range c.expectedSubstrings {
+ if !strings.Contains(data, s) {
+ t.Fatalf("expected substring: %s, got %v (case=%+v)", s, data, c)
+ }
+ }
+ for _, s := range c.unexpectedSubstrings {
+ if strings.Contains(data, s) {
+ t.Fatalf("unexpected substring: %s, got %v (case=%+v)", s, data, c)
+ }
+ }
+ }
+}
+
+type mockFiProvider struct{}
+
+func (mockFiProvider) fileInfo(path string) (exists, isDir bool, err error) {
+ dirs := map[string]struct{}{
+ `c:\`: {},
+ `c:\windows\`: {},
+ `c:\windows`: {},
+ `c:\program files`: {},
+ `c:\Windows`: {},
+ `c:\Program Files (x86)`: {},
+ `\\?\c:\windows\`: {},
+ }
+ files := map[string]struct{}{
+ `c:\windows\system32\ntdll.dll`: {},
+ }
+ if _, ok := dirs[path]; ok {
+ return true, true, nil
+ }
+ if _, ok := files[path]; ok {
+ return true, false, nil
+ }
+ return false, false, nil
+}
+
+func TestParseMountRaw(t *testing.T) {
+
+ previousProvider := currentFileInfoProvider
+ defer func() { currentFileInfoProvider = previousProvider }()
+ currentFileInfoProvider = mockFiProvider{}
+ windowsSet := parseMountRawTestSet{
+ valid: []string{
+ `d:\`,
+ `d:`,
+ `d:\path`,
+ `d:\path with space`,
+ `c:\:d:\`,
+ `c:\windows\:d:`,
+ `c:\windows:d:\s p a c e`,
+ `c:\windows:d:\s p a c e:RW`,
+ `c:\program files:d:\s p a c e i n h o s t d i r`,
+ `0123456789name:d:`,
+ `MiXeDcAsEnAmE:d:`,
+ `name:D:`,
+ `name:D::rW`,
+ `name:D::RW`,
+ `name:D::RO`,
+ `c:/:d:/forward/slashes/are/good/too`,
+ `c:/:d:/including with/spaces:ro`,
+ `c:\Windows`, // With capital
+ `c:\Program Files (x86)`, // With capitals and brackets
+ `\\?\c:\windows\:d:`, // Long path handling (source)
+ `c:\windows\:\\?\d:\`, // Long path handling (target)
+ `\\.\pipe\foo:\\.\pipe\foo`, // named pipe
+ `//./pipe/foo://./pipe/foo`, // named pipe forward slashes
+ },
+ invalid: map[string]string{
+ ``: "invalid volume specification: ",
+ `.`: "invalid volume specification: ",
+ `..\`: "invalid volume specification: ",
+ `c:\:..\`: "invalid volume specification: ",
+ `c:\:d:\:xyzzy`: "invalid volume specification: ",
+ `c:`: "cannot be `c:`",
+ `c:\`: "cannot be `c:`",
+ `c:\notexist:d:`: `bind mount source path does not exist: c:\notexist`,
+ `c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`,
+ `name<:d:`: `invalid volume specification`,
+ `name>:d:`: `invalid volume specification`,
+ `name::d:`: `invalid volume specification`,
+ `name":d:`: `invalid volume specification`,
+ `name\:d:`: `invalid volume specification`,
+ `name*:d:`: `invalid volume specification`,
+ `name|:d:`: `invalid volume specification`,
+ `name?:d:`: `invalid volume specification`,
+ `name/:d:`: `invalid volume specification`,
+ `d:\pathandmode:rw`: `invalid volume specification`,
+ `d:\pathandmode:ro`: `invalid volume specification`,
+ `con:d:`: `cannot be a reserved word for Windows filenames`,
+ `PRN:d:`: `cannot be a reserved word for Windows filenames`,
+ `aUx:d:`: `cannot be a reserved word for Windows filenames`,
+ `nul:d:`: `cannot be a reserved word for Windows filenames`,
+ `com1:d:`: `cannot be a reserved word for Windows filenames`,
+ `com2:d:`: `cannot be a reserved word for Windows filenames`,
+ `com3:d:`: `cannot be a reserved word for Windows filenames`,
+ `com4:d:`: `cannot be a reserved word for Windows filenames`,
+ `com5:d:`: `cannot be a reserved word for Windows filenames`,
+ `com6:d:`: `cannot be a reserved word for Windows filenames`,
+ `com7:d:`: `cannot be a reserved word for Windows filenames`,
+ `com8:d:`: `cannot be a reserved word for Windows filenames`,
+ `com9:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt1:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt2:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt3:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt4:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt5:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt6:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt7:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt8:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt9:d:`: `cannot be a reserved word for Windows filenames`,
+ `c:\windows\system32\ntdll.dll`: `Only directories can be mapped on this platform`,
+ `\\.\pipe\foo:c:\pipe`: `'c:\pipe' is not a valid pipe path`,
+ },
+ }
+ lcowSet := parseMountRawTestSet{
+ valid: []string{
+ `/foo`,
+ `/foo/`,
+ `/foo bar`,
+ `c:\:/foo`,
+ `c:\windows\:/foo`,
+ `c:\windows:/s p a c e`,
+ `c:\windows:/s p a c e:RW`,
+ `c:\program files:/s p a c e i n h o s t d i r`,
+ `0123456789name:/foo`,
+ `MiXeDcAsEnAmE:/foo`,
+ `name:/foo`,
+ `name:/foo:rW`,
+ `name:/foo:RW`,
+ `name:/foo:RO`,
+ `c:/:/forward/slashes/are/good/too`,
+ `c:/:/including with/spaces:ro`,
+ `/Program Files (x86)`, // With capitals and brackets
+ },
+ invalid: map[string]string{
+ ``: "invalid volume specification: ",
+ `.`: "invalid volume specification: ",
+ `c:`: "invalid volume specification: ",
+ `c:\`: "invalid volume specification: ",
+ `../`: "invalid volume specification: ",
+ `c:\:../`: "invalid volume specification: ",
+ `c:\:/foo:xyzzy`: "invalid volume specification: ",
+ `/`: "destination can't be '/'",
+ `/..`: "destination can't be '/'",
+ `c:\notexist:/foo`: `bind mount source path does not exist: c:\notexist`,
+ `c:\windows\system32\ntdll.dll:/foo`: `source path must be a directory`,
+ `name<:/foo`: `invalid volume specification`,
+ `name>:/foo`: `invalid volume specification`,
+ `name::/foo`: `invalid volume specification`,
+ `name":/foo`: `invalid volume specification`,
+ `name\:/foo`: `invalid volume specification`,
+ `name*:/foo`: `invalid volume specification`,
+ `name|:/foo`: `invalid volume specification`,
+ `name?:/foo`: `invalid volume specification`,
+ `name/:/foo`: `invalid volume specification`,
+ `/foo:rw`: `invalid volume specification`,
+ `/foo:ro`: `invalid volume specification`,
+ `con:/foo`: `cannot be a reserved word for Windows filenames`,
+ `PRN:/foo`: `cannot be a reserved word for Windows filenames`,
+ `aUx:/foo`: `cannot be a reserved word for Windows filenames`,
+ `nul:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com1:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com2:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com3:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com4:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com5:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com6:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com7:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com8:/foo`: `cannot be a reserved word for Windows filenames`,
+ `com9:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt1:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt2:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt3:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt4:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt5:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt6:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt7:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt8:/foo`: `cannot be a reserved word for Windows filenames`,
+ `lpt9:/foo`: `cannot be a reserved word for Windows filenames`,
+ `\\.\pipe\foo:/foo`: `Linux containers on Windows do not support named pipe mounts`,
+ },
+ }
+ linuxSet := parseMountRawTestSet{
+ valid: []string{
+ "/home",
+ "/home:/home",
+ "/home:/something/else",
+ "/with space",
+ "/home:/with space",
+ "relative:/absolute-path",
+ "hostPath:/containerPath:ro",
+ "/hostPath:/containerPath:rw",
+ "/rw:/ro",
+ "/hostPath:/containerPath:shared",
+ "/hostPath:/containerPath:rshared",
+ "/hostPath:/containerPath:slave",
+ "/hostPath:/containerPath:rslave",
+ "/hostPath:/containerPath:private",
+ "/hostPath:/containerPath:rprivate",
+ "/hostPath:/containerPath:ro,shared",
+ "/hostPath:/containerPath:ro,slave",
+ "/hostPath:/containerPath:ro,private",
+ "/hostPath:/containerPath:ro,z,shared",
+ "/hostPath:/containerPath:ro,Z,slave",
+ "/hostPath:/containerPath:Z,ro,slave",
+ "/hostPath:/containerPath:slave,Z,ro",
+ "/hostPath:/containerPath:Z,slave,ro",
+ "/hostPath:/containerPath:slave,ro,Z",
+ "/hostPath:/containerPath:rslave,ro,Z",
+ "/hostPath:/containerPath:ro,rshared,Z",
+ "/hostPath:/containerPath:ro,Z,rprivate",
+ },
+ invalid: map[string]string{
+ "": "invalid volume specification",
+ "./": "mount path must be absolute",
+ "../": "mount path must be absolute",
+ "/:../": "mount path must be absolute",
+ "/:path": "mount path must be absolute",
+ ":": "invalid volume specification",
+ "/tmp:": "invalid volume specification",
+ ":test": "invalid volume specification",
+ ":/test": "invalid volume specification",
+ "tmp:": "invalid volume specification",
+ ":test:": "invalid volume specification",
+ "::": "invalid volume specification",
+ ":::": "invalid volume specification",
+ "/tmp:::": "invalid volume specification",
+ ":/tmp::": "invalid volume specification",
+ "/path:rw": "invalid volume specification",
+ "/path:ro": "invalid volume specification",
+ "/rw:rw": "invalid volume specification",
+ "path:ro": "invalid volume specification",
+ "/path:/path:sw": `invalid mode`,
+ "/path:/path:rwz": `invalid mode`,
+ "/path:/path:ro,rshared,rslave": `invalid mode`,
+ "/path:/path:ro,z,rshared,rslave": `invalid mode`,
+ "/path:shared": "invalid volume specification",
+ "/path:slave": "invalid volume specification",
+ "/path:private": "invalid volume specification",
+ "name:/absolute-path:shared": "invalid volume specification",
+ "name:/absolute-path:rshared": "invalid volume specification",
+ "name:/absolute-path:slave": "invalid volume specification",
+ "name:/absolute-path:rslave": "invalid volume specification",
+ "name:/absolute-path:private": "invalid volume specification",
+ "name:/absolute-path:rprivate": "invalid volume specification",
+ },
+ }
+
+ linParser := &linuxParser{}
+ winParser := &windowsParser{}
+ lcowParser := &lcowParser{}
+ tester := func(parser Parser, set parseMountRawTestSet) {
+
+ for _, path := range set.valid {
+
+ if _, err := parser.ParseMountRaw(path, "local"); err != nil {
+ t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
+ }
+ }
+
+ for path, expectedError := range set.invalid {
+ if mp, err := parser.ParseMountRaw(path, "local"); err == nil {
+ t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
+ } else {
+ if !strings.Contains(err.Error(), expectedError) {
+ t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
+ }
+ }
+ }
+ }
+ tester(linParser, linuxSet)
+ tester(winParser, windowsSet)
+ tester(lcowParser, lcowSet)
+
+}
+
+// testParseMountRaw is a structure used by TestParseMountRawSplit for
+// specifying test cases for the ParseMountRaw() function.
+type testParseMountRaw struct {
+ bind string
+ driver string
+ expType mount.Type
+ expDest string
+ expSource string
+ expName string
+ expDriver string
+ expRW bool
+ fail bool
+}
+
+func TestParseMountRawSplit(t *testing.T) {
+ previousProvider := currentFileInfoProvider
+ defer func() { currentFileInfoProvider = previousProvider }()
+ currentFileInfoProvider = mockFiProvider{}
+ windowsCases := []testParseMountRaw{
+ {`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},
+ {`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},
+ {`\\.\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},
+ }
+ lcowCases := []testParseMountRaw{
+ {`c:\:/foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false},
+ {`c:\:/foo:ro`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, false},
+ {`c:\:/foo:rw`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false},
+ {`c:\:/foo:foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, true},
+ {`name:/foo:rw`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false},
+ {`name:/foo`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false},
+ {`name:/foo:ro`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", false, false},
+ {`name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
+ {`driver/name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
+ {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, true},
+ {`\\.\pipe\foo:/data`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
+ {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
+ }
+ linuxCases := []testParseMountRaw{
+ {"/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},
+ }
+ linParser := &linuxParser{}
+ winParser := &windowsParser{}
+ lcowParser := &lcowParser{}
+ tester := func(parser Parser, cases []testParseMountRaw) {
+ for i, c := range cases {
+ t.Logf("case %d", i)
+ m, err := parser.ParseMountRaw(c.bind, c.driver)
+ if c.fail {
+ if err == nil {
+ t.Errorf("Expected error, was nil, for spec %s\n", c.bind)
+ }
+ continue
+ }
+
+ if m == nil || err != nil {
+ t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err.Error())
+ continue
+ }
+
+ if m.Destination != c.expDest {
+ t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
+ }
+
+ if m.Source != c.expSource {
+ t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind)
+ }
+
+ if m.Name != c.expName {
+ t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind)
+ }
+
+ if m.Driver != c.expDriver {
+ t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind)
+ }
+
+ if m.RW != c.expRW {
+ t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind)
+ }
+ if m.Type != c.expType {
+ t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind)
+ }
+ }
+ }
+
+ tester(linParser, linuxCases)
+ tester(winParser, windowsCases)
+ tester(lcowParser, lcowCases)
+}
+
+func TestParseMountSpec(t *testing.T) {
+ type c struct {
+ input mount.Mount
+ expected MountPoint
+ }
+ testDir, err := ioutil.TempDir("", "test-mount-config")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(testDir)
+ parser := NewParser(runtime.GOOS)
+ cases := []c{
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true, Propagation: parser.DefaultPropagationMode()}},
+ {mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath + string(os.PathSeparator), ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
+ {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: parser.DefaultCopyMode()}},
+ {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath + string(os.PathSeparator)}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: parser.DefaultCopyMode()}},
+ }
+
+ for i, c := range cases {
+ t.Logf("case %d", i)
+ mp, err := parser.ParseMountSpec(c.input)
+ if err != nil {
+ t.Error(err)
+ }
+
+ if c.expected.Type != mp.Type {
+ t.Errorf("Expected mount types to match. Expected: '%s', Actual: '%s'", c.expected.Type, mp.Type)
+ }
+ if c.expected.Destination != mp.Destination {
+ t.Errorf("Expected mount destination to match. Expected: '%s', Actual: '%s'", c.expected.Destination, mp.Destination)
+ }
+ if c.expected.Source != mp.Source {
+ t.Errorf("Expected mount source to match. Expected: '%s', Actual: '%s'", c.expected.Source, mp.Source)
+ }
+ if c.expected.RW != mp.RW {
+ t.Errorf("Expected mount writable to match. Expected: '%v', Actual: '%v'", c.expected.RW, mp.RW)
+ }
+ if c.expected.Propagation != mp.Propagation {
+ t.Errorf("Expected mount propagation to match. Expected: '%v', Actual: '%s'", c.expected.Propagation, mp.Propagation)
+ }
+ if c.expected.Driver != mp.Driver {
+ t.Errorf("Expected mount driver to match. Expected: '%v', Actual: '%s'", c.expected.Driver, mp.Driver)
+ }
+ if c.expected.CopyData != mp.CopyData {
+ t.Errorf("Expected mount copy data to match. Expected: '%v', Actual: '%v'", c.expected.CopyData, mp.CopyData)
+ }
+ }
+}
diff --git a/volume/mounts/validate.go b/volume/mounts/validate.go
new file mode 100644
index 0000000000..0b71526901
--- /dev/null
+++ b/volume/mounts/validate.go
@@ -0,0 +1,28 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "fmt"
+
+ "github.com/docker/docker/api/types/mount"
+ "github.com/pkg/errors"
+)
+
+type errMountConfig struct {
+ mount *mount.Mount
+ err error
+}
+
+func (e *errMountConfig) Error() string {
+ return fmt.Sprintf("invalid mount config for type %q: %v", e.mount.Type, e.err.Error())
+}
+
+func errBindSourceDoesNotExist(path string) error {
+ return errors.Errorf("bind mount source path does not exist: %s", path)
+}
+
+func errExtraField(name string) error {
+ return errors.Errorf("field %s must not be specified", name)
+}
+func errMissingField(name string) error {
+ return errors.Errorf("field %s must not be empty", name)
+}
diff --git a/volume/mounts/validate_test.go b/volume/mounts/validate_test.go
new file mode 100644
index 0000000000..4f83856043
--- /dev/null
+++ b/volume/mounts/validate_test.go
@@ -0,0 +1,73 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/api/types/mount"
+)
+
+func TestValidateMount(t *testing.T) {
+ testDir, err := ioutil.TempDir("", "test-validate-mount")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(testDir)
+
+ cases := []struct {
+ input mount.Mount
+ expected error
+ }{
+ {mount.Mount{Type: mount.TypeVolume}, errMissingField("Target")},
+ {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath, Source: "hello"}, nil},
+ {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, nil},
+ {mount.Mount{Type: mount.TypeBind}, errMissingField("Target")},
+ {mount.Mount{Type: mount.TypeBind, Target: testDestinationPath}, errMissingField("Source")},
+ {mount.Mount{Type: mount.TypeBind, Target: testDestinationPath, Source: testSourcePath, VolumeOptions: &mount.VolumeOptions{}}, errExtraField("VolumeOptions")},
+
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, nil},
+ {mount.Mount{Type: "invalid", Target: testDestinationPath}, errors.New("mount type unknown")},
+ {mount.Mount{Type: mount.TypeBind, Source: testSourcePath, Target: testDestinationPath}, errBindSourceDoesNotExist(testSourcePath)},
+ }
+
+ lcowCases := []struct {
+ input mount.Mount
+ expected error
+ }{
+ {mount.Mount{Type: mount.TypeVolume}, errMissingField("Target")},
+ {mount.Mount{Type: mount.TypeVolume, Target: "/foo", Source: "hello"}, nil},
+ {mount.Mount{Type: mount.TypeVolume, Target: "/foo"}, nil},
+ {mount.Mount{Type: mount.TypeBind}, errMissingField("Target")},
+ {mount.Mount{Type: mount.TypeBind, Target: "/foo"}, errMissingField("Source")},
+ {mount.Mount{Type: mount.TypeBind, Target: "/foo", Source: "c:\\foo", VolumeOptions: &mount.VolumeOptions{}}, errExtraField("VolumeOptions")},
+ {mount.Mount{Type: mount.TypeBind, Source: "c:\\foo", Target: "/foo"}, errBindSourceDoesNotExist("c:\\foo")},
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: "/foo"}, nil},
+ {mount.Mount{Type: "invalid", Target: "/foo"}, errors.New("mount type unknown")},
+ }
+ parser := NewParser(runtime.GOOS)
+ for i, x := range cases {
+ err := parser.ValidateMountConfig(&x.input)
+ if err == nil && x.expected == nil {
+ continue
+ }
+ if (err == nil && x.expected != nil) || (x.expected == nil && err != nil) || !strings.Contains(err.Error(), x.expected.Error()) {
+ t.Errorf("expected %q, got %q, case: %d", x.expected, err, i)
+ }
+ }
+ if runtime.GOOS == "windows" {
+ parser = &lcowParser{}
+ for i, x := range lcowCases {
+ err := parser.ValidateMountConfig(&x.input)
+ if err == nil && x.expected == nil {
+ continue
+ }
+ if (err == nil && x.expected != nil) || (x.expected == nil && err != nil) || !strings.Contains(err.Error(), x.expected.Error()) {
+ t.Errorf("expected %q, got %q, case: %d", x.expected, err, i)
+ }
+ }
+ }
+}
diff --git a/volume/mounts/validate_unix_test.go b/volume/mounts/validate_unix_test.go
new file mode 100644
index 0000000000..a319371451
--- /dev/null
+++ b/volume/mounts/validate_unix_test.go
@@ -0,0 +1,8 @@
+// +build !windows
+
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+var (
+ testDestinationPath = "/foo"
+ testSourcePath = "/foo"
+)
diff --git a/volume/mounts/validate_windows_test.go b/volume/mounts/validate_windows_test.go
new file mode 100644
index 0000000000..74b40a6c30
--- /dev/null
+++ b/volume/mounts/validate_windows_test.go
@@ -0,0 +1,6 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+var (
+ testDestinationPath = `c:\foo`
+ testSourcePath = `c:\foo`
+)
diff --git a/volume/mounts/volume_copy.go b/volume/mounts/volume_copy.go
new file mode 100644
index 0000000000..04056fa50a
--- /dev/null
+++ b/volume/mounts/volume_copy.go
@@ -0,0 +1,23 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import "strings"
+
+// {<copy mode>=isEnabled}
+var copyModes = map[string]bool{
+ "nocopy": false,
+}
+
+func copyModeExists(mode string) bool {
+ _, exists := copyModes[mode]
+ return exists
+}
+
+// GetCopyMode gets the copy mode from the mode string for mounts
+func getCopyMode(mode string, def bool) (bool, bool) {
+ for _, o := range strings.Split(mode, ",") {
+ if isEnabled, exists := copyModes[o]; exists {
+ return isEnabled, true
+ }
+ }
+ return def, false
+}
diff --git a/volume/mounts/volume_unix.go b/volume/mounts/volume_unix.go
new file mode 100644
index 0000000000..c6d51e0710
--- /dev/null
+++ b/volume/mounts/volume_unix.go
@@ -0,0 +1,18 @@
+// +build linux freebsd darwin
+
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+)
+
+func (p *linuxParser) HasResource(m *MountPoint, absolutePath string) bool {
+ relPath, err := filepath.Rel(m.Destination, absolutePath)
+ return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
+}
+
+func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool {
+ return false
+}
diff --git a/volume/mounts/volume_windows.go b/volume/mounts/volume_windows.go
new file mode 100644
index 0000000000..773e7db88a
--- /dev/null
+++ b/volume/mounts/volume_windows.go
@@ -0,0 +1,8 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool {
+ return false
+}
+func (p *linuxParser) HasResource(m *MountPoint, absolutePath string) bool {
+ return false
+}
diff --git a/volume/mounts/windows_parser.go b/volume/mounts/windows_parser.go
new file mode 100644
index 0000000000..ac61044043
--- /dev/null
+++ b/volume/mounts/windows_parser.go
@@ -0,0 +1,456 @@
+package mounts // import "github.com/docker/docker/volume/mounts"
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "regexp"
+ "runtime"
+ "strings"
+
+ "github.com/docker/docker/api/types/mount"
+ "github.com/docker/docker/pkg/stringid"
+)
+
+type windowsParser struct {
+}
+
+const (
+ // Spec should be in the format [source:]destination[:mode]
+ //
+ // Examples: c:\foo bar:d:rw
+ // c:\foo:d:\bar
+ // myname:d:
+ // d:\
+ //
+ // Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See
+ // https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to
+ // test is https://regex-golang.appspot.com/assets/html/index.html
+ //
+ // Useful link for referencing named capturing groups:
+ // http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex
+ //
+ // There are three match groups: source, destination and mode.
+ //
+
+ // rxHostDir is the first option of a source
+ rxHostDir = `(?:\\\\\?\\)?[a-z]:[\\/](?:[^\\/:*?"<>|\r\n]+[\\/]?)*`
+ // rxName is the second option of a source
+ rxName = `[^\\/:*?"<>|\r\n]+`
+
+ // RXReservedNames are reserved names not possible on Windows
+ rxReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])`
+
+ // rxPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \)
+ rxPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+`
+ // rxSource is the combined possibilities for a source
+ rxSource = `((?P<source>((` + rxHostDir + `)|(` + rxName + `)|(` + rxPipe + `))):)?`
+
+ // Source. Can be either a host directory, a name, or omitted:
+ // HostDir:
+ // - Essentially using the folder solution from
+ // https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
+ // but adding case insensitivity.
+ // - Must be an absolute path such as c:\path
+ // - Can include spaces such as `c:\program files`
+ // - And then followed by a colon which is not in the capture group
+ // - And can be optional
+ // Name:
+ // - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
+ // - And then followed by a colon which is not in the capture group
+ // - And can be optional
+
+ // rxDestination is the regex expression for the mount destination
+ rxDestination = `(?P<destination>((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))`
+
+ rxLCOWDestination = `(?P<destination>/(?:[^\\/:*?"<>\r\n]+[/]?)*)`
+ // 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
+ // - Drive cannot be c: (explicitly checked in code, not RegEx)
+
+ // rxMode is the regex expression for the mode of the mount
+ // Mode (optional):
+ // - Hopefully self explanatory in comparison to above regex's.
+ // - Colon is not in the capture group
+ rxMode = `(:(?P<mode>(?i)ro|rw))?`
+)
+
+type mountValidator func(mnt *mount.Mount) error
+
+func windowsSplitRawSpec(raw, destRegex string) ([]string, error) {
+ specExp := regexp.MustCompile(`^` + rxSource + destRegex + rxMode + `$`)
+ match := specExp.FindStringSubmatch(strings.ToLower(raw))
+
+ // Must have something back
+ if len(match) == 0 {
+ return nil, errInvalidSpec(raw)
+ }
+
+ var split []string
+ matchgroups := make(map[string]string)
+ // Pull out the sub expressions from the named capture groups
+ for i, name := range specExp.SubexpNames() {
+ matchgroups[name] = strings.ToLower(match[i])
+ }
+ if source, exists := matchgroups["source"]; exists {
+ if source != "" {
+ split = append(split, source)
+ }
+ }
+ if destination, exists := matchgroups["destination"]; exists {
+ if destination != "" {
+ split = append(split, destination)
+ }
+ }
+ if mode, exists := matchgroups["mode"]; exists {
+ if mode != "" {
+ split = append(split, mode)
+ }
+ }
+ // Fix #26329. If the destination appears to be a file, and the source is null,
+ // it may be because we've fallen through the possible naming regex and hit a
+ // situation where the user intention was to map a file into a container through
+ // a local volume, but this is not supported by the platform.
+ if matchgroups["source"] == "" && matchgroups["destination"] != "" {
+ volExp := regexp.MustCompile(`^` + rxName + `$`)
+ reservedNameExp := regexp.MustCompile(`^` + rxReservedNames + `$`)
+
+ if volExp.MatchString(matchgroups["destination"]) {
+ if reservedNameExp.MatchString(matchgroups["destination"]) {
+ return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"])
+ }
+ } else {
+
+ exists, isDir, _ := currentFileInfoProvider.fileInfo(matchgroups["destination"])
+ if exists && !isDir {
+ return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
+
+ }
+ }
+ }
+ return split, nil
+}
+
+func windowsValidMountMode(mode string) bool {
+ if mode == "" {
+ return true
+ }
+ return rwModes[strings.ToLower(mode)]
+}
+func windowsValidateNotRoot(p string) error {
+ p = strings.ToLower(strings.Replace(p, `/`, `\`, -1))
+ if p == "c:" || p == `c:\` {
+ return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
+ }
+ return nil
+}
+
+var windowsSpecificValidators mountValidator = func(mnt *mount.Mount) error {
+ return windowsValidateNotRoot(mnt.Target)
+}
+
+func windowsValidateRegex(p, r string) error {
+ if regexp.MustCompile(`^` + r + `$`).MatchString(strings.ToLower(p)) {
+ return nil
+ }
+ return fmt.Errorf("invalid mount path: '%s'", p)
+}
+func windowsValidateAbsolute(p string) error {
+ if err := windowsValidateRegex(p, rxDestination); err != nil {
+ return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
+ }
+ return nil
+}
+
+func windowsDetectMountType(p string) mount.Type {
+ if strings.HasPrefix(p, `\\.\pipe\`) {
+ return mount.TypeNamedPipe
+ } else if regexp.MustCompile(`^` + rxHostDir + `$`).MatchString(p) {
+ return mount.TypeBind
+ } else {
+ return mount.TypeVolume
+ }
+}
+
+func (p *windowsParser) ReadWrite(mode string) bool {
+ return strings.ToLower(mode) != "ro"
+}
+
+// IsVolumeNameValid checks a volume name in a platform specific manner.
+func (p *windowsParser) ValidateVolumeName(name string) error {
+ nameExp := regexp.MustCompile(`^` + rxName + `$`)
+ if !nameExp.MatchString(name) {
+ return errors.New("invalid volume name")
+ }
+ nameExp = regexp.MustCompile(`^` + rxReservedNames + `$`)
+ if nameExp.MatchString(name) {
+ return fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
+ }
+ return nil
+}
+func (p *windowsParser) ValidateMountConfig(mnt *mount.Mount) error {
+ return p.validateMountConfigReg(mnt, rxDestination, windowsSpecificValidators)
+}
+
+type fileInfoProvider interface {
+ fileInfo(path string) (exist, isDir bool, err error)
+}
+
+type defaultFileInfoProvider struct {
+}
+
+func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) {
+ fi, err := os.Stat(path)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return false, false, err
+ }
+ return false, false, nil
+ }
+ return true, fi.IsDir(), nil
+}
+
+var currentFileInfoProvider fileInfoProvider = defaultFileInfoProvider{}
+
+func (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, destRegex string, additionalValidators ...mountValidator) error {
+
+ for _, v := range additionalValidators {
+ if err := v(mnt); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+ }
+ if len(mnt.Target) == 0 {
+ return &errMountConfig{mnt, errMissingField("Target")}
+ }
+
+ if err := windowsValidateRegex(mnt.Target, destRegex); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+
+ switch mnt.Type {
+ case mount.TypeBind:
+ if len(mnt.Source) == 0 {
+ return &errMountConfig{mnt, errMissingField("Source")}
+ }
+ // Don't error out just because the propagation mode is not supported on the platform
+ if opts := mnt.BindOptions; opts != nil {
+ if len(opts.Propagation) > 0 {
+ return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
+ }
+ }
+ if mnt.VolumeOptions != nil {
+ return &errMountConfig{mnt, errExtraField("VolumeOptions")}
+ }
+
+ if err := windowsValidateAbsolute(mnt.Source); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+
+ exists, isdir, err := currentFileInfoProvider.fileInfo(mnt.Source)
+ if err != nil {
+ return &errMountConfig{mnt, err}
+ }
+ if !exists {
+ return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)}
+ }
+ if !isdir {
+ return &errMountConfig{mnt, fmt.Errorf("source path must be a directory")}
+ }
+
+ case mount.TypeVolume:
+ if mnt.BindOptions != nil {
+ return &errMountConfig{mnt, errExtraField("BindOptions")}
+ }
+
+ if len(mnt.Source) == 0 && mnt.ReadOnly {
+ return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
+ }
+
+ if len(mnt.Source) != 0 {
+ if err := p.ValidateVolumeName(mnt.Source); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+ }
+ case mount.TypeNamedPipe:
+ 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 windowsDetectMountType(mnt.Source) != mount.TypeNamedPipe {
+ return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)}
+ }
+
+ if windowsDetectMountType(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")}
+ }
+ return nil
+}
+func (p *windowsParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
+ return p.parseMountRaw(raw, volumeDriver, rxDestination, true, windowsSpecificValidators)
+}
+
+func (p *windowsParser) parseMountRaw(raw, volumeDriver, destRegex string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
+ arr, err := windowsSplitRawSpec(raw, destRegex)
+ if err != nil {
+ return nil, err
+ }
+
+ var spec mount.Mount
+ var mode string
+ switch len(arr) {
+ case 1:
+ // Just a destination path in the container
+ spec.Target = arr[0]
+ case 2:
+ if windowsValidMountMode(arr[1]) {
+ // Destination + Mode is not a valid volume - volumes
+ // cannot include a mode. e.g. /foo:rw
+ return nil, errInvalidSpec(raw)
+ }
+ // Host Source Path or Name + Destination
+ spec.Source = strings.Replace(arr[0], `/`, `\`, -1)
+ spec.Target = arr[1]
+ case 3:
+ // HostSourcePath+DestinationPath+Mode
+ spec.Source = strings.Replace(arr[0], `/`, `\`, -1)
+ spec.Target = arr[1]
+ mode = arr[2]
+ default:
+ return nil, errInvalidSpec(raw)
+ }
+ if convertTargetToBackslash {
+ spec.Target = strings.Replace(spec.Target, `/`, `\`, -1)
+ }
+
+ if !windowsValidMountMode(mode) {
+ return nil, errInvalidMode(mode)
+ }
+
+ spec.Type = windowsDetectMountType(spec.Source)
+ spec.ReadOnly = !p.ReadWrite(mode)
+
+ // cannot assume that if a volume driver is passed in that we should set it
+ if volumeDriver != "" && spec.Type == mount.TypeVolume {
+ spec.VolumeOptions = &mount.VolumeOptions{
+ DriverConfig: &mount.Driver{Name: volumeDriver},
+ }
+ }
+
+ if copyData, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
+ if spec.VolumeOptions == nil {
+ spec.VolumeOptions = &mount.VolumeOptions{}
+ }
+ spec.VolumeOptions.NoCopy = !copyData
+ }
+
+ mp, err := p.parseMountSpec(spec, destRegex, convertTargetToBackslash, additionalValidators...)
+ if mp != nil {
+ mp.Mode = mode
+ }
+ if err != nil {
+ err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
+ }
+ return mp, err
+}
+
+func (p *windowsParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
+ return p.parseMountSpec(cfg, rxDestination, true, windowsSpecificValidators)
+}
+func (p *windowsParser) parseMountSpec(cfg mount.Mount, destRegex string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
+ if err := p.validateMountConfigReg(&cfg, destRegex, additionalValidators...); err != nil {
+ return nil, err
+ }
+ mp := &MountPoint{
+ RW: !cfg.ReadOnly,
+ Destination: cfg.Target,
+ Type: cfg.Type,
+ Spec: cfg,
+ }
+ if convertTargetToBackslash {
+ mp.Destination = strings.Replace(cfg.Target, `/`, `\`, -1)
+ }
+
+ switch cfg.Type {
+ case mount.TypeVolume:
+ if cfg.Source == "" {
+ mp.Name = stringid.GenerateNonCryptoID()
+ } else {
+ mp.Name = cfg.Source
+ }
+ mp.CopyData = p.DefaultCopyMode()
+
+ if cfg.VolumeOptions != nil {
+ if cfg.VolumeOptions.DriverConfig != nil {
+ mp.Driver = cfg.VolumeOptions.DriverConfig.Name
+ }
+ if cfg.VolumeOptions.NoCopy {
+ mp.CopyData = false
+ }
+ }
+ case mount.TypeBind:
+ mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1)
+ case mount.TypeNamedPipe:
+ mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1)
+ }
+ // cleanup trailing `\` except for paths like `c:\`
+ if len(mp.Source) > 3 && mp.Source[len(mp.Source)-1] == '\\' {
+ mp.Source = mp.Source[:len(mp.Source)-1]
+ }
+ if len(mp.Destination) > 3 && mp.Destination[len(mp.Destination)-1] == '\\' {
+ mp.Destination = mp.Destination[:len(mp.Destination)-1]
+ }
+ return mp, nil
+}
+
+func (p *windowsParser) ParseVolumesFrom(spec string) (string, string, error) {
+ if len(spec) == 0 {
+ return "", "", fmt.Errorf("volumes-from specification cannot be an empty string")
+ }
+
+ specParts := strings.SplitN(spec, ":", 2)
+ id := specParts[0]
+ mode := "rw"
+
+ if len(specParts) == 2 {
+ mode = specParts[1]
+ if !windowsValidMountMode(mode) {
+ return "", "", errInvalidMode(mode)
+ }
+
+ // Do not allow copy modes on volumes-from
+ if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
+ return "", "", errInvalidMode(mode)
+ }
+ }
+ return id, mode, nil
+}
+
+func (p *windowsParser) DefaultPropagationMode() mount.Propagation {
+ return mount.Propagation("")
+}
+
+func (p *windowsParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) {
+ return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS)
+}
+func (p *windowsParser) DefaultCopyMode() bool {
+ return false
+}
+func (p *windowsParser) IsBackwardCompatible(m *MountPoint) bool {
+ return false
+}
+
+func (p *windowsParser) ValidateTmpfsMountDestination(dest string) error {
+ return errors.New("Platform does not support tmpfs")
+}