From e89b6e8c2d2c36c43f22aeaf2a885646c2994051 Mon Sep 17 00:00:00 2001 From: Simon Ferquel Date: Tue, 1 Aug 2017 19:32:44 +0200 Subject: [PATCH] Volume refactoring for LCOW Signed-off-by: Simon Ferquel --- container/container.go | 7 +- container/container_unix.go | 13 +- daemon/archive_unix.go | 4 +- daemon/create_windows.go | 4 +- daemon/daemon_unix.go | 3 +- daemon/oci_linux.go | 4 +- daemon/oci_windows.go | 6 + daemon/volumes.go | 17 +- daemon/volumes_unit_test.go | 5 +- libcontainerd/client_windows.go | 91 +++++ runconfig/config.go | 27 -- runconfig/config_test.go | 183 --------- volume/lcow_parser.go | 35 ++ volume/linux_parser.go | 416 +++++++++++++++++++++ volume/parser.go | 43 +++ volume/store/errors.go | 9 - volume/store/store.go | 10 +- volume/validate.go | 150 -------- volume/validate_test.go | 40 +- volume/volume.go | 148 +------- volume/volume_copy.go | 4 +- volume/volume_copy_unix.go | 8 - volume/volume_copy_windows.go | 6 - volume/volume_linux.go | 56 --- volume/volume_linux_test.go | 51 --- volume/volume_propagation_linux.go | 47 --- volume/volume_propagation_linux_test.go | 65 ---- volume/volume_propagation_unsupported.go | 24 -- volume/volume_test.go | 455 +++++++++++++++------- volume/volume_unix.go | 144 +------ volume/volume_unsupported.go | 16 - volume/volume_windows.go | 211 +---------- volume/windows_parser.go | 456 +++++++++++++++++++++++ 33 files changed, 1467 insertions(+), 1291 deletions(-) create mode 100644 volume/lcow_parser.go create mode 100644 volume/linux_parser.go create mode 100644 volume/parser.go delete mode 100644 volume/volume_copy_unix.go delete mode 100644 volume/volume_copy_windows.go delete mode 100644 volume/volume_linux.go delete mode 100644 volume/volume_linux_test.go delete mode 100644 volume/volume_propagation_linux.go delete mode 100644 volume/volume_propagation_linux_test.go delete mode 100644 volume/volume_propagation_unsupported.go delete mode 100644 volume/volume_unsupported.go create mode 100644 volume/windows_parser.go diff --git a/container/container.go b/container/container.go index 188c017cf9..49120ddeb4 100644 --- a/container/container.go +++ b/container/container.go @@ -435,6 +435,11 @@ func (container *Container) ShouldRestart() bool { // AddMountPointWithVolume adds a new mount point configured with a volume to the container. func (container *Container) AddMountPointWithVolume(destination string, vol volume.Volume, rw bool) { + operatingSystem := container.Platform + if operatingSystem == "" { + operatingSystem = runtime.GOOS + } + volumeParser := volume.NewParser(operatingSystem) container.MountPoints[destination] = &volume.MountPoint{ Type: mounttypes.TypeVolume, Name: vol.Name(), @@ -442,7 +447,7 @@ func (container *Container) AddMountPointWithVolume(destination string, vol volu Destination: destination, RW: rw, Volume: vol, - CopyData: volume.DefaultCopyMode, + CopyData: volumeParser.DefaultCopyMode(), } } diff --git a/container/container_unix.go b/container/container_unix.go index 8212cb9d7c..89e3c2ecc9 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -68,6 +68,7 @@ func (container *Container) BuildHostnameFile() error { func (container *Container) NetworkMounts() []Mount { var mounts []Mount shared := container.HostConfig.NetworkMode.IsContainer() + parser := volume.NewParser(container.Platform) if container.ResolvConfPath != "" { if _, err := os.Stat(container.ResolvConfPath); err != nil { logrus.Warnf("ResolvConfPath set to %q, but can't stat this filename (err = %v); skipping", container.ResolvConfPath, err) @@ -83,7 +84,7 @@ func (container *Container) NetworkMounts() []Mount { Source: container.ResolvConfPath, Destination: "/etc/resolv.conf", Writable: writable, - Propagation: string(volume.DefaultPropagationMode), + Propagation: string(parser.DefaultPropagationMode()), }) } } @@ -102,7 +103,7 @@ func (container *Container) NetworkMounts() []Mount { Source: container.HostnamePath, Destination: "/etc/hostname", Writable: writable, - Propagation: string(volume.DefaultPropagationMode), + Propagation: string(parser.DefaultPropagationMode()), }) } } @@ -121,7 +122,7 @@ func (container *Container) NetworkMounts() []Mount { Source: container.HostsPath, Destination: "/etc/hosts", Writable: writable, - Propagation: string(volume.DefaultPropagationMode), + Propagation: string(parser.DefaultPropagationMode()), }) } } @@ -196,6 +197,7 @@ func (container *Container) UnmountIpcMount(unmount func(pth string) error) erro // IpcMounts returns the list of IPC mounts func (container *Container) IpcMounts() []Mount { var mounts []Mount + parser := volume.NewParser(container.Platform) if container.HasMountFor("/dev/shm") { return mounts @@ -209,7 +211,7 @@ func (container *Container) IpcMounts() []Mount { Source: container.ShmPath, Destination: "/dev/shm", Writable: true, - Propagation: string(volume.DefaultPropagationMode), + Propagation: string(parser.DefaultPropagationMode()), }) return mounts @@ -429,6 +431,7 @@ func copyOwnership(source, destination string) error { // TmpfsMounts returns the list of tmpfs mounts func (container *Container) TmpfsMounts() ([]Mount, error) { + parser := volume.NewParser(container.Platform) var mounts []Mount for dest, data := range container.HostConfig.Tmpfs { mounts = append(mounts, Mount{ @@ -439,7 +442,7 @@ func (container *Container) TmpfsMounts() ([]Mount, error) { } for dest, mnt := range container.MountPoints { if mnt.Type == mounttypes.TypeTmpfs { - data, err := volume.ConvertTmpfsOptions(mnt.Spec.TmpfsOptions, mnt.Spec.ReadOnly) + data, err := parser.ConvertTmpfsOptions(mnt.Spec.TmpfsOptions, mnt.Spec.ReadOnly) if err != nil { return nil, err } diff --git a/daemon/archive_unix.go b/daemon/archive_unix.go index d5dfad78cb..e2d4c308d2 100644 --- a/daemon/archive_unix.go +++ b/daemon/archive_unix.go @@ -4,6 +4,7 @@ package daemon import ( "github.com/docker/docker/container" + "github.com/docker/docker/volume" ) // checkIfPathIsInAVolume checks if the path is in a volume. If it is, it @@ -11,8 +12,9 @@ import ( // cannot be configured with a read-only rootfs. func checkIfPathIsInAVolume(container *container.Container, absPath string) (bool, error) { var toVolume bool + parser := volume.NewParser(container.Platform) for _, mnt := range container.MountPoints { - if toVolume = mnt.HasResource(absPath); toVolume { + if toVolume = parser.HasResource(mnt, absPath); toVolume { if mnt.RW { break } diff --git a/daemon/create_windows.go b/daemon/create_windows.go index c63bfff17d..059980b47f 100644 --- a/daemon/create_windows.go +++ b/daemon/create_windows.go @@ -26,10 +26,10 @@ func (daemon *Daemon) createContainerPlatformSpecificSettings(container *contain } hostConfig.Isolation = "hyperv" } - + parser := volume.NewParser(container.Platform) for spec := range config.Volumes { - mp, err := volume.ParseMountRaw(spec, hostConfig.VolumeDriver) + mp, err := parser.ParseMountRaw(spec, hostConfig.VolumeDriver) if err != nil { return fmt.Errorf("Unrecognised volume spec: %v", err) } diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go index 0082706b6c..5e2cc62049 100644 --- a/daemon/daemon_unix.go +++ b/daemon/daemon_unix.go @@ -612,8 +612,9 @@ func verifyPlatformContainerSettings(daemon *Daemon, hostConfig *containertypes. return warnings, fmt.Errorf("Unknown runtime specified %s", hostConfig.Runtime) } + parser := volume.NewParser(runtime.GOOS) for dest := range hostConfig.Tmpfs { - if err := volume.ValidateTmpfsMountDestination(dest); err != nil { + if err := parser.ValidateTmpfsMountDestination(dest); err != nil { return warnings, err } } diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index 3d411bb251..9b8a6f56f4 100644 --- a/daemon/oci_linux.go +++ b/daemon/oci_linux.go @@ -498,6 +498,7 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c // Filter out mounts from spec noIpc := c.HostConfig.IpcMode.IsNone() + // Filter out mounts that are overridden by user supplied mounts var defaultMounts []specs.Mount _, mountDev := userMounts["/dev"] for _, m := range s.Mounts { @@ -524,7 +525,8 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c if m.Source == "tmpfs" { data := m.Data - options := []string{"noexec", "nosuid", "nodev", string(volume.DefaultPropagationMode)} + parser := volume.NewParser("linux") + options := []string{"noexec", "nosuid", "nodev", string(parser.DefaultPropagationMode())} if data != "" { options = append(options, strings.Split(data, ",")...) } diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go index 0254351569..13054ff43e 100644 --- a/daemon/oci_windows.go +++ b/daemon/oci_windows.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "path/filepath" + "runtime" "strings" containertypes "github.com/docker/docker/api/types/container" @@ -108,6 +109,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { if !mount.Writable { m.Options = append(m.Options, "ro") } + if img.OS != runtime.GOOS { + m.Type = "bind" + m.Options = append(m.Options, "rbind") + m.Options = append(m.Options, fmt.Sprintf("uvmpath=/tmp/gcs/%s/binds", c.ID)) + } s.Mounts = append(s.Mounts, m) } diff --git a/daemon/volumes.go b/daemon/volumes.go index b2f17dfab9..03bfda6f5d 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -75,6 +75,7 @@ func (m mounts) parts(i int) int { func (daemon *Daemon) registerMountPoints(container *container.Container, hostConfig *containertypes.HostConfig) (retErr error) { binds := map[string]bool{} mountPoints := map[string]*volume.MountPoint{} + parser := volume.NewParser(container.Platform) defer func() { // clean up the container mountpoints once return with error if retErr != nil { @@ -103,7 +104,7 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo // 2. Read volumes from other containers. for _, v := range hostConfig.VolumesFrom { - containerID, mode, err := volume.ParseVolumesFrom(v) + containerID, mode, err := parser.ParseVolumesFrom(v) if err != nil { return err } @@ -118,7 +119,7 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo Type: m.Type, Name: m.Name, Source: m.Source, - RW: m.RW && volume.ReadWrite(mode), + RW: m.RW && parser.ReadWrite(mode), Driver: m.Driver, Destination: m.Destination, Propagation: m.Propagation, @@ -140,7 +141,7 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo // 3. Read bind mounts for _, b := range hostConfig.Binds { - bind, err := volume.ParseMountRaw(b, hostConfig.VolumeDriver) + bind, err := parser.ParseMountRaw(b, hostConfig.VolumeDriver) if err != nil { return err } @@ -172,7 +173,7 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo } for _, cfg := range hostConfig.Mounts { - mp, err := volume.ParseMountSpec(cfg) + mp, err := parser.ParseMountSpec(cfg) if err != nil { return validationError{err} } @@ -217,7 +218,7 @@ func (daemon *Daemon) registerMountPoints(container *container.Container, hostCo // 4. Cleanup old volumes that are about to be reassigned. for _, m := range mountPoints { - if m.BackwardsCompatible() { + if parser.IsBackwardCompatible(m) { if mp, exists := container.MountPoints[m.Destination]; exists && mp.Volume != nil { daemon.volumes.Dereference(mp.Volume, container.ID) } @@ -252,6 +253,8 @@ func (daemon *Daemon) backportMountSpec(container *container.Container) { container.Lock() defer container.Unlock() + parser := volume.NewParser(container.Platform) + maybeUpdate := make(map[string]bool) for _, mp := range container.MountPoints { if mp.Spec.Source != "" && mp.Type != "" { @@ -270,7 +273,7 @@ func (daemon *Daemon) backportMountSpec(container *container.Container) { binds := make(map[string]*volume.MountPoint, len(container.HostConfig.Binds)) for _, rawSpec := range container.HostConfig.Binds { - mp, err := volume.ParseMountRaw(rawSpec, container.HostConfig.VolumeDriver) + mp, err := parser.ParseMountRaw(rawSpec, container.HostConfig.VolumeDriver) if err != nil { logrus.WithError(err).Error("Got unexpected error while re-parsing raw volume spec during spec backport") continue @@ -280,7 +283,7 @@ func (daemon *Daemon) backportMountSpec(container *container.Container) { volumesFrom := make(map[string]volume.MountPoint) for _, fromSpec := range container.HostConfig.VolumesFrom { - from, _, err := volume.ParseVolumesFrom(fromSpec) + from, _, err := parser.ParseVolumesFrom(fromSpec) if err != nil { logrus.WithError(err).WithField("id", container.ID).Error("Error reading volumes-from spec during mount spec backport") continue diff --git a/daemon/volumes_unit_test.go b/daemon/volumes_unit_test.go index 450d17f978..3f57f0ceee 100644 --- a/daemon/volumes_unit_test.go +++ b/daemon/volumes_unit_test.go @@ -1,6 +1,7 @@ package daemon import ( + "runtime" "testing" "github.com/docker/docker/volume" @@ -20,8 +21,10 @@ func TestParseVolumesFrom(t *testing.T) { {"foobar:baz", "", "", true}, } + parser := volume.NewParser(runtime.GOOS) + for _, c := range cases { - id, mode, err := volume.ParseVolumesFrom(c.spec) + id, mode, err := parser.ParseVolumesFrom(c.spec) if c.fail { if err == nil { t.Fatalf("Expected error, was nil, for spec %s\n", c.spec) diff --git a/libcontainerd/client_windows.go b/libcontainerd/client_windows.go index 67ebb5998d..8b13d44699 100644 --- a/libcontainerd/client_windows.go +++ b/libcontainerd/client_windows.go @@ -7,6 +7,7 @@ import ( "io" "io/ioutil" "os" + "path" "path/filepath" "regexp" "strings" @@ -388,11 +389,101 @@ func (clnt *client) createLinux(containerID string, checkpoint string, checkpoin configuration.NetworkSharedContainerName = spec.Windows.Network.NetworkSharedContainerName } + // Add the mounts (volumes, bind mounts etc) to the structure. We have to do + // some translation for both the mapped directories passed into HCS and in + // the spec. + // + // For HCS, we only pass in the mounts from the spec which are type "bind". + // Further, the "ContainerPath" field (which is a little mis-leadingly + // named when it applies to the utility VM rather than the container in the + // utility VM) is moved to under /tmp/gcs//binds, where this is passed + // by the caller through a 'uvmpath' option. + // + // We do similar translation for the mounts in the spec by stripping out + // the uvmpath option, and translating the Source path to the location in the + // utility VM calculated above. + // + // From inside the utility VM, you would see a 9p mount such as in the following + // where a host folder has been mapped to /target. The line with /tmp/gcs//binds + // specifically: + // + // / # mount + // rootfs on / type rootfs (rw,size=463736k,nr_inodes=115934) + // proc on /proc type proc (rw,relatime) + // sysfs on /sys type sysfs (rw,relatime) + // udev on /dev type devtmpfs (rw,relatime,size=498100k,nr_inodes=124525,mode=755) + // tmpfs on /run type tmpfs (rw,relatime) + // cgroup on /sys/fs/cgroup type cgroup (rw,relatime,cpuset,cpu,cpuacct,blkio,memory,devices,freezer,net_cls,perf_event,net_prio,hugetlb,pids,rdma) + // mqueue on /dev/mqueue type mqueue (rw,relatime) + // devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000) + // /binds/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc/target on /binds/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc/target type 9p (rw,sync,dirsync,relatime,trans=fd,rfdno=6,wfdno=6) + // /dev/pmem0 on /tmp/gcs/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc/layer0 type ext4 (ro,relatime,block_validity,delalloc,norecovery,barrier,dax,user_xattr,acl) + // /dev/sda on /tmp/gcs/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc/scratch type ext4 (rw,relatime,block_validity,delalloc,barrier,user_xattr,acl) + // overlay on /tmp/gcs/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc/rootfs type overlay (rw,relatime,lowerdir=/tmp/base/:/tmp/gcs/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc/layer0,upperdir=/tmp/gcs/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc/scratch/upper,workdir=/tmp/gcs/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc/scratch/work) + // + // /tmp/gcs/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc # ls -l + // total 16 + // drwx------ 3 0 0 60 Sep 7 18:54 binds + // -rw-r--r-- 1 0 0 3345 Sep 7 18:54 config.json + // drwxr-xr-x 10 0 0 4096 Sep 6 17:26 layer0 + // drwxr-xr-x 1 0 0 4096 Sep 7 18:54 rootfs + // drwxr-xr-x 5 0 0 4096 Sep 7 18:54 scratch + // + // /tmp/gcs/b3ea9126d67702173647ece2744f7c11181c0150e9890fc9a431849838033edc # ls -l binds + // total 0 + // drwxrwxrwt 2 0 0 4096 Sep 7 16:51 target + + mds := []hcsshim.MappedDir{} + specMounts := []specs.Mount{} + for _, mount := range spec.Mounts { + specMount := mount + if mount.Type == "bind" { + // Strip out the uvmpath from the options + updatedOptions := []string{} + uvmPath := "" + readonly := false + for _, opt := range mount.Options { + dropOption := false + elements := strings.SplitN(opt, "=", 2) + switch elements[0] { + case "uvmpath": + uvmPath = elements[1] + dropOption = true + case "rw": + case "ro": + readonly = true + case "rbind": + default: + return fmt.Errorf("unsupported option %q", opt) + } + if !dropOption { + updatedOptions = append(updatedOptions, opt) + } + } + mount.Options = updatedOptions + if uvmPath == "" { + return fmt.Errorf("no uvmpath for bind mount %+v", mount) + } + md := hcsshim.MappedDir{ + HostPath: mount.Source, + ContainerPath: path.Join(uvmPath, mount.Destination), + CreateInUtilityVM: true, + ReadOnly: readonly, + } + mds = append(mds, md) + specMount.Source = path.Join(uvmPath, mount.Destination) + } + specMounts = append(specMounts, specMount) + } + configuration.MappedDirectories = mds + hcsContainer, err := hcsshim.CreateContainer(containerID, configuration) if err != nil { return err } + spec.Mounts = specMounts + // Construct a container object for calling start on it. container := &container{ containerCommon: containerCommon{ diff --git a/runconfig/config.go b/runconfig/config.go index 3d236deb53..56eb946cc8 100644 --- a/runconfig/config.go +++ b/runconfig/config.go @@ -7,8 +7,6 @@ import ( "github.com/docker/docker/api/types/container" networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/pkg/sysinfo" - "github.com/docker/docker/volume" - "github.com/pkg/errors" ) // ContainerDecoder implements httputils.ContainerDecoder @@ -46,11 +44,6 @@ func decodeContainerConfig(src io.Reader) (*container.Config, *container.HostCon if w.Config.Volumes == nil { w.Config.Volumes = make(map[string]struct{}) } - - // Now validate all the volumes and binds - if err := validateMountSettings(w.Config, hc); err != nil { - return nil, nil, nil, err - } } // Certain parameters need daemon-side validation that cannot be done @@ -86,23 +79,3 @@ func decodeContainerConfig(src io.Reader) (*container.Config, *container.HostCon return w.Config, hc, w.NetworkingConfig, nil } - -// validateMountSettings validates each of the volumes and bind settings -// passed by the caller to ensure they are valid. -func validateMountSettings(c *container.Config, hc *container.HostConfig) error { - // it is ok to have len(hc.Mounts) > 0 && (len(hc.Binds) > 0 || len (c.Volumes) > 0 || len (hc.Tmpfs) > 0 ) - - // Ensure all volumes and binds are valid. - for spec := range c.Volumes { - if _, err := volume.ParseMountRaw(spec, hc.VolumeDriver); err != nil { - return errors.Wrapf(err, "invalid volume spec %q", spec) - } - } - for _, spec := range hc.Binds { - if _, err := volume.ParseMountRaw(spec, hc.VolumeDriver); err != nil { - return errors.Wrapf(err, "invalid bind mount spec %q", spec) - } - } - - return nil -} diff --git a/runconfig/config_test.go b/runconfig/config_test.go index ebd74ea31c..0a4ee1fa2a 100644 --- a/runconfig/config_test.go +++ b/runconfig/config_test.go @@ -9,12 +9,9 @@ import ( "strings" "testing" - "os" - "github.com/docker/docker/api/types/container" networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/strslice" - "github.com/gotestyourself/gotestyourself/skip" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -143,103 +140,6 @@ func callDecodeContainerConfigIsolation(isolation string) (*container.Config, *c return decodeContainerConfig(bytes.NewReader(b)) } -func TestDecodeContainerConfigWithVolumes(t *testing.T) { - var testcases = []decodeConfigTestcase{ - { - doc: "no paths volume", - wrapper: containerWrapperWithVolume(":"), - expectedErr: `invalid volume specification: ':'`, - }, - { - doc: "no paths bind", - wrapper: containerWrapperWithBind(":"), - expectedErr: `invalid volume specification: ':'`, - }, - { - doc: "no paths or mode volume", - wrapper: containerWrapperWithVolume("::"), - expectedErr: `invalid volume specification: '::'`, - }, - { - doc: "no paths or mode bind", - wrapper: containerWrapperWithBind("::"), - expectedErr: `invalid volume specification: '::'`, - }, - } - for _, testcase := range testcases { - t.Run(testcase.doc, runDecodeContainerConfigTestCase(testcase)) - } -} - -func TestDecodeContainerConfigWithVolumesUnix(t *testing.T) { - skip.IfCondition(t, runtime.GOOS == "windows") - - baseErr := `invalid mount config for type "volume": invalid specification: ` - var testcases = []decodeConfigTestcase{ - { - doc: "root to root volume", - wrapper: containerWrapperWithVolume("/:/"), - expectedErr: `invalid volume specification: '/:/'`, - }, - { - doc: "root to root bind", - wrapper: containerWrapperWithBind("/:/"), - expectedErr: `invalid volume specification: '/:/'`, - }, - { - doc: "no destination path volume", - wrapper: containerWrapperWithVolume(`/tmp:`), - expectedErr: ` invalid volume specification: '/tmp:'`, - }, - { - doc: "no destination path bind", - wrapper: containerWrapperWithBind(`/tmp:`), - expectedErr: ` invalid volume specification: '/tmp:'`, - }, - { - doc: "no destination path or mode volume", - wrapper: containerWrapperWithVolume(`/tmp::`), - expectedErr: `invalid mount config for type "bind": field Target must not be empty`, - }, - { - doc: "no destination path or mode bind", - wrapper: containerWrapperWithBind(`/tmp::`), - expectedErr: `invalid mount config for type "bind": field Target must not be empty`, - }, - { - doc: "too many sections volume", - wrapper: containerWrapperWithVolume(`/tmp:/tmp:/tmp:/tmp`), - expectedErr: `invalid volume specification: '/tmp:/tmp:/tmp:/tmp'`, - }, - { - doc: "too many sections bind", - wrapper: containerWrapperWithBind(`/tmp:/tmp:/tmp:/tmp`), - expectedErr: `invalid volume specification: '/tmp:/tmp:/tmp:/tmp'`, - }, - { - doc: "just root volume", - wrapper: containerWrapperWithVolume("/"), - expectedErr: baseErr + `destination can't be '/'`, - }, - { - doc: "just root bind", - wrapper: containerWrapperWithBind("/"), - expectedErr: baseErr + `destination can't be '/'`, - }, - { - doc: "bind mount passed as a volume", - wrapper: containerWrapperWithVolume(`/foo:/bar`), - expectedConfig: &container.Config{ - Volumes: map[string]struct{}{`/foo:/bar`: {}}, - }, - expectedHostConfig: &container.HostConfig{NetworkMode: "default"}, - }, - } - for _, testcase := range testcases { - t.Run(testcase.doc, runDecodeContainerConfigTestCase(testcase)) - } -} - type decodeConfigTestcase struct { doc string wrapper ContainerConfigWrapper @@ -266,89 +166,6 @@ func runDecodeContainerConfigTestCase(testcase decodeConfigTestcase) func(t *tes } } -func TestDecodeContainerConfigWithVolumesWindows(t *testing.T) { - skip.IfCondition(t, runtime.GOOS != "windows") - - tmpDir := os.Getenv("TEMP") - systemDrive := os.Getenv("SystemDrive") - var testcases = []decodeConfigTestcase{ - { - doc: "root to root volume", - wrapper: containerWrapperWithVolume(systemDrive + `\:c:\`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "root to root bind", - wrapper: containerWrapperWithBind(systemDrive + `\:c:\`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "no destination path volume", - wrapper: containerWrapperWithVolume(tmpDir + `\:`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "no destination path bind", - wrapper: containerWrapperWithBind(tmpDir + `\:`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "no destination path or mode volume", - wrapper: containerWrapperWithVolume(tmpDir + `\::`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "no destination path or mode bind", - wrapper: containerWrapperWithBind(tmpDir + `\::`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "too many sections volume", - wrapper: containerWrapperWithVolume(tmpDir + ":" + tmpDir + ":" + tmpDir + ":" + tmpDir), - expectedErr: `invalid volume specification: `, - }, - { - doc: "too many sections bind", - wrapper: containerWrapperWithBind(tmpDir + ":" + tmpDir + ":" + tmpDir + ":" + tmpDir), - expectedErr: `invalid volume specification: `, - }, - { - doc: "no drive letter volume", - wrapper: containerWrapperWithVolume(`\tmp`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "no drive letter bind", - wrapper: containerWrapperWithBind(`\tmp`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "root to c-drive volume", - wrapper: containerWrapperWithVolume(systemDrive + `\:c:`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "root to c-drive bind", - wrapper: containerWrapperWithBind(systemDrive + `\:c:`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "container path without driver letter volume", - wrapper: containerWrapperWithVolume(`c:\windows:\somewhere`), - expectedErr: `invalid volume specification: `, - }, - { - doc: "container path without driver letter bind", - wrapper: containerWrapperWithBind(`c:\windows:\somewhere`), - expectedErr: `invalid volume specification: `, - }, - } - - for _, testcase := range testcases { - t.Run(testcase.doc, runDecodeContainerConfigTestCase(testcase)) - } -} - func marshal(t *testing.T, w ContainerConfigWrapper, doc string) []byte { b, err := json.Marshal(w) require.NoError(t, err, "%s: failed to encode config wrapper", doc) diff --git a/volume/lcow_parser.go b/volume/lcow_parser.go new file mode 100644 index 0000000000..aeb81a4202 --- /dev/null +++ b/volume/lcow_parser.go @@ -0,0 +1,35 @@ +package volume + +import ( + "errors" + "fmt" + "path" + + "github.com/docker/docker/api/types/mount" +) + +var lcowSpecificValidators mountValidator = func(m *mount.Mount) error { + if path.Clean(m.Target) == "/" { + return fmt.Errorf("invalid specification: destination can't be '/'") + } + 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/linux_parser.go b/volume/linux_parser.go new file mode 100644 index 0000000000..1b3493ac75 --- /dev/null +++ b/volume/linux_parser.go @@ -0,0 +1,416 @@ +package volume + +import ( + "errors" + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/pkg/stringid" +) + +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 fmt.Errorf("invalid specification: destination can't be '/'") + } + 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 existance 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, errBindNotExist} + } + } + + 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 == DefaultDriverName +} + +func (p *linuxParser) ValidateTmpfsMountDestination(dest string) error { + if err := linuxValidateNotRoot(dest); err != nil { + return err + } + return linuxValidateAbsolute(dest) +} diff --git a/volume/parser.go b/volume/parser.go new file mode 100644 index 0000000000..1f48b60e27 --- /dev/null +++ b/volume/parser.go @@ -0,0 +1,43 @@ +package volume + +import ( + "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" +) + +// 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/store/errors.go b/volume/store/errors.go index 13c7765070..75e24e619c 100644 --- a/volume/store/errors.go +++ b/volume/store/errors.go @@ -9,8 +9,6 @@ const ( errVolumeInUse conflictError = "volume is in use" // errNoSuchVolume is a typed error returned if the requested volume doesn't exist in the volume store errNoSuchVolume notFoundError = "no such volume" - // errInvalidName is a typed error returned when creating a volume with a name that is not valid on the platform - errInvalidName invalidName = "volume name is not valid on this platform" // errNameConflict is a typed error returned on create when a volume exists with the given name, but for a different driver errNameConflict conflictError = "volume name must be unique" ) @@ -30,13 +28,6 @@ func (e notFoundError) Error() string { func (notFoundError) NotFound() {} -type invalidName string - -func (e invalidName) Error() string { - return string(e) -} -func (invalidName) InvalidParameter() {} - // OpErr is the error type returned by functions in the store package. It describes // the operation, volume name, and error. type OpErr struct { diff --git a/volume/store/store.go b/volume/store/store.go index bc447612f9..e47ec0e7da 100644 --- a/volume/store/store.go +++ b/volume/store/store.go @@ -4,6 +4,7 @@ import ( "net" "os" "path/filepath" + "runtime" "sync" "time" @@ -369,13 +370,14 @@ func volumeExists(v volume.Volume) (bool, error) { // It is expected that callers of this function hold any necessary locks. func (s *VolumeStore) create(name, driverName string, opts, labels map[string]string) (volume.Volume, error) { // Validate the name in a platform-specific manner - valid, err := volume.IsVolumeNameValid(name) + + // volume name validation is specific to the host os and not on container image + // windows/lcow should have an equivalent volumename validation logic so we create a parser for current host OS + parser := volume.NewParser(runtime.GOOS) + err := parser.ValidateVolumeName(name) if err != nil { return nil, err } - if !valid { - return nil, &OpErr{Err: errInvalidName, Name: name, Op: "create"} - } v, err := s.checkConflict(name, driverName) if err != nil { diff --git a/volume/validate.go b/volume/validate.go index 04883f35c5..b3f6409487 100644 --- a/volume/validate.go +++ b/volume/validate.go @@ -2,8 +2,6 @@ package volume import ( "fmt" - "os" - "runtime" "github.com/docker/docker/api/types/mount" "github.com/pkg/errors" @@ -11,120 +9,6 @@ import ( var errBindNotExist = errors.New("bind source path does not exist") -type validateOpts struct { - skipBindSourceCheck bool -} - -func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error { - opts := validateOpts{} - for _, o := range options { - o(&opts) - } - - if len(mnt.Target) == 0 { - return &errMountConfig{mnt, errMissingField("Target")} - } - - if err := validateNotRoot(mnt.Target); err != nil { - return &errMountConfig{mnt, err} - } - - if err := validateAbsolute(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(propagationModes) > 0 { - if _, ok := propagationModes[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 := validateAbsolute(mnt.Source); err != nil { - return &errMountConfig{mnt, err} - } - - // Do not allow binding to non-existent path - if !opts.skipBindSourceCheck { - fi, err := os.Stat(mnt.Source) - if err != nil { - if !os.IsNotExist(err) { - return &errMountConfig{mnt, err} - } - return &errMountConfig{mnt, errBindNotExist} - } - if err := validateStat(fi); err != nil { - return &errMountConfig{mnt, err} - } - } - 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 valid, err := IsVolumeNameValid(mnt.Source); !valid { - if err == nil { - err = errors.New("invalid volume name") - } - return &errMountConfig{mnt, err} - } - } - case mount.TypeTmpfs: - if len(mnt.Source) != 0 { - return &errMountConfig{mnt, errExtraField("Source")} - } - if err := ValidateTmpfsMountDestination(mnt.Target); err != nil { - return &errMountConfig{mnt, err} - } - 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")} - } - return nil -} - type errMountConfig struct { mount *mount.Mount err error @@ -140,37 +24,3 @@ func errExtraField(name string) error { func errMissingField(name string) error { return errors.Errorf("field %s must not be empty", name) } - -func validateAbsolute(p string) error { - p = convertSlash(p) - if isAbsPath(p) { - return nil - } - return errors.Errorf("invalid mount path: '%s' mount path must be absolute", p) -} - -// ValidateTmpfsMountDestination validates the destination of tmpfs mount. -// Currently, we have only two obvious rule for validation: -// - path must not be "/" -// - path must be absolute -// We should add more rules carefully (#30166) -func ValidateTmpfsMountDestination(dest string) error { - if err := validateNotRoot(dest); err != nil { - return err - } - return validateAbsolute(dest) -} - -type validationError struct { - err error -} - -func (e validationError) Error() string { - return e.err.Error() -} - -func (e validationError) InvalidParameter() {} - -func (e validationError) Cause() error { - return e.err -} diff --git a/volume/validate_test.go b/volume/validate_test.go index 8732500fc0..6a8e28682b 100644 --- a/volume/validate_test.go +++ b/volume/validate_test.go @@ -4,6 +4,7 @@ import ( "errors" "io/ioutil" "os" + "runtime" "strings" "testing" @@ -27,17 +28,50 @@ func TestValidateMount(t *testing.T) { {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: testSourcePath, Target: testDestinationPath}, errBindNotExist}, + {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, nil}, {mount.Mount{Type: "invalid", Target: testDestinationPath}, errors.New("mount type unknown")}, } + if runtime.GOOS == "windows" { + cases = append(cases, struct { + input mount.Mount + expected error + }{mount.Mount{Type: mount.TypeBind, Source: testSourcePath, Target: testDestinationPath}, errBindNotExist}) // bind source existance is not checked on linux + } + 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"}, errBindNotExist}, + {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 := validateMountConfig(&x.input) + 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.Fatalf("expected %q, got %q, case: %d", x.expected, err, i) + 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/volume.go b/volume/volume.go index 7f962a981e..4aa4de513d 100644 --- a/volume/volume.go +++ b/volume/volume.go @@ -3,7 +3,6 @@ package volume import ( "fmt" "os" - "strings" "syscall" "time" @@ -216,153 +215,10 @@ func (m *MountPoint) Path() string { return m.Source } -// ParseVolumesFrom ensures that the supplied volumes-from is valid. -func 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 !ValidMountMode(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 HasPropagation(mode) { - return "", "", errInvalidMode(mode) - } - // Do not allow copy modes on volumes-from - if _, isSet := getCopyMode(mode); isSet { - return "", "", errInvalidMode(mode) - } - } - return id, mode, nil -} - -// ParseMountRaw parses a raw volume spec (e.g. `-v /foo:/bar:shared`) into a -// structured spec. Once the raw spec is parsed it relies on `ParseMountSpec` to -// validate the spec and create a MountPoint -func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) { - arr, err := splitRawSpec(convertSlash(raw)) - if err != nil { - return nil, err - } - - var spec mounttypes.Mount - var mode string - switch len(arr) { - case 1: - // Just a destination path in the container - spec.Target = arr[0] - case 2: - if ValidMountMode(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 !ValidMountMode(mode) { - return nil, errInvalidMode(mode) - } - - spec.Type = detectMountType(spec.Source) - spec.ReadOnly = !ReadWrite(mode) - - // cannot assume that if a volume driver is passed in that we should set it - if volumeDriver != "" && spec.Type == mounttypes.TypeVolume { - spec.VolumeOptions = &mounttypes.VolumeOptions{ - DriverConfig: &mounttypes.Driver{Name: volumeDriver}, - } - } - - if copyData, isSet := getCopyMode(mode); isSet { - if spec.VolumeOptions == nil { - spec.VolumeOptions = &mounttypes.VolumeOptions{} - } - spec.VolumeOptions.NoCopy = !copyData - } - if HasPropagation(mode) { - spec.BindOptions = &mounttypes.BindOptions{ - Propagation: GetPropagation(mode), - } - } - - mp, err := ParseMountSpec(spec, platformRawValidationOpts...) - if mp != nil { - mp.Mode = mode - } - if err != nil { - err = errors.Wrap(err, errInvalidSpec(raw).Error()) - } - return mp, err -} - -// ParseMountSpec reads a mount config, validates it, and configures a mountpoint from it. -func ParseMountSpec(cfg mounttypes.Mount, options ...func(*validateOpts)) (*MountPoint, error) { - if err := validateMountConfig(&cfg, options...); err != nil { - return nil, validationError{err} - } - mp := &MountPoint{ - RW: !cfg.ReadOnly, - Destination: clean(convertSlash(cfg.Target)), - Type: cfg.Type, - Spec: cfg, - } - - switch cfg.Type { - case mounttypes.TypeVolume: - if cfg.Source == "" { - mp.Name = stringid.GenerateNonCryptoID() - } else { - mp.Name = cfg.Source - } - mp.CopyData = DefaultCopyMode - - if cfg.VolumeOptions != nil { - if cfg.VolumeOptions.DriverConfig != nil { - mp.Driver = cfg.VolumeOptions.DriverConfig.Name - } - if cfg.VolumeOptions.NoCopy { - mp.CopyData = false - } - } - 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 - } else { - // If user did not specify a propagation mode, get - // default propagation mode. - mp.Propagation = DefaultPropagationMode - } - case mounttypes.TypeTmpfs: - // NOP - } - return mp, nil -} - func errInvalidMode(mode string) error { - return validationError{errors.Errorf("invalid mode: %v", mode)} + return errors.Errorf("invalid mode: %v", mode) } func errInvalidSpec(spec string) error { - return validationError{errors.Errorf("invalid volume specification: '%s'", spec)} + return errors.Errorf("invalid volume specification: '%s'", spec) } diff --git a/volume/volume_copy.go b/volume/volume_copy.go index 77f06a0d1f..37c7fa74a6 100644 --- a/volume/volume_copy.go +++ b/volume/volume_copy.go @@ -13,11 +13,11 @@ func copyModeExists(mode string) bool { } // GetCopyMode gets the copy mode from the mode string for mounts -func getCopyMode(mode string) (bool, bool) { +func getCopyMode(mode string, def bool) (bool, bool) { for _, o := range strings.Split(mode, ",") { if isEnabled, exists := copyModes[o]; exists { return isEnabled, true } } - return DefaultCopyMode, false + return def, false } diff --git a/volume/volume_copy_unix.go b/volume/volume_copy_unix.go deleted file mode 100644 index ad66e17637..0000000000 --- a/volume/volume_copy_unix.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build !windows - -package volume - -const ( - // DefaultCopyMode is the copy mode used by default for normal/named volumes - DefaultCopyMode = true -) diff --git a/volume/volume_copy_windows.go b/volume/volume_copy_windows.go deleted file mode 100644 index 798638c878..0000000000 --- a/volume/volume_copy_windows.go +++ /dev/null @@ -1,6 +0,0 @@ -package volume - -const ( - // DefaultCopyMode is the copy mode used by default for normal/named volumes - DefaultCopyMode = false -) diff --git a/volume/volume_linux.go b/volume/volume_linux.go deleted file mode 100644 index fdf7b63e4b..0000000000 --- a/volume/volume_linux.go +++ /dev/null @@ -1,56 +0,0 @@ -// +build linux - -package volume - -import ( - "fmt" - "strings" - - mounttypes "github.com/docker/docker/api/types/mount" -) - -// ConvertTmpfsOptions converts *mounttypes.TmpfsOptions to the raw option string -// for mount(2). -func ConvertTmpfsOptions(opt *mounttypes.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 -} diff --git a/volume/volume_linux_test.go b/volume/volume_linux_test.go deleted file mode 100644 index 40ce5525a3..0000000000 --- a/volume/volume_linux_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// +build linux - -package volume - -import ( - "strings" - "testing" - - mounttypes "github.com/docker/docker/api/types/mount" -) - -func TestConvertTmpfsOptions(t *testing.T) { - type testCase struct { - opt mounttypes.TmpfsOptions - readOnly bool - expectedSubstrings []string - unexpectedSubstrings []string - } - cases := []testCase{ - { - opt: mounttypes.TmpfsOptions{SizeBytes: 1024 * 1024, Mode: 0700}, - readOnly: false, - expectedSubstrings: []string{"size=1m", "mode=700"}, - unexpectedSubstrings: []string{"ro"}, - }, - { - opt: mounttypes.TmpfsOptions{}, - readOnly: true, - expectedSubstrings: []string{"ro"}, - unexpectedSubstrings: []string{}, - }, - } - for _, c := range cases { - data, err := 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) - } - } - } -} diff --git a/volume/volume_propagation_linux.go b/volume/volume_propagation_linux.go deleted file mode 100644 index 1de57ab52b..0000000000 --- a/volume/volume_propagation_linux.go +++ /dev/null @@ -1,47 +0,0 @@ -// +build linux - -package volume - -import ( - "strings" - - mounttypes "github.com/docker/docker/api/types/mount" -) - -// DefaultPropagationMode defines what propagation mode should be used by -// default if user has not specified one explicitly. -// propagation modes -const DefaultPropagationMode = mounttypes.PropagationRPrivate - -var propagationModes = map[mounttypes.Propagation]bool{ - mounttypes.PropagationPrivate: true, - mounttypes.PropagationRPrivate: true, - mounttypes.PropagationSlave: true, - mounttypes.PropagationRSlave: true, - mounttypes.PropagationShared: true, - mounttypes.PropagationRShared: true, -} - -// GetPropagation extracts and returns the mount propagation mode. If there -// are no specifications, then by default it is "private". -func GetPropagation(mode string) mounttypes.Propagation { - for _, o := range strings.Split(mode, ",") { - prop := mounttypes.Propagation(o) - if propagationModes[prop] { - return prop - } - } - return DefaultPropagationMode -} - -// HasPropagation checks if there is a valid propagation mode present in -// passed string. Returns true if a valid propagation mode specifier is -// present, false otherwise. -func HasPropagation(mode string) bool { - for _, o := range strings.Split(mode, ",") { - if propagationModes[mounttypes.Propagation(o)] { - return true - } - } - return false -} diff --git a/volume/volume_propagation_linux_test.go b/volume/volume_propagation_linux_test.go deleted file mode 100644 index 46d0265062..0000000000 --- a/volume/volume_propagation_linux_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// +build linux - -package volume - -import ( - "strings" - "testing" -) - -func TestParseMountRawPropagation(t *testing.T) { - var ( - valid []string - invalid map[string]string - ) - - valid = []string{ - "/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{ - "/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", - } - - for _, path := range valid { - if _, err := ParseMountRaw(path, "local"); err != nil { - t.Fatalf("ParseMountRaw(`%q`) should succeed: error %q", path, err) - } - } - - for path, expectedError := range invalid { - if _, err := ParseMountRaw(path, "local"); err == nil { - t.Fatalf("ParseMountRaw(`%q`) should have failed validation. Err %v", path, err) - } else { - if !strings.Contains(err.Error(), expectedError) { - t.Fatalf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) - } - } - } -} diff --git a/volume/volume_propagation_unsupported.go b/volume/volume_propagation_unsupported.go deleted file mode 100644 index 7311ffc2e0..0000000000 --- a/volume/volume_propagation_unsupported.go +++ /dev/null @@ -1,24 +0,0 @@ -// +build !linux - -package volume - -import mounttypes "github.com/docker/docker/api/types/mount" - -// DefaultPropagationMode is used only in linux. In other cases it returns -// empty string. -const DefaultPropagationMode mounttypes.Propagation = "" - -// propagation modes not supported on this platform. -var propagationModes = map[mounttypes.Propagation]bool{} - -// GetPropagation is not supported. Return empty string. -func GetPropagation(mode string) mounttypes.Propagation { - return DefaultPropagationMode -} - -// HasPropagation checks if there is a valid propagation mode present in -// passed string. Returns true if a valid propagation mode specifier is -// present, false otherwise. -func HasPropagation(mode string) bool { - return false -} diff --git a/volume/volume_test.go b/volume/volume_test.go index 395f374ff0..9f2020d58d 100644 --- a/volume/volume_test.go +++ b/volume/volume_test.go @@ -10,14 +10,84 @@ import ( "github.com/docker/docker/api/types/mount" ) -func TestParseMountRaw(t *testing.T) { - var ( - valid []string - invalid map[string]string - ) +type parseMountRawTestSet struct { + valid []string + invalid map[string]string +} - if runtime.GOOS == "windows" { - valid = []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`, @@ -35,10 +105,14 @@ func TestParseMountRaw(t *testing.T) { `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 - } - invalid = map[string]string{ + `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: ", @@ -82,10 +156,79 @@ func TestParseMountRaw(t *testing.T) { `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`, - } - - } else { - valid = []string{ + `\\.\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`: `source path does not exist`, + `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", @@ -95,47 +238,87 @@ func TestParseMountRaw(t *testing.T) { "hostPath:/containerPath:ro", "/hostPath:/containerPath:rw", "/rw:/ro", - } - 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`, - } + "/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", + }, } - for _, path := range valid { - if _, err := ParseMountRaw(path, "local"); err != nil { - t.Fatalf("ParseMountRaw(`%q`) should succeed: error %q", path, err) - } - } + linParser := &linuxParser{} + winParser := &windowsParser{} + lcowParser := &lcowParser{} + tester := func(parser Parser, set parseMountRawTestSet) { - for path, expectedError := range invalid { - if mp, err := ParseMountRaw(path, "local"); err == nil { - t.Fatalf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp) - } else { - if !strings.Contains(err.Error(), expectedError) { - t.Fatalf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) + 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 @@ -153,76 +336,96 @@ type testParseMountRaw struct { } func TestParseMountRawSplit(t *testing.T) { - var cases []testParseMountRaw - if runtime.GOOS == "windows" { - cases = []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}, - {`\\.\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", "", 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}, - } + 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}, } - - for i, c := range cases { - t.Logf("case %d", i) - m, err := ParseMountRaw(c.bind, c.driver) - if c.fail { - if err == nil { - t.Fatalf("Expected error, was nil, for spec %s\n", c.bind) + 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 } - continue - } - if m == nil || err != nil { - t.Fatalf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err.Error()) - 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.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.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind) + } - if m.Destination != c.expDest { - t.Fatalf("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.Source != c.expSource { - t.Fatalf("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.Name != c.expName { - t.Fatalf("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.Driver != c.expDriver { - t.Fatalf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind) - } - - if m.RW != c.expRW { - t.Fatalf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, 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) { @@ -235,43 +438,43 @@ func TestParseMountSpec(t *testing.T) { 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: DefaultPropagationMode}}, - {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true, Propagation: DefaultPropagationMode}}, - {mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: DefaultPropagationMode}}, - {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath + string(os.PathSeparator), ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: DefaultPropagationMode}}, - {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: DefaultCopyMode}}, - {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath + string(os.PathSeparator)}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: DefaultCopyMode}}, + {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 := ParseMountSpec(c.input) + mp, err := parser.ParseMountSpec(c.input) if err != nil { - t.Fatal(err) + t.Error(err) } if c.expected.Type != mp.Type { - t.Fatalf("Expected mount types to match. Expected: '%s', Actual: '%s'", 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.Fatalf("Expected mount destination to match. Expected: '%s', Actual: '%s'", 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.Fatalf("Expected mount source to match. Expected: '%s', Actual: '%s'", 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.Fatalf("Expected mount writable to match. Expected: '%v', Actual: '%v'", 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.Fatalf("Expected mount propagation to match. Expected: '%v', Actual: '%s'", 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.Fatalf("Expected mount driver to match. Expected: '%v', Actual: '%s'", 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.Fatalf("Expected mount copy data to match. Expected: '%v', Actual: '%v'", 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/volume_unix.go b/volume/volume_unix.go index 31555a691a..0968fe37e1 100644 --- a/volume/volume_unix.go +++ b/volume/volume_unix.go @@ -4,153 +4,15 @@ package volume import ( "fmt" - "os" "path/filepath" "strings" - - mounttypes "github.com/docker/docker/api/types/mount" ) -var platformRawValidationOpts = []func(o *validateOpts){ - // need to make sure to not error out if the bind source does not exist on unix - // this is supported for historical reasons, the path will be automatically - // created later. - func(o *validateOpts) { o.skipBindSourceCheck = true }, -} - -// read-write modes -var rwModes = map[string]bool{ - "rw": true, - "ro": true, -} - -// label modes -var labelModes = map[string]bool{ - "Z": true, - "z": true, -} - -// consistency modes -var consistencyModes = map[mounttypes.Consistency]bool{ - mounttypes.ConsistencyFull: true, - mounttypes.ConsistencyCached: true, - mounttypes.ConsistencyDelegated: true, -} - -// BackwardsCompatible decides whether this mount point can be -// used in old versions of Docker or not. -// Only bind mounts and local volumes can be used in old versions of Docker. -func (m *MountPoint) BackwardsCompatible() bool { - return len(m.Source) > 0 || m.Driver == DefaultDriverName -} - -// HasResource checks whether the given absolute path for a container is in -// this mount point. If the relative path starts with `../` then the resource -// is outside of this mount point, but we can't simply check for this prefix -// because it misses `..` which is also outside of the mount, so check both. -func (m *MountPoint) HasResource(absolutePath string) bool { +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)) } -// IsVolumeNameValid checks a volume name in a platform specific manner. -func IsVolumeNameValid(name string) (bool, error) { - return true, nil -} - -// ValidMountMode will make sure the mount mode is valid. -// returns if it's a valid mount mode or not. -func ValidMountMode(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 labelModes[o]: - labelModeCount++ - case propagationModes[mounttypes.Propagation(o)]: - propagationModeCount++ - case copyModeExists(o): - copyModeCount++ - case consistencyModes[mounttypes.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 -} - -// ReadWrite tells you if a mode string is a valid read-write mode or not. -// If there are no specifications w.r.t read write mode, then by default -// it returns true. -func ReadWrite(mode string) bool { - if !ValidMountMode(mode) { - return false - } - - for _, o := range strings.Split(mode, ",") { - if o == "ro" { - return false - } - } - return true -} - -func validateNotRoot(p string) error { - p = filepath.Clean(convertSlash(p)) - if p == "/" { - return fmt.Errorf("invalid specification: destination can't be '/'") - } - return nil -} - -func convertSlash(p string) string { - return p -} - -// isAbsPath reports whether the path is absolute. -func isAbsPath(p string) bool { - return filepath.IsAbs(p) -} - -func splitRawSpec(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 detectMountType(p string) mounttypes.Type { - if filepath.IsAbs(p) { - return mounttypes.TypeBind - } - return mounttypes.TypeVolume -} - -func clean(p string) string { - return filepath.Clean(p) -} - -func validateStat(fi os.FileInfo) error { - return nil +func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool { + return false } diff --git a/volume/volume_unsupported.go b/volume/volume_unsupported.go deleted file mode 100644 index ff9d6afa27..0000000000 --- a/volume/volume_unsupported.go +++ /dev/null @@ -1,16 +0,0 @@ -// +build !linux - -package volume - -import ( - "fmt" - "runtime" - - mounttypes "github.com/docker/docker/api/types/mount" -) - -// ConvertTmpfsOptions converts *mounttypes.TmpfsOptions to the raw option string -// for mount(2). -func ConvertTmpfsOptions(opt *mounttypes.TmpfsOptions, readOnly bool) (string, error) { - return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS) -} diff --git a/volume/volume_windows.go b/volume/volume_windows.go index 5bee223702..8ec1d6c801 100644 --- a/volume/volume_windows.go +++ b/volume/volume_windows.go @@ -1,213 +1,8 @@ package volume -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - - mounttypes "github.com/docker/docker/api/types/mount" -) - -// read-write modes -var rwModes = map[string]bool{ - "rw": true, -} - -// read-only modes -var roModes = map[string]bool{ - "ro": true, -} - -var platformRawValidationOpts = []func(*validateOpts){} - -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]+` - // 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((` + 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 - - // 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(` + 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 - // - 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(?i)ro|rw))?` -) - -// BackwardsCompatible decides whether this mount point can be -// used in old versions of Docker or not. -// Windows volumes are never backwards compatible. -func (m *MountPoint) BackwardsCompatible() bool { +func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool { return false } - -func splitRawSpec(raw string) ([]string, error) { - specExp := regexp.MustCompile(`^` + RXSource + RXDestination + 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"] != "" { - validName, err := IsVolumeNameValid(matchgroups["destination"]) - if err != nil { - return nil, err - } - if !validName { - if fi, err := os.Stat(matchgroups["destination"]); err == nil { - if !fi.IsDir() { - return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"]) - } - } - } - } - 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 + `$`) - if !nameExp.MatchString(name) { - return false, nil - } - nameExp = regexp.MustCompile(`^` + RXReservedNames + `$`) - if nameExp.MatchString(name) { - return false, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name) - } - return true, nil -} - -// ValidMountMode will make sure the mount mode is valid. -// returns if it's a valid mount mode or not. -func ValidMountMode(mode string) bool { - if mode == "" { - return true - } - return roModes[strings.ToLower(mode)] || rwModes[strings.ToLower(mode)] -} - -// ReadWrite tells you if a mode string is a valid read-write mode or not. -func ReadWrite(mode string) bool { - return rwModes[strings.ToLower(mode)] || mode == "" -} - -func validateNotRoot(p string) error { - p = strings.ToLower(convertSlash(p)) - if p == "c:" || p == `c:\` { - return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p) - } - return nil -} - -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 := cleanRegexp.MatchString(p); match { - return p - } - return filepath.Clean(p) -} - -func validateStat(fi os.FileInfo) error { - if !fi.IsDir() { - return fmt.Errorf("source path must be a directory") - } - return nil +func (p *linuxParser) HasResource(m *MountPoint, absolutePath string) bool { + return false } diff --git a/volume/windows_parser.go b/volume/windows_parser.go new file mode 100644 index 0000000000..172610dbdd --- /dev/null +++ b/volume/windows_parser.go @@ -0,0 +1,456 @@ +package volume + +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((` + 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((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))` + + rxLCOWDestination = `(?P/(?:[^\\/:*?"<>\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(?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, errBindNotExist} + } + 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") +}