Merge pull request #34258 from simonferquel/lcow-mounts
LCOW: Prepare work for bind mounts
This commit is contained in:
commit
0300fa7f80
33 changed files with 1467 additions and 1291 deletions
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, ",")...)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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/<ID>/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/<ID>/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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
35
volume/lcow_parser.go
Normal file
35
volume/lcow_parser.go
Normal file
|
@ -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)
|
||||
}
|
416
volume/linux_parser.go
Normal file
416
volume/linux_parser.go
Normal file
|
@ -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)
|
||||
}
|
43
volume/parser.go
Normal file
43
volume/parser.go
Normal file
|
@ -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{}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
148
volume/volume.go
148
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
// +build !windows
|
||||
|
||||
package volume
|
||||
|
||||
const (
|
||||
// DefaultCopyMode is the copy mode used by default for normal/named volumes
|
||||
DefaultCopyMode = true
|
||||
)
|
|
@ -1,6 +0,0 @@
|
|||
package volume
|
||||
|
||||
const (
|
||||
// DefaultCopyMode is the copy mode used by default for normal/named volumes
|
||||
DefaultCopyMode = false
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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<source>((` + RXHostDir + `)|(` + RXName + `)|(` + RXPipe + `))):)?`
|
||||
|
||||
// Source. Can be either a host directory, a name, or omitted:
|
||||
// HostDir:
|
||||
// - Essentially using the folder solution from
|
||||
// https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
|
||||
// but adding case insensitivity.
|
||||
// - Must be an absolute path such as c:\path
|
||||
// - Can include spaces such as `c:\program files`
|
||||
// - And then followed by a colon which is not in the capture group
|
||||
// - And can be optional
|
||||
// Name:
|
||||
// - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
|
||||
// - And then followed by a colon which is not in the capture group
|
||||
// - And can be optional
|
||||
|
||||
// RXDestinationDir is the file path option for the mount destination
|
||||
RXDestinationDir = `([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?)`
|
||||
// RXDestination is the regex expression for the mount destination
|
||||
RXDestination = `(?P<destination>(` + 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<mode>(?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
|
||||
}
|
||||
|
|
456
volume/windows_parser.go
Normal file
456
volume/windows_parser.go
Normal file
|
@ -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<source>((` + rxHostDir + `)|(` + rxName + `)|(` + rxPipe + `))):)?`
|
||||
|
||||
// Source. Can be either a host directory, a name, or omitted:
|
||||
// HostDir:
|
||||
// - Essentially using the folder solution from
|
||||
// https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
|
||||
// but adding case insensitivity.
|
||||
// - Must be an absolute path such as c:\path
|
||||
// - Can include spaces such as `c:\program files`
|
||||
// - And then followed by a colon which is not in the capture group
|
||||
// - And can be optional
|
||||
// Name:
|
||||
// - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
|
||||
// - And then followed by a colon which is not in the capture group
|
||||
// - And can be optional
|
||||
|
||||
// rxDestination is the regex expression for the mount destination
|
||||
rxDestination = `(?P<destination>((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))`
|
||||
|
||||
rxLCOWDestination = `(?P<destination>/(?:[^\\/:*?"<>\r\n]+[/]?)*)`
|
||||
// Destination (aka container path):
|
||||
// - Variation on hostdir but can be a drive followed by colon as well
|
||||
// - If a path, must be absolute. Can include spaces
|
||||
// - Drive cannot be c: (explicitly checked in code, not RegEx)
|
||||
|
||||
// rxMode is the regex expression for the mode of the mount
|
||||
// Mode (optional):
|
||||
// - Hopefully self explanatory in comparison to above regex's.
|
||||
// - Colon is not in the capture group
|
||||
rxMode = `(:(?P<mode>(?i)ro|rw))?`
|
||||
)
|
||||
|
||||
type mountValidator func(mnt *mount.Mount) error
|
||||
|
||||
func windowsSplitRawSpec(raw, destRegex string) ([]string, error) {
|
||||
specExp := regexp.MustCompile(`^` + rxSource + destRegex + rxMode + `$`)
|
||||
match := specExp.FindStringSubmatch(strings.ToLower(raw))
|
||||
|
||||
// Must have something back
|
||||
if len(match) == 0 {
|
||||
return nil, errInvalidSpec(raw)
|
||||
}
|
||||
|
||||
var split []string
|
||||
matchgroups := make(map[string]string)
|
||||
// Pull out the sub expressions from the named capture groups
|
||||
for i, name := range specExp.SubexpNames() {
|
||||
matchgroups[name] = strings.ToLower(match[i])
|
||||
}
|
||||
if source, exists := matchgroups["source"]; exists {
|
||||
if source != "" {
|
||||
split = append(split, source)
|
||||
}
|
||||
}
|
||||
if destination, exists := matchgroups["destination"]; exists {
|
||||
if destination != "" {
|
||||
split = append(split, destination)
|
||||
}
|
||||
}
|
||||
if mode, exists := matchgroups["mode"]; exists {
|
||||
if mode != "" {
|
||||
split = append(split, mode)
|
||||
}
|
||||
}
|
||||
// Fix #26329. If the destination appears to be a file, and the source is null,
|
||||
// it may be because we've fallen through the possible naming regex and hit a
|
||||
// situation where the user intention was to map a file into a container through
|
||||
// a local volume, but this is not supported by the platform.
|
||||
if matchgroups["source"] == "" && matchgroups["destination"] != "" {
|
||||
volExp := regexp.MustCompile(`^` + rxName + `$`)
|
||||
reservedNameExp := regexp.MustCompile(`^` + rxReservedNames + `$`)
|
||||
|
||||
if volExp.MatchString(matchgroups["destination"]) {
|
||||
if reservedNameExp.MatchString(matchgroups["destination"]) {
|
||||
return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"])
|
||||
}
|
||||
} else {
|
||||
|
||||
exists, isDir, _ := currentFileInfoProvider.fileInfo(matchgroups["destination"])
|
||||
if exists && !isDir {
|
||||
return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return split, nil
|
||||
}
|
||||
|
||||
func windowsValidMountMode(mode string) bool {
|
||||
if mode == "" {
|
||||
return true
|
||||
}
|
||||
return rwModes[strings.ToLower(mode)]
|
||||
}
|
||||
func windowsValidateNotRoot(p string) error {
|
||||
p = strings.ToLower(strings.Replace(p, `/`, `\`, -1))
|
||||
if p == "c:" || p == `c:\` {
|
||||
return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var windowsSpecificValidators mountValidator = func(mnt *mount.Mount) error {
|
||||
return windowsValidateNotRoot(mnt.Target)
|
||||
}
|
||||
|
||||
func windowsValidateRegex(p, r string) error {
|
||||
if regexp.MustCompile(`^` + r + `$`).MatchString(strings.ToLower(p)) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid mount path: '%s'", p)
|
||||
}
|
||||
func windowsValidateAbsolute(p string) error {
|
||||
if err := windowsValidateRegex(p, rxDestination); err != nil {
|
||||
return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func windowsDetectMountType(p string) mount.Type {
|
||||
if strings.HasPrefix(p, `\\.\pipe\`) {
|
||||
return mount.TypeNamedPipe
|
||||
} else if regexp.MustCompile(`^` + rxHostDir + `$`).MatchString(p) {
|
||||
return mount.TypeBind
|
||||
} else {
|
||||
return mount.TypeVolume
|
||||
}
|
||||
}
|
||||
|
||||
func (p *windowsParser) ReadWrite(mode string) bool {
|
||||
return strings.ToLower(mode) != "ro"
|
||||
}
|
||||
|
||||
// IsVolumeNameValid checks a volume name in a platform specific manner.
|
||||
func (p *windowsParser) ValidateVolumeName(name string) error {
|
||||
nameExp := regexp.MustCompile(`^` + rxName + `$`)
|
||||
if !nameExp.MatchString(name) {
|
||||
return errors.New("invalid volume name")
|
||||
}
|
||||
nameExp = regexp.MustCompile(`^` + rxReservedNames + `$`)
|
||||
if nameExp.MatchString(name) {
|
||||
return fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (p *windowsParser) validateMountConfig(mnt *mount.Mount) error {
|
||||
return p.validateMountConfigReg(mnt, rxDestination, windowsSpecificValidators)
|
||||
}
|
||||
|
||||
type fileInfoProvider interface {
|
||||
fileInfo(path string) (exist, isDir bool, err error)
|
||||
}
|
||||
|
||||
type defaultFileInfoProvider struct {
|
||||
}
|
||||
|
||||
func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, false, err
|
||||
}
|
||||
return false, false, nil
|
||||
}
|
||||
return true, fi.IsDir(), nil
|
||||
}
|
||||
|
||||
var currentFileInfoProvider fileInfoProvider = defaultFileInfoProvider{}
|
||||
|
||||
func (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, destRegex string, additionalValidators ...mountValidator) error {
|
||||
|
||||
for _, v := range additionalValidators {
|
||||
if err := v(mnt); err != nil {
|
||||
return &errMountConfig{mnt, err}
|
||||
}
|
||||
}
|
||||
if len(mnt.Target) == 0 {
|
||||
return &errMountConfig{mnt, errMissingField("Target")}
|
||||
}
|
||||
|
||||
if err := windowsValidateRegex(mnt.Target, destRegex); err != nil {
|
||||
return &errMountConfig{mnt, err}
|
||||
}
|
||||
|
||||
switch mnt.Type {
|
||||
case mount.TypeBind:
|
||||
if len(mnt.Source) == 0 {
|
||||
return &errMountConfig{mnt, errMissingField("Source")}
|
||||
}
|
||||
// Don't error out just because the propagation mode is not supported on the platform
|
||||
if opts := mnt.BindOptions; opts != nil {
|
||||
if len(opts.Propagation) > 0 {
|
||||
return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
|
||||
}
|
||||
}
|
||||
if mnt.VolumeOptions != nil {
|
||||
return &errMountConfig{mnt, errExtraField("VolumeOptions")}
|
||||
}
|
||||
|
||||
if err := windowsValidateAbsolute(mnt.Source); err != nil {
|
||||
return &errMountConfig{mnt, err}
|
||||
}
|
||||
|
||||
exists, isdir, err := currentFileInfoProvider.fileInfo(mnt.Source)
|
||||
if err != nil {
|
||||
return &errMountConfig{mnt, err}
|
||||
}
|
||||
if !exists {
|
||||
return &errMountConfig{mnt, 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")
|
||||
}
|
Loading…
Reference in a new issue