From a7e686a779523100a092acb2683b849126953931 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 9 Sep 2015 19:23:06 -0700 Subject: [PATCH] Windows: Add volume support Signed-off-by: John Howard --- api/server/router/local/container.go | 7 +- builder/dockerfile/evaluator.go | 2 +- daemon/archive_unix.go | 2 +- daemon/container.go | 116 ++++++- daemon/container_unix.go | 118 ++----- daemon/container_windows.go | 18 +- daemon/create.go | 48 +-- daemon/create_unix.go | 6 +- daemon/create_windows.go | 72 +++++ daemon/daemon_unix.go | 7 +- daemon/daemonbuilder/builder.go | 7 +- daemon/execdriver/driver.go | 10 +- daemon/execdriver/driver_unix.go | 9 + daemon/execdriver/driver_windows.go | 7 + daemon/execdriver/windows/run.go | 47 ++- daemon/inspect_windows.go | 12 +- daemon/volumes.go | 208 +++++++----- daemon/volumes_linux_unit_test.go | 58 ---- daemon/volumes_unit_test.go | 7 +- daemon/volumes_unix.go | 250 +++------------ daemon/volumes_windows.go | 60 ++-- errors/daemon.go | 66 +++- image/fixtures/pre1.9/expected_config | 1 + integration-cli/docker_cli_run_test.go | 4 +- opts/opts.go | 9 - opts/opts_test.go | 52 --- pkg/system/syscall_unix.go | 11 + pkg/system/syscall_windows.go | 6 + runconfig/config.go | 39 ++- runconfig/config_test.go | 37 ++- .../{ => unix}/container_config_1_14.json | 0 .../{ => unix}/container_config_1_17.json | 0 .../{ => unix}/container_config_1_19.json | 0 .../{ => unix}/container_hostconfig_1_14.json | 0 .../{ => unix}/container_hostconfig_1_19.json | 0 .../windows/container_config_1_19.json | 58 ++++ runconfig/hostconfig_test.go | 4 +- runconfig/parse.go | 10 +- runconfig/parse_test.go | 299 ++++++++++++++---- volume/drivers/extpoint_test.go | 1 - volume/store/store.go | 33 +- volume/store/store_unix.go | 9 + volume/store/store_windows.go | 12 + volume/volume.go | 147 +++++++-- volume/volume_test.go | 261 +++++++++++++++ volume/volume_unix.go | 132 ++++++++ volume/volume_windows.go | 181 +++++++++++ 47 files changed, 1711 insertions(+), 732 deletions(-) delete mode 100644 daemon/volumes_linux_unit_test.go create mode 100644 pkg/system/syscall_unix.go create mode 100644 pkg/system/syscall_windows.go rename runconfig/fixtures/{ => unix}/container_config_1_14.json (100%) rename runconfig/fixtures/{ => unix}/container_config_1_17.json (100%) rename runconfig/fixtures/{ => unix}/container_config_1_19.json (100%) rename runconfig/fixtures/{ => unix}/container_hostconfig_1_14.json (100%) rename runconfig/fixtures/{ => unix}/container_hostconfig_1_19.json (100%) create mode 100644 runconfig/fixtures/windows/container_config_1_19.json create mode 100644 volume/store/store_unix.go create mode 100644 volume/store/store_windows.go create mode 100644 volume/volume_test.go create mode 100644 volume/volume_unix.go create mode 100644 volume/volume_windows.go diff --git a/api/server/router/local/container.go b/api/server/router/local/container.go index 9cf14367db..f43093378b 100644 --- a/api/server/router/local/container.go +++ b/api/server/router/local/container.go @@ -331,7 +331,12 @@ func (s *router) postContainersCreate(ctx context.Context, w http.ResponseWriter version := httputils.VersionFromContext(ctx) adjustCPUShares := version.LessThan("1.19") - ccr, err := s.daemon.ContainerCreate(name, config, hostConfig, adjustCPUShares) + ccr, err := s.daemon.ContainerCreate(&daemon.ContainerCreateConfig{ + Name: name, + Config: config, + HostConfig: hostConfig, + AdjustCPUShares: adjustCPUShares, + }) if err != nil { return err } diff --git a/builder/dockerfile/evaluator.go b/builder/dockerfile/evaluator.go index 0629ee9bbc..a27018915f 100644 --- a/builder/dockerfile/evaluator.go +++ b/builder/dockerfile/evaluator.go @@ -186,7 +186,7 @@ func platformSupports(command string) error { return nil } switch command { - case "expose", "volume", "user", "stopsignal", "arg": + case "expose", "user", "stopsignal", "arg": return fmt.Errorf("The daemon on this platform does not support the command '%s'", command) } return nil diff --git a/daemon/archive_unix.go b/daemon/archive_unix.go index 100fc78880..7588d76d15 100644 --- a/daemon/archive_unix.go +++ b/daemon/archive_unix.go @@ -8,7 +8,7 @@ package daemon func checkIfPathIsInAVolume(container *Container, absPath string) (bool, error) { var toVolume bool for _, mnt := range container.MountPoints { - if toVolume = mnt.hasResource(absPath); toVolume { + if toVolume = mnt.HasResource(absPath); toVolume { if mnt.RW { break } diff --git a/daemon/container.go b/daemon/container.go index 53807f90e6..a6bb4120ef 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "sync" "syscall" "time" @@ -30,8 +31,10 @@ import ( "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/pkg/system" "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" + "github.com/docker/docker/volume/store" ) var ( @@ -72,6 +75,7 @@ type CommonContainer struct { RestartCount int HasBeenStartedBefore bool HasBeenManuallyStopped bool // used for unless-stopped restart policy + MountPoints map[string]*volume.MountPoint hostConfig *runconfig.HostConfig command *execdriver.Command monitor *containerMonitor @@ -1108,29 +1112,109 @@ func (container *Container) mountVolumes() error { return nil } -func (container *Container) copyImagePathContent(v volume.Volume, destination string) error { - rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs) - if err != nil { - return err - } - - if _, err = ioutil.ReadDir(rootfs); err != nil { - if os.IsNotExist(err) { - return nil +func (container *Container) prepareMountPoints() error { + for _, config := range container.MountPoints { + if len(config.Driver) > 0 { + v, err := container.daemon.createVolume(config.Name, config.Driver, nil) + if err != nil { + return err + } + config.Volume = v } + } + return nil +} + +func (container *Container) removeMountPoints(rm bool) error { + var rmErrors []string + for _, m := range container.MountPoints { + if m.Volume == nil { + continue + } + container.daemon.volumes.Decrement(m.Volume) + if rm { + err := container.daemon.volumes.Remove(m.Volume) + // ErrVolumeInUse is ignored because having this + // volume being referenced by other container is + // not an error, but an implementation detail. + // This prevents docker from logging "ERROR: Volume in use" + // where there is another container using the volume. + if err != nil && err != store.ErrVolumeInUse { + rmErrors = append(rmErrors, err.Error()) + } + } + } + if len(rmErrors) > 0 { + return derr.ErrorCodeRemovingVolume.WithArgs(strings.Join(rmErrors, "\n")) + } + return nil +} + +func (container *Container) unmountVolumes(forceSyscall bool) error { + var ( + volumeMounts []volume.MountPoint + err error + ) + + for _, mntPoint := range container.MountPoints { + dest, err := container.GetResourcePath(mntPoint.Destination) + if err != nil { + return err + } + + volumeMounts = append(volumeMounts, volume.MountPoint{Destination: dest, Volume: mntPoint.Volume}) + } + + // Append any network mounts to the list (this is a no-op on Windows) + if volumeMounts, err = appendNetworkMounts(container, volumeMounts); err != nil { return err } - path, err := v.Mount() - if err != nil { - return err + for _, volumeMount := range volumeMounts { + if forceSyscall { + system.UnmountWithSyscall(volumeMount.Destination) + } + + if volumeMount.Volume != nil { + if err := volumeMount.Volume.Unmount(); err != nil { + return err + } + } } - if err := copyExistingContents(rootfs, path); err != nil { - return err - } + return nil +} - return v.Unmount() +func (container *Container) addBindMountPoint(name, source, destination string, rw bool) { + container.MountPoints[destination] = &volume.MountPoint{ + Name: name, + Source: source, + Destination: destination, + RW: rw, + } +} + +func (container *Container) addLocalMountPoint(name, destination string, rw bool) { + container.MountPoints[destination] = &volume.MountPoint{ + Name: name, + Driver: volume.DefaultDriverName, + Destination: destination, + RW: rw, + } +} + +func (container *Container) addMountPointWithVolume(destination string, vol volume.Volume, rw bool) { + container.MountPoints[destination] = &volume.MountPoint{ + Name: vol.Name(), + Driver: vol.DriverName(), + Destination: destination, + RW: rw, + Volume: vol, + } +} + +func (container *Container) isDestinationMounted(destination string) bool { + return container.MountPoints[destination] != nil } func (container *Container) stopSignal() int { diff --git a/daemon/container_unix.go b/daemon/container_unix.go index 4141f982c3..aa7aa92a1d 100644 --- a/daemon/container_unix.go +++ b/daemon/container_unix.go @@ -23,12 +23,12 @@ import ( "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/nat" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/ulimit" "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" "github.com/docker/docker/volume" - "github.com/docker/docker/volume/store" "github.com/docker/libnetwork" "github.com/docker/libnetwork/drivers/bridge" "github.com/docker/libnetwork/netlabel" @@ -54,9 +54,8 @@ type Container struct { AppArmorProfile string HostnamePath string HostsPath string - ShmPath string - MqueuePath string - MountPoints map[string]*mountPoint + ShmPath string // TODO Windows - Factor this out (GH15862) + MqueuePath string // TODO Windows - Factor this out (GH15862) ResolvConfPath string Volumes map[string]string // Deprecated since 1.7, kept for backwards compatibility @@ -1197,40 +1196,16 @@ func (container *Container) disconnectFromNetwork(n libnetwork.Network) error { return nil } -func (container *Container) unmountVolumes(forceSyscall bool) error { - var volumeMounts []mountPoint - - for _, mntPoint := range container.MountPoints { - dest, err := container.GetResourcePath(mntPoint.Destination) - if err != nil { - return err - } - - volumeMounts = append(volumeMounts, mountPoint{Destination: dest, Volume: mntPoint.Volume}) - } - +// appendNetworkMounts appends any network mounts to the array of mount points passed in +func appendNetworkMounts(container *Container, volumeMounts []volume.MountPoint) ([]volume.MountPoint, error) { for _, mnt := range container.networkMounts() { dest, err := container.GetResourcePath(mnt.Destination) if err != nil { - return err + return nil, err } - - volumeMounts = append(volumeMounts, mountPoint{Destination: dest}) + volumeMounts = append(volumeMounts, volume.MountPoint{Destination: dest}) } - - for _, volumeMount := range volumeMounts { - if forceSyscall { - syscall.Unmount(volumeMount.Destination, 0) - } - - if volumeMount.Volume != nil { - if err := volumeMount.Volume.Unmount(); err != nil { - return err - } - } - } - - return nil + return volumeMounts, nil } func (container *Container) networkMounts() []execdriver.Mount { @@ -1290,74 +1265,29 @@ func (container *Container) networkMounts() []execdriver.Mount { return mounts } -func (container *Container) addBindMountPoint(name, source, destination string, rw bool) { - container.MountPoints[destination] = &mountPoint{ - Name: name, - Source: source, - Destination: destination, - RW: rw, +func (container *Container) copyImagePathContent(v volume.Volume, destination string) error { + rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs) + if err != nil { + return err } -} -func (container *Container) addLocalMountPoint(name, destination string, rw bool) { - container.MountPoints[destination] = &mountPoint{ - Name: name, - Driver: volume.DefaultDriverName, - Destination: destination, - RW: rw, - } -} - -func (container *Container) addMountPointWithVolume(destination string, vol volume.Volume, rw bool) { - container.MountPoints[destination] = &mountPoint{ - Name: vol.Name(), - Driver: vol.DriverName(), - Destination: destination, - RW: rw, - Volume: vol, - } -} - -func (container *Container) isDestinationMounted(destination string) bool { - return container.MountPoints[destination] != nil -} - -func (container *Container) prepareMountPoints() error { - for _, config := range container.MountPoints { - if len(config.Driver) > 0 { - v, err := container.daemon.createVolume(config.Name, config.Driver, nil) - if err != nil { - return err - } - config.Volume = v + if _, err = ioutil.ReadDir(rootfs); err != nil { + if os.IsNotExist(err) { + return nil } + return err } - return nil -} -func (container *Container) removeMountPoints(rm bool) error { - var rmErrors []string - for _, m := range container.MountPoints { - if m.Volume == nil { - continue - } - container.daemon.volumes.Decrement(m.Volume) - if rm { - err := container.daemon.volumes.Remove(m.Volume) - // ErrVolumeInUse is ignored because having this - // volume being referenced by othe container is - // not an error, but an implementation detail. - // This prevents docker from logging "ERROR: Volume in use" - // where there is another container using the volume. - if err != nil && err != store.ErrVolumeInUse { - rmErrors = append(rmErrors, err.Error()) - } - } + path, err := v.Mount() + if err != nil { + return err } - if len(rmErrors) > 0 { - return derr.ErrorCodeRemovingVolume.WithArgs(strings.Join(rmErrors, "\n")) + + if err := copyExistingContents(rootfs, path); err != nil { + return err } - return nil + + return v.Unmount() } func (container *Container) shmPath() (string, error) { diff --git a/daemon/container_windows.go b/daemon/container_windows.go index f0d0c06ea1..178521087a 100644 --- a/daemon/container_windows.go +++ b/daemon/container_windows.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/daemon/execdriver" derr "github.com/docker/docker/errors" + "github.com/docker/docker/volume" "github.com/docker/libnetwork" ) @@ -169,18 +170,11 @@ func (container *Container) updateNetwork() error { func (container *Container) releaseNetwork() { } -func (container *Container) unmountVolumes(forceSyscall bool) error { - return nil -} - -// prepareMountPoints is a no-op on Windows -func (container *Container) prepareMountPoints() error { - return nil -} - -// removeMountPoints is a no-op on Windows. -func (container *Container) removeMountPoints(_ bool) error { - return nil +// appendNetworkMounts appends any network mounts to the array of mount points passed in. +// Windows does not support network mounts (not to be confused with SMB network mounts), so +// this is a no-op. +func appendNetworkMounts(container *Container, volumeMounts []volume.MountPoint) ([]volume.MountPoint, error) { + return volumeMounts, nil } func (container *Container) setupIpcDirs() error { diff --git a/daemon/create.go b/daemon/create.go index 83a1a8b7e4..61c60cfc93 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -15,26 +15,34 @@ import ( "github.com/opencontainers/runc/libcontainer/label" ) +// ContainerCreateConfig is the parameter set to ContainerCreate() +type ContainerCreateConfig struct { + Name string + Config *runconfig.Config + HostConfig *runconfig.HostConfig + AdjustCPUShares bool +} + // ContainerCreate takes configs and creates a container. -func (daemon *Daemon) ContainerCreate(name string, config *runconfig.Config, hostConfig *runconfig.HostConfig, adjustCPUShares bool) (types.ContainerCreateResponse, error) { - if config == nil { +func (daemon *Daemon) ContainerCreate(params *ContainerCreateConfig) (types.ContainerCreateResponse, error) { + if params.Config == nil { return types.ContainerCreateResponse{}, derr.ErrorCodeEmptyConfig } - warnings, err := daemon.verifyContainerSettings(hostConfig, config) + warnings, err := daemon.verifyContainerSettings(params.HostConfig, params.Config) if err != nil { return types.ContainerCreateResponse{"", warnings}, err } - daemon.adaptContainerSettings(hostConfig, adjustCPUShares) + daemon.adaptContainerSettings(params.HostConfig, params.AdjustCPUShares) - container, err := daemon.Create(config, hostConfig, name) + container, err := daemon.create(params) if err != nil { - if daemon.Graph().IsNotExist(err, config.Image) { - if strings.Contains(config.Image, "@") { - return types.ContainerCreateResponse{"", warnings}, derr.ErrorCodeNoSuchImageHash.WithArgs(config.Image) + if daemon.Graph().IsNotExist(err, params.Config.Image) { + if strings.Contains(params.Config.Image, "@") { + return types.ContainerCreateResponse{"", warnings}, derr.ErrorCodeNoSuchImageHash.WithArgs(params.Config.Image) } - img, tag := parsers.ParseRepositoryTag(config.Image) + img, tag := parsers.ParseRepositoryTag(params.Config.Image) if tag == "" { tag = tags.DefaultTag } @@ -47,7 +55,7 @@ func (daemon *Daemon) ContainerCreate(name string, config *runconfig.Config, hos } // Create creates a new container from the given configuration with a given name. -func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.HostConfig, name string) (retC *Container, retErr error) { +func (daemon *Daemon) create(params *ContainerCreateConfig) (retC *Container, retErr error) { var ( container *Container img *image.Image @@ -55,8 +63,8 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos err error ) - if config.Image != "" { - img, err = daemon.repositories.LookupImage(config.Image) + if params.Config.Image != "" { + img, err = daemon.repositories.LookupImage(params.Config.Image) if err != nil { return nil, err } @@ -66,20 +74,20 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos imgID = img.ID } - if err := daemon.mergeAndVerifyConfig(config, img); err != nil { + if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil { return nil, err } - if hostConfig == nil { - hostConfig = &runconfig.HostConfig{} + if params.HostConfig == nil { + params.HostConfig = &runconfig.HostConfig{} } - if hostConfig.SecurityOpt == nil { - hostConfig.SecurityOpt, err = daemon.generateSecurityOpt(hostConfig.IpcMode, hostConfig.PidMode) + if params.HostConfig.SecurityOpt == nil { + params.HostConfig.SecurityOpt, err = daemon.generateSecurityOpt(params.HostConfig.IpcMode, params.HostConfig.PidMode) if err != nil { return nil, err } } - if container, err = daemon.newContainer(name, config, imgID); err != nil { + if container, err = daemon.newContainer(params.Name, params.Config, imgID); err != nil { return nil, err } defer func() { @@ -96,7 +104,7 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos if err := daemon.createRootfs(container); err != nil { return nil, err } - if err := daemon.setHostConfig(container, hostConfig); err != nil { + if err := daemon.setHostConfig(container, params.HostConfig); err != nil { return nil, err } defer func() { @@ -111,7 +119,7 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos } defer container.Unmount() - if err := createContainerPlatformSpecificSettings(container, config, hostConfig, img); err != nil { + if err := createContainerPlatformSpecificSettings(container, params.Config, params.HostConfig, img); err != nil { return nil, err } diff --git a/daemon/create_unix.go b/daemon/create_unix.go index 66138db967..85fb1cdbee 100644 --- a/daemon/create_unix.go +++ b/daemon/create_unix.go @@ -16,9 +16,11 @@ import ( // createContainerPlatformSpecificSettings performs platform specific container create functionality func createContainerPlatformSpecificSettings(container *Container, config *runconfig.Config, hostConfig *runconfig.HostConfig, img *image.Image) error { + var name, destination string + for spec := range config.Volumes { - name := stringid.GenerateNonCryptoID() - destination := filepath.Clean(spec) + name = stringid.GenerateNonCryptoID() + destination = filepath.Clean(spec) // Skip volumes for which we already have something mounted on that // destination because of a --volume-from. diff --git a/daemon/create_windows.go b/daemon/create_windows.go index 21aac13d31..1ea465f71c 100644 --- a/daemon/create_windows.go +++ b/daemon/create_windows.go @@ -1,11 +1,83 @@ package daemon import ( + "fmt" + "github.com/docker/docker/image" + "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/runconfig" + "github.com/docker/docker/volume" ) // createContainerPlatformSpecificSettings performs platform specific container create functionality func createContainerPlatformSpecificSettings(container *Container, config *runconfig.Config, hostConfig *runconfig.HostConfig, img *image.Image) error { + for spec := range config.Volumes { + + mp, err := volume.ParseMountSpec(spec, hostConfig.VolumeDriver) + if err != nil { + return fmt.Errorf("Unrecognised volume spec: %v", err) + } + + // If the mountpoint doesn't have a name, generate one. + if len(mp.Name) == 0 { + mp.Name = stringid.GenerateNonCryptoID() + } + + // Skip volumes for which we already have something mounted on that + // destination because of a --volume-from. + if container.isDestinationMounted(mp.Destination) { + continue + } + + volumeDriver := hostConfig.VolumeDriver + if mp.Destination != "" && img != nil { + if _, ok := img.ContainerConfig.Volumes[mp.Destination]; ok { + // check for whether bind is not specified and then set to local + if _, ok := container.MountPoints[mp.Destination]; !ok { + volumeDriver = volume.DefaultDriverName + } + } + } + + // Create the volume in the volume driver. If it doesn't exist, + // a new one will be created. + v, err := container.daemon.createVolume(mp.Name, volumeDriver, nil) + if err != nil { + return err + } + + // FIXME Windows: This code block is present in the Linux version and + // allows the contents to be copied to the container FS prior to it + // being started. However, the function utilises the FollowSymLinkInScope + // path which does not cope with Windows volume-style file paths. There + // is a seperate effort to resolve this (@swernli), so this processing + // is deferred for now. A case where this would be useful is when + // a dockerfile includes a VOLUME statement, but something is created + // in that directory during the dockerfile processing. What this means + // on Windows for TP4 is that in that scenario, the contents will not + // copied, but that's (somewhat) OK as HCS will bomb out soon after + // at it doesn't support mapped directories which have contents in the + // destination path anyway. + // + // Example for repro later: + // FROM windowsservercore + // RUN mkdir c:\myvol + // RUN copy c:\windows\system32\ntdll.dll c:\myvol + // VOLUME "c:\myvol" + // + // Then + // docker build -t vol . + // docker run -it --rm vol cmd <-- This is where HCS will error out. + // + // // never attempt to copy existing content in a container FS to a shared volume + // if v.DriverName() == volume.DefaultDriverName { + // if err := container.copyImagePathContent(v, mp.Destination); err != nil { + // return err + // } + // } + + // Add it to container.MountPoints + container.addMountPointWithVolume(mp.Destination, v, mp.RW) + } return nil } diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go index 38de83cef8..4291edee45 100644 --- a/daemon/daemon_unix.go +++ b/daemon/daemon_unix.go @@ -22,6 +22,7 @@ import ( "github.com/docker/docker/pkg/sysinfo" "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" + "github.com/docker/docker/volume" "github.com/docker/libnetwork" nwconfig "github.com/docker/libnetwork/config" "github.com/docker/libnetwork/drivers/bridge" @@ -603,10 +604,10 @@ func (daemon *Daemon) newBaseContainer(id string) Container { State: NewState(), execCommands: newExecStore(), root: daemon.containerRoot(id), + MountPoints: make(map[string]*volume.MountPoint), }, - MountPoints: make(map[string]*mountPoint), - Volumes: make(map[string]string), - VolumesRW: make(map[string]bool), + Volumes: make(map[string]string), + VolumesRW: make(map[string]bool), } } diff --git a/daemon/daemonbuilder/builder.go b/daemon/daemonbuilder/builder.go index 6786401631..fab97face2 100644 --- a/daemon/daemonbuilder/builder.go +++ b/daemon/daemonbuilder/builder.go @@ -83,7 +83,12 @@ func (d Docker) Container(id string) (*daemon.Container, error) { // Create creates a new Docker container and returns potential warnings func (d Docker) Create(cfg *runconfig.Config, hostCfg *runconfig.HostConfig) (*daemon.Container, []string, error) { - ccr, err := d.Daemon.ContainerCreate("", cfg, hostCfg, true) + ccr, err := d.Daemon.ContainerCreate(&daemon.ContainerCreateConfig{ + Name: "", + Config: cfg, + HostConfig: hostCfg, + AdjustCPUShares: true, + }) if err != nil { return nil, nil, err } diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index e88ea0bde7..462666b392 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -165,16 +165,8 @@ type ResourceStats struct { SystemUsage uint64 `json:"system_usage"` } -// Mount contains information for a mount operation. -type Mount struct { - Source string `json:"source"` - Destination string `json:"destination"` - Writable bool `json:"writable"` - Private bool `json:"private"` - Slave bool `json:"slave"` -} - // User contains the uid and gid representing a Unix user +// TODO Windows: Factor out User type User struct { UID int `json:"root_uid"` GID int `json:"root_gid"` diff --git a/daemon/execdriver/driver_unix.go b/daemon/execdriver/driver_unix.go index 3baa3e0965..2ebf95fc84 100644 --- a/daemon/execdriver/driver_unix.go +++ b/daemon/execdriver/driver_unix.go @@ -18,6 +18,15 @@ import ( "github.com/opencontainers/runc/libcontainer/configs" ) +// Mount contains information for a mount operation. +type Mount struct { + Source string `json:"source"` + Destination string `json:"destination"` + Writable bool `json:"writable"` + Private bool `json:"private"` + Slave bool `json:"slave"` +} + // Network settings of the container type Network struct { Mtu int `json:"mtu"` diff --git a/daemon/execdriver/driver_windows.go b/daemon/execdriver/driver_windows.go index 08568bc242..b422bcd79a 100644 --- a/daemon/execdriver/driver_windows.go +++ b/daemon/execdriver/driver_windows.go @@ -2,6 +2,13 @@ package execdriver import "github.com/docker/docker/pkg/nat" +// Mount contains information for a mount operation. +type Mount struct { + Source string `json:"source"` + Destination string `json:"destination"` + Writable bool `json:"writable"` +} + // Network settings of the container type Network struct { Interface *NetworkInterface `json:"interface"` diff --git a/daemon/execdriver/windows/run.go b/daemon/execdriver/windows/run.go index 98d8d1c921..3b86109b2c 100644 --- a/daemon/execdriver/windows/run.go +++ b/daemon/execdriver/windows/run.go @@ -2,8 +2,6 @@ package windows -// Note this is alpha code for the bring up of containers on Windows. - import ( "encoding/json" "errors" @@ -60,18 +58,25 @@ type device struct { Settings interface{} } +type mappedDir struct { + HostPath string + ContainerPath string + ReadOnly bool +} + type containerInit struct { - SystemType string // HCS requires this to be hard-coded to "Container" - Name string // Name of the container. We use the docker ID. - Owner string // The management platform that created this container - IsDummy bool // Used for development purposes. - VolumePath string // Windows volume path for scratch space - Devices []device // Devices used by the container - IgnoreFlushesDuringBoot bool // Optimisation hint for container startup in Windows - LayerFolderPath string // Where the layer folders are located - Layers []layer // List of storage layers - ProcessorWeight int64 // CPU Shares 1..9 on Windows; or 0 is platform default. - HostName string // Hostname + SystemType string // HCS requires this to be hard-coded to "Container" + Name string // Name of the container. We use the docker ID. + Owner string // The management platform that created this container + IsDummy bool // Used for development purposes. + VolumePath string // Windows volume path for scratch space + Devices []device // Devices used by the container + IgnoreFlushesDuringBoot bool // Optimisation hint for container startup in Windows + LayerFolderPath string // Where the layer folders are located + Layers []layer // List of storage layers + ProcessorWeight int64 // CPU Shares 1..9 on Windows; or 0 is platform default. + HostName string // Hostname + MappedDirectories []mappedDir // List of mapped directories (volumes/mounts) } // defaultOwner is a tag passed to HCS to allow it to differentiate between @@ -105,18 +110,28 @@ func (d *Driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, hooks execd HostName: c.Hostname, } - for i := 0; i < len(c.LayerPaths); i++ { - _, filename := filepath.Split(c.LayerPaths[i]) + for _, layerPath := range c.LayerPaths { + _, filename := filepath.Split(layerPath) g, err := hcsshim.NameToGuid(filename) if err != nil { return execdriver.ExitStatus{ExitCode: -1}, err } cu.Layers = append(cu.Layers, layer{ ID: g.ToString(), - Path: c.LayerPaths[i], + Path: layerPath, }) } + // Add the mounts (volumes, bind mounts etc) to the structure + mds := make([]mappedDir, len(c.Mounts)) + for i, mount := range c.Mounts { + mds[i] = mappedDir{ + HostPath: mount.Source, + ContainerPath: mount.Destination, + ReadOnly: !mount.Writable} + } + cu.MappedDirectories = mds + // TODO Windows. At some point, when there is CLI on docker run to // enable the IP Address of the container to be passed into docker run, // the IP Address needs to be wired through to HCS in the JSON. It diff --git a/daemon/inspect_windows.go b/daemon/inspect_windows.go index 654d78c35e..26b386160d 100644 --- a/daemon/inspect_windows.go +++ b/daemon/inspect_windows.go @@ -8,7 +8,17 @@ func setPlatformSpecificContainerFields(container *Container, contJSONBase *type } func addMountPoints(container *Container) []types.MountPoint { - return nil + mountPoints := make([]types.MountPoint, 0, len(container.MountPoints)) + for _, m := range container.MountPoints { + mountPoints = append(mountPoints, types.MountPoint{ + Name: m.Name, + Source: m.Path(), + Destination: m.Destination, + Driver: m.Driver, + RW: m.RW, + }) + } + return mountPoints } // ContainerInspectPre120 get containers for pre 1.20 APIs. diff --git a/daemon/volumes.go b/daemon/volumes.go index 54aca3522b..cca889830f 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -2,18 +2,16 @@ package daemon import ( "errors" - "fmt" - "io/ioutil" "os" "path/filepath" "strings" - "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" + "github.com/docker/docker/daemon/execdriver" derr "github.com/docker/docker/errors" - "github.com/docker/docker/pkg/chrootarchive" - "github.com/docker/docker/pkg/system" + "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" + "github.com/opencontainers/runc/libcontainer/label" ) var ( @@ -22,82 +20,7 @@ var ( ErrVolumeReadonly = errors.New("mounted volume is marked read-only") ) -// mountPoint is the intersection point between a volume and a container. It -// specifies which volume is to be used and where inside a container it should -// be mounted. -type mountPoint struct { - Name string - Destination string - Driver string - RW bool - Volume volume.Volume `json:"-"` - Source string - Mode string `json:"Relabel"` // Originally field was `Relabel`" -} - -// Setup sets up a mount point by either mounting the volume if it is -// configured, or creating the source directory if supplied. -func (m *mountPoint) Setup() (string, error) { - if m.Volume != nil { - return m.Volume.Mount() - } - - if len(m.Source) > 0 { - if _, err := os.Stat(m.Source); err != nil { - if !os.IsNotExist(err) { - return "", err - } - logrus.Warnf("Auto-creating non-existant volume host path %s, this is deprecated and will be removed soon", m.Source) - if err := system.MkdirAll(m.Source, 0755); err != nil { - return "", err - } - } - return m.Source, nil - } - - return "", derr.ErrorCodeMountSetup -} - -// 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 { - relPath, err := filepath.Rel(m.Destination, absolutePath) - - return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator)) -} - -// Path returns the path of a volume in a mount point. -func (m *mountPoint) Path() string { - if m.Volume != nil { - return m.Volume.Path() - } - - return m.Source -} - -// copyExistingContents copies from the source to the destination and -// ensures the ownership is appropriately set. -func copyExistingContents(source, destination string) error { - volList, err := ioutil.ReadDir(source) - if err != nil { - return err - } - if len(volList) > 0 { - srcList, err := ioutil.ReadDir(destination) - if err != nil { - return err - } - if len(srcList) == 0 { - // If the source volume is empty copy files from the root into the volume - if err := chrootarchive.CopyWithTar(source, destination); err != nil { - return err - } - } - } - return copyOwnership(source, destination) -} +type mounts []execdriver.Mount // volumeToAPIType converts a volume.Volume to the type used by the remote API func volumeToAPIType(v volume.Volume) *types.Volume { @@ -107,3 +30,126 @@ func volumeToAPIType(v volume.Volume) *types.Volume { Mountpoint: v.Path(), } } + +// createVolume creates a volume. +func (daemon *Daemon) createVolume(name, driverName string, opts map[string]string) (volume.Volume, error) { + v, err := daemon.volumes.Create(name, driverName, opts) + if err != nil { + return nil, err + } + daemon.volumes.Increment(v) + return v, nil +} + +// Len returns the number of mounts. Used in sorting. +func (m mounts) Len() int { + return len(m) +} + +// Less returns true if the number of parts (a/b/c would be 3 parts) in the +// mount indexed by parameter 1 is less than that of the mount indexed by +// parameter 2. Used in sorting. +func (m mounts) Less(i, j int) bool { + return m.parts(i) < m.parts(j) +} + +// Swap swaps two items in an array of mounts. Used in sorting +func (m mounts) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} + +// parts returns the number of parts in the destination of a mount. Used in sorting. +func (m mounts) parts(i int) int { + return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator)) +} + +// registerMountPoints initializes the container mount points with the configured volumes and bind mounts. +// It follows the next sequence to decide what to mount in each final destination: +// +// 1. Select the previously configured mount points for the containers, if any. +// 2. Select the volumes mounted from another containers. Overrides previously configured mount point destination. +// 3. Select the bind mounts set by the client. Overrides previously configured mount point destinations. +func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runconfig.HostConfig) error { + binds := map[string]bool{} + mountPoints := map[string]*volume.MountPoint{} + + // 1. Read already configured mount points. + for name, point := range container.MountPoints { + mountPoints[name] = point + } + + // 2. Read volumes from other containers. + for _, v := range hostConfig.VolumesFrom { + containerID, mode, err := volume.ParseVolumesFrom(v) + if err != nil { + return err + } + + c, err := daemon.Get(containerID) + if err != nil { + return err + } + + for _, m := range c.MountPoints { + cp := &volume.MountPoint{ + Name: m.Name, + Source: m.Source, + RW: m.RW && volume.ReadWrite(mode), + Driver: m.Driver, + Destination: m.Destination, + } + + if len(cp.Source) == 0 { + v, err := daemon.createVolume(cp.Name, cp.Driver, nil) + if err != nil { + return err + } + cp.Volume = v + } + + mountPoints[cp.Destination] = cp + } + } + + // 3. Read bind mounts + for _, b := range hostConfig.Binds { + // #10618 + bind, err := volume.ParseMountSpec(b, hostConfig.VolumeDriver) + if err != nil { + return err + } + + if binds[bind.Destination] { + return derr.ErrorCodeVolumeDup.WithArgs(bind.Destination) + } + + if len(bind.Name) > 0 && len(bind.Driver) > 0 { + // create the volume + v, err := daemon.createVolume(bind.Name, bind.Driver, nil) + if err != nil { + return err + } + bind.Volume = v + bind.Source = v.Path() + // bind.Name is an already existing volume, we need to use that here + bind.Driver = v.DriverName() + bind = setBindModeIfNull(bind) + } + shared := label.IsShared(bind.Mode) + if err := label.Relabel(bind.Source, container.MountLabel, shared); err != nil { + return err + } + binds[bind.Destination] = true + mountPoints[bind.Destination] = bind + } + + bcVolumes, bcVolumesRW := configureBackCompatStructures(daemon, container, mountPoints) + + container.Lock() + container.MountPoints = mountPoints + setBackCompatStructures(container, bcVolumes, bcVolumesRW) + + container.Unlock() + + return nil +} diff --git a/daemon/volumes_linux_unit_test.go b/daemon/volumes_linux_unit_test.go deleted file mode 100644 index 6f4ab882dc..0000000000 --- a/daemon/volumes_linux_unit_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// +build experimental - -package daemon - -import "testing" - -func TestParseBindMount(t *testing.T) { - cases := []struct { - bind string - driver string - expDest string - expSource string - expName string - expDriver string - expRW bool - fail bool - }{ - {"/tmp:/tmp", "", "/tmp", "/tmp", "", "", true, false}, - {"/tmp:/tmp:ro", "", "/tmp", "/tmp", "", "", false, false}, - {"/tmp:/tmp:rw", "", "/tmp", "/tmp", "", "", true, false}, - {"/tmp:/tmp:foo", "", "/tmp", "/tmp", "", "", false, true}, - {"name:/tmp", "", "/tmp", "", "name", "local", true, false}, - {"name:/tmp", "external", "/tmp", "", "name", "external", true, false}, - {"name:/tmp:ro", "local", "/tmp", "", "name", "local", false, false}, - {"local/name:/tmp:rw", "", "/tmp", "", "local/name", "local", true, false}, - {"/tmp:tmp", "", "", "", "", "", true, true}, - } - - for _, c := range cases { - m, err := parseBindMount(c.bind, c.driver) - if c.fail { - if err == nil { - t.Fatalf("Expected error, was nil, for spec %s\n", c.bind) - } - continue - } - - if m.Destination != c.expDest { - t.Fatalf("Expected destination %s, was %s, for spec %s\n", c.expDest, m.Destination, c.bind) - } - - if m.Source != c.expSource { - t.Fatalf("Expected source %s, was %s, for spec %s\n", c.expSource, m.Source, c.bind) - } - - if m.Name != c.expName { - t.Fatalf("Expected name %s, was %s for spec %s\n", c.expName, m.Name, c.bind) - } - - if m.Driver != c.expDriver { - t.Fatalf("Expected driver %s, was %s, for spec %s\n", c.expDriver, m.Driver, c.bind) - } - - if m.RW != c.expRW { - t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind) - } - } -} diff --git a/daemon/volumes_unit_test.go b/daemon/volumes_unit_test.go index 9420989b1c..dadf24e144 100644 --- a/daemon/volumes_unit_test.go +++ b/daemon/volumes_unit_test.go @@ -1,6 +1,9 @@ package daemon -import "testing" +import ( + "github.com/docker/docker/volume" + "testing" +) func TestParseVolumesFrom(t *testing.T) { cases := []struct { @@ -17,7 +20,7 @@ func TestParseVolumesFrom(t *testing.T) { } for _, c := range cases { - id, mode, err := parseVolumesFrom(c.spec) + id, mode, err := volume.ParseVolumesFrom(c.spec) if c.fail { if err == nil { t.Fatalf("Expected error, was nil, for spec %s\n", c.spec) diff --git a/daemon/volumes_unix.go b/daemon/volumes_unix.go index 3b32c65f9a..b2a3c27b67 100644 --- a/daemon/volumes_unix.go +++ b/daemon/volumes_unix.go @@ -11,15 +11,35 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/execdriver" - derr "github.com/docker/docker/errors" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/system" - "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" volumedrivers "github.com/docker/docker/volume/drivers" "github.com/docker/docker/volume/local" - "github.com/opencontainers/runc/libcontainer/label" ) +// copyExistingContents copies from the source to the destination and +// ensures the ownership is appropriately set. +func copyExistingContents(source, destination string) error { + volList, err := ioutil.ReadDir(source) + if err != nil { + return err + } + if len(volList) > 0 { + srcList, err := ioutil.ReadDir(destination) + if err != nil { + return err + } + if len(srcList) == 0 { + // If the source volume is empty copy files from the root into the volume + if err := chrootarchive.CopyWithTar(source, destination); err != nil { + return err + } + } + } + return copyOwnership(source, destination) +} + // copyOwnership copies the permissions and uid:gid of the source file // to the destination file func copyOwnership(source, destination string) error { @@ -68,53 +88,6 @@ func (container *Container) setupMounts() ([]execdriver.Mount, error) { return append(mounts, netMounts...), nil } -// parseBindMount validates the configuration of mount information in runconfig is valid. -func parseBindMount(spec, volumeDriver string) (*mountPoint, error) { - bind := &mountPoint{ - RW: true, - } - arr := strings.Split(spec, ":") - - switch len(arr) { - case 2: - bind.Destination = arr[1] - case 3: - bind.Destination = arr[1] - mode := arr[2] - if !volume.ValidMountMode(mode) { - return nil, derr.ErrorCodeVolumeInvalidMode.WithArgs(mode) - } - bind.RW = volume.ReadWrite(mode) - // Mode field is used by SELinux to decide whether to apply label - bind.Mode = mode - default: - return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) - } - - //validate the volumes destination path - if !filepath.IsAbs(bind.Destination) { - return nil, derr.ErrorCodeVolumeAbs.WithArgs(bind.Destination) - } - - name, source, err := parseVolumeSource(arr[0]) - if err != nil { - return nil, err - } - - if len(source) == 0 { - bind.Driver = volumeDriver - if len(bind.Driver) == 0 { - bind.Driver = volume.DefaultDriverName - } - } else { - bind.Source = filepath.Clean(source) - } - - bind.Name = name - bind.Destination = filepath.Clean(bind.Destination) - return bind, nil -} - // sortMounts sorts an array of mounts in lexicographic order. This ensure that // when mounting, the mounts don't shadow other mounts. For example, if mounting // /etc and /etc/resolv.conf, /etc/resolv.conf must not be mounted first. @@ -123,30 +96,6 @@ func sortMounts(m []execdriver.Mount) []execdriver.Mount { return m } -type mounts []execdriver.Mount - -// Len returns the number of mounts -func (m mounts) Len() int { - return len(m) -} - -// Less returns true if the number of parts (a/b/c would be 3 parts) in the -// mount indexed by parameter 1 is less than that of the mount indexed by -// parameter 2. -func (m mounts) Less(i, j int) bool { - return m.parts(i) < m.parts(j) -} - -// Swap swaps two items in an array of mounts. -func (m mounts) Swap(i, j int) { - m[i], m[j] = m[j], m[i] -} - -// parts returns the number of parts in the destination of a mount. -func (m mounts) parts(i int) int { - return len(strings.Split(filepath.Clean(m[i].Destination), string(os.PathSeparator))) -} - // migrateVolume links the contents of a volume created pre Docker 1.7 // into the location expected by the local driver. // It creates a symlink from DOCKER_ROOT/vfs/dir/VOLUME_ID to DOCKER_ROOT/volumes/VOLUME_ID/_container_data. @@ -211,12 +160,7 @@ func (daemon *Daemon) verifyVolumesInfo(container *Container) error { } container.addLocalMountPoint(id, destination, rw) } else { // Bind mount - id, source, err := parseVolumeSource(hostPath) - // We should not find an error here coming - // from the old configuration, but who knows. - if err != nil { - return err - } + id, source := volume.ParseVolumeSource(hostPath) container.addBindMountPoint(id, source, destination, rw) } } @@ -270,109 +214,19 @@ func (daemon *Daemon) verifyVolumesInfo(container *Container) error { return nil } -// parseVolumesFrom ensure that the supplied volumes-from is valid. -func parseVolumesFrom(spec string) (string, string, error) { - if len(spec) == 0 { - return "", "", derr.ErrorCodeVolumeFromBlank.WithArgs(spec) +// setBindModeIfNull is platform specific processing to ensure the +// shared mode is set to 'z' if it is null. This is called in the case +// of processing a named volume and not a typical bind. +func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint { + if bind.Mode == "" { + bind.Mode = "z" } - - specParts := strings.SplitN(spec, ":", 2) - id := specParts[0] - mode := "rw" - - if len(specParts) == 2 { - mode = specParts[1] - if !volume.ValidMountMode(mode) { - return "", "", derr.ErrorCodeVolumeMode.WithArgs(mode) - } - } - return id, mode, nil + return bind } -// registerMountPoints initializes the container mount points with the configured volumes and bind mounts. -// It follows the next sequence to decide what to mount in each final destination: -// -// 1. Select the previously configured mount points for the containers, if any. -// 2. Select the volumes mounted from another containers. Overrides previously configured mount point destination. -// 3. Select the bind mounts set by the client. Overrides previously configured mount point destinations. -func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runconfig.HostConfig) error { - binds := map[string]bool{} - mountPoints := map[string]*mountPoint{} - - // 1. Read already configured mount points. - for name, point := range container.MountPoints { - mountPoints[name] = point - } - - // 2. Read volumes from other containers. - for _, v := range hostConfig.VolumesFrom { - containerID, mode, err := parseVolumesFrom(v) - if err != nil { - return err - } - - c, err := daemon.Get(containerID) - if err != nil { - return err - } - - for _, m := range c.MountPoints { - cp := &mountPoint{ - Name: m.Name, - Source: m.Source, - RW: m.RW && volume.ReadWrite(mode), - Driver: m.Driver, - Destination: m.Destination, - } - - if len(cp.Source) == 0 { - v, err := daemon.createVolume(cp.Name, cp.Driver, nil) - if err != nil { - return err - } - cp.Volume = v - } - - mountPoints[cp.Destination] = cp - } - } - - // 3. Read bind mounts - for _, b := range hostConfig.Binds { - // #10618 - bind, err := parseBindMount(b, hostConfig.VolumeDriver) - if err != nil { - return err - } - - if binds[bind.Destination] { - return derr.ErrorCodeVolumeDup.WithArgs(bind.Destination) - } - - if len(bind.Name) > 0 && len(bind.Driver) > 0 { - // create the volume - v, err := daemon.createVolume(bind.Name, bind.Driver, nil) - if err != nil { - return err - } - bind.Volume = v - bind.Source = v.Path() - // bind.Name is an already existing volume, we need to use that here - bind.Driver = v.DriverName() - // Since this is just a named volume and not a typical bind, set to shared mode `z` - if bind.Mode == "" { - bind.Mode = "z" - } - } - - shared := label.IsShared(bind.Mode) - if err := label.Relabel(bind.Source, container.MountLabel, shared); err != nil { - return err - } - binds[bind.Destination] = true - mountPoints[bind.Destination] = bind - } - +// configureBackCompatStructures is platform specific processing for +// registering mount points to populate old structures. +func configureBackCompatStructures(daemon *Daemon, container *Container, mountPoints map[string]*volume.MountPoint) (map[string]string, map[string]bool) { // Keep backwards compatible structures bcVolumes := map[string]string{} bcVolumesRW := map[string]bool{} @@ -387,38 +241,12 @@ func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runc } } } + return bcVolumes, bcVolumesRW +} - container.Lock() - container.MountPoints = mountPoints +// setBackCompatStructures is a platform specific helper function to set +// backwards compatible structures in the container when registering volumes. +func setBackCompatStructures(container *Container, bcVolumes map[string]string, bcVolumesRW map[string]bool) { container.Volumes = bcVolumes container.VolumesRW = bcVolumesRW - container.Unlock() - - return nil -} - -// createVolume creates a volume. -func (daemon *Daemon) createVolume(name, driverName string, opts map[string]string) (volume.Volume, error) { - v, err := daemon.volumes.Create(name, driverName, opts) - if err != nil { - return nil, err - } - daemon.volumes.Increment(v) - return v, nil -} - -// parseVolumeSource parses the origin sources that's mounted into the container. -func parseVolumeSource(spec string) (string, string, error) { - if !filepath.IsAbs(spec) { - return spec, "", nil - } - - return "", spec, nil -} - -// 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 == volume.DefaultDriverName } diff --git a/daemon/volumes_windows.go b/daemon/volumes_windows.go index df122ecbac..5c6182da53 100644 --- a/daemon/volumes_windows.go +++ b/daemon/volumes_windows.go @@ -4,22 +4,35 @@ package daemon import ( "github.com/docker/docker/daemon/execdriver" - "github.com/docker/docker/runconfig" + derr "github.com/docker/docker/errors" + "github.com/docker/docker/volume" + "sort" ) -// copyOwnership copies the permissions and group of a source file to the -// destination file. This is a no-op on Windows. -func copyOwnership(source, destination string) error { - return nil -} - -// setupMounts configures the mount points for a container. -// setupMounts on Linux iterates through each of the mount points for a -// container and calls Setup() on each. It also looks to see if is a network -// mount such as /etc/resolv.conf, and if it is not, appends it to the array -// of mounts. As Windows does not support mount points, this is a no-op. +// setupMounts configures the mount points for a container by appending each +// of the configured mounts on the container to the execdriver mount structure +// which will ultimately be passed into the exec driver during container creation. +// It also ensures each of the mounts are lexographically sorted. func (container *Container) setupMounts() ([]execdriver.Mount, error) { - return nil, nil + var mnts []execdriver.Mount + for _, mount := range container.MountPoints { // type is volume.MountPoint + // If there is no source, take it from the volume path + s := mount.Source + if s == "" && mount.Volume != nil { + s = mount.Volume.Path() + } + if s == "" { + return nil, derr.ErrorCodeVolumeNoSourceForMount.WithArgs(mount.Name, mount.Driver, mount.Destination) + } + mnts = append(mnts, execdriver.Mount{ + Source: s, + Destination: mount.Destination, + Writable: mount.RW, + }) + } + + sort.Sort(mounts(mnts)) + return mnts, nil } // verifyVolumesInfo ports volumes configured for the containers pre docker 1.7. @@ -28,9 +41,20 @@ func (daemon *Daemon) verifyVolumesInfo(container *Container) error { return nil } -// registerMountPoints initializes the container mount points with the -// configured volumes and bind mounts. Windows does not support volumes or -// mount points. -func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runconfig.HostConfig) error { - return nil +// setBindModeIfNull is platform specific processing which is a no-op on +// Windows. +func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint { + return bind +} + +// configureBackCompatStructures is platform specific processing for +// registering mount points to populate old structures. This is a no-op on Windows. +func configureBackCompatStructures(*Daemon, *Container, map[string]*volume.MountPoint) (map[string]string, map[string]bool) { + return nil, nil +} + +// setBackCompatStructures is a platform specific helper function to set +// backwards compatible structures in the container when registering volumes. +// This is a no-op on Windows. +func setBackCompatStructures(*Container, map[string]string, map[string]bool) { } diff --git a/errors/daemon.go b/errors/daemon.go index ea31580094..4673ad306d 100644 --- a/errors/daemon.go +++ b/errors/daemon.go @@ -359,12 +359,12 @@ var ( HTTPStatusCode: http.StatusInternalServerError, }) - // ErrorCodeVolumeInvalidMode is generated when we the mode of a volume + // ErrorCodeVolumeInvalidMode is generated when we the mode of a volume/bind // mount is invalid. ErrorCodeVolumeInvalidMode = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "VOLUMEINVALIDMODE", - Message: "invalid mode for volumes-from: %s", - Description: "An invalid 'mode' was specified in the mount request", + Message: "invalid mode: %s", + Description: "An invalid 'mode' was specified", HTTPStatusCode: http.StatusInternalServerError, }) @@ -393,6 +393,41 @@ var ( HTTPStatusCode: http.StatusBadRequest, }) + // ErrorCodeVolumeSlash is generated when destination path to a volume is / + ErrorCodeVolumeSlash = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "VOLUMESLASH", + Message: "Invalid specification: destination can't be '/' in '%s'", + HTTPStatusCode: http.StatusInternalServerError, + }) + + // ErrorCodeVolumeDestIsC is generated the destination is c: (Windows specific) + ErrorCodeVolumeDestIsC = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "VOLUMEDESTISC", + Message: "Destination drive letter in '%s' cannot be c:", + HTTPStatusCode: http.StatusInternalServerError, + }) + + // ErrorCodeVolumeDestIsCRoot is generated the destination path is c:\ (Windows specific) + ErrorCodeVolumeDestIsCRoot = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "VOLUMEDESTISCROOT", + Message: `Destination path in '%s' cannot be c:\`, + HTTPStatusCode: http.StatusInternalServerError, + }) + + // ErrorCodeVolumeSourceNotFound is generated the source directory could not be found (Windows specific) + ErrorCodeVolumeSourceNotFound = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "VOLUMESOURCENOTFOUND", + Message: "Source directory '%s' could not be found: %v", + HTTPStatusCode: http.StatusInternalServerError, + }) + + // ErrorCodeVolumeSourceNotDirectory is generated the source is not a directory (Windows specific) + ErrorCodeVolumeSourceNotDirectory = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "VOLUMESOURCENOTDIRECTORY", + Message: "Source '%s' is not a directory", + HTTPStatusCode: http.StatusInternalServerError, + }) + // ErrorCodeVolumeFromBlank is generated when path to a volume is blank. ErrorCodeVolumeFromBlank = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "VOLUMEFROMBLANK", @@ -401,15 +436,6 @@ var ( HTTPStatusCode: http.StatusInternalServerError, }) - // ErrorCodeVolumeMode is generated when 'mode' for a volume - // isn't a valid. - ErrorCodeVolumeMode = errcode.Register(errGroup, errcode.ErrorDescriptor{ - Value: "VOLUMEMODE", - Message: "invalid mode for volumes-from: %s", - Description: "An invalid 'mode' path was specified in the mount request", - HTTPStatusCode: http.StatusInternalServerError, - }) - // ErrorCodeVolumeDup is generated when we try to mount two volumes // to the same path. ErrorCodeVolumeDup = errcode.Register(errGroup, errcode.ErrorDescriptor{ @@ -419,6 +445,22 @@ var ( HTTPStatusCode: http.StatusInternalServerError, }) + // ErrorCodeVolumeNoSourceForMount is generated when no source directory + // for a volume mount was found. (Windows specific) + ErrorCodeVolumeNoSourceForMount = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "VOLUMENOSOURCEFORMOUNT", + Message: "No source for mount name %q driver %q destination %s", + HTTPStatusCode: http.StatusInternalServerError, + }) + + // ErrorCodeVolumeNameReservedWord is generated when the name in a volume + // uses a reserved word for filenames. (Windows specific) + ErrorCodeVolumeNameReservedWord = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "VOLUMENAMERESERVEDWORD", + Message: "Volume name %q cannot be a reserved word for Windows filenames", + HTTPStatusCode: http.StatusInternalServerError, + }) + // ErrorCodeCantUnpause is generated when there's an error while trying // to unpause a container. ErrorCodeCantUnpause = errcode.Register(errGroup, errcode.ErrorDescriptor{ diff --git a/image/fixtures/pre1.9/expected_config b/image/fixtures/pre1.9/expected_config index 83fc30487a..121efe1fe6 100644 --- a/image/fixtures/pre1.9/expected_config +++ b/image/fixtures/pre1.9/expected_config @@ -1 +1,2 @@ {"architecture":"amd64","config":{"Hostname":"03797203757d","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":null,"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"OnBuild":[],"Labels":{}},"container":"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253","container_config":{"Hostname":"03797203757d","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":["/bin/sh","-c","#(nop) ENTRYPOINT [\"/go/bin/dnsdock\"]"],"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"OnBuild":[],"Labels":{}},"created":"2015-08-19T16:49:11.368300679Z","docker_version":"1.6.2","layer_id":"sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a","os":"linux","parent_id":"sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02"} + diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 80d4ad26fd..e167f00416 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -293,8 +293,8 @@ func (s *DockerSuite) TestRunVolumesFromInReadWriteMode(c *check.C) { dockerCmd(c, "run", "--name", "parent", "-v", "/test", "busybox", "true") dockerCmd(c, "run", "--volumes-from", "parent:rw", "busybox", "touch", "/test/file") - if out, _, err := dockerCmdWithError("run", "--volumes-from", "parent:bar", "busybox", "touch", "/test/file"); err == nil || !strings.Contains(out, "invalid mode for volumes-from: bar") { - c.Fatalf("running --volumes-from foo:bar should have failed with invalid mount mode: %q", out) + if out, _, err := dockerCmdWithError("run", "--volumes-from", "parent:bar", "busybox", "touch", "/test/file"); err == nil || !strings.Contains(out, "invalid mode: bar") { + c.Fatalf("running --volumes-from foo:bar should have failed with invalid mode: %q", out) } dockerCmd(c, "run", "--volumes-from", "parent", "busybox", "touch", "/test/file") diff --git a/opts/opts.go b/opts/opts.go index 3d8e4b4816..46adef1d85 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/docker/docker/pkg/parsers" - "github.com/docker/docker/volume" ) var ( @@ -214,14 +213,6 @@ func ValidateDevice(val string) (string, error) { return validatePath(val, ValidDeviceMode) } -// ValidatePath validates a path for volumes -// It will make sure 'val' is in the form: -// [host-dir:]container-path[:rw|ro] -// It also validates the mount mode. -func ValidatePath(val string) (string, error) { - return validatePath(val, volume.ValidMountMode) -} - func validatePath(val string, validator func(string) bool) (string, error) { var containerPath string var mode string diff --git a/opts/opts_test.go b/opts/opts_test.go index baf5f53362..e02d3f8ea6 100644 --- a/opts/opts_test.go +++ b/opts/opts_test.go @@ -274,58 +274,6 @@ func TestValidateLink(t *testing.T) { } } -func TestValidatePath(t *testing.T) { - valid := []string{ - "/home", - "/home:/home", - "/home:/something/else", - "/with space", - "/home:/with space", - "relative:/absolute-path", - "hostPath:/containerPath:ro", - "/hostPath:/containerPath:rw", - "/rw:/ro", - "/path:rw", - "/path:ro", - "/rw:rw", - } - invalid := map[string]string{ - "": "bad format for path: ", - "./": "./ is not an absolute path", - "../": "../ is not an absolute path", - "/:../": "../ is not an absolute path", - "/:path": "path is not an absolute path", - ":": "bad format for path: :", - "/tmp:": " is not an absolute path", - ":test": "bad format for path: :test", - ":/test": "bad format for path: :/test", - "tmp:": " is not an absolute path", - ":test:": "bad format for path: :test:", - "::": "bad format for path: ::", - ":::": "bad format for path: :::", - "/tmp:::": "bad format for path: /tmp:::", - ":/tmp::": "bad format for path: :/tmp::", - "path:ro": "path is not an absolute path", - "/path:/path:sw": "bad mode specified: sw", - "/path:/path:rwz": "bad mode specified: rwz", - } - - for _, path := range valid { - if _, err := ValidatePath(path); err != nil { - t.Fatalf("ValidatePath(`%q`) should succeed: error %q", path, err) - } - } - - for path, expectedError := range invalid { - if _, err := ValidatePath(path); err == nil { - t.Fatalf("ValidatePath(`%q`) should have failed validation", path) - } else { - if err.Error() != expectedError { - t.Fatalf("ValidatePath(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) - } - } - } -} func TestValidateDevice(t *testing.T) { valid := []string{ "/home", diff --git a/pkg/system/syscall_unix.go b/pkg/system/syscall_unix.go new file mode 100644 index 0000000000..e1b90b38e6 --- /dev/null +++ b/pkg/system/syscall_unix.go @@ -0,0 +1,11 @@ +// +build linux freebsd + +package system + +import "syscall" + +// UnmountWithSyscall is a platform-specific helper function to call +// the unmount syscall. +func UnmountWithSyscall(dest string) { + syscall.Unmount(dest, 0) +} diff --git a/pkg/system/syscall_windows.go b/pkg/system/syscall_windows.go new file mode 100644 index 0000000000..26bd80bd5d --- /dev/null +++ b/pkg/system/syscall_windows.go @@ -0,0 +1,6 @@ +package system + +// UnmountWithSyscall is a platform-specific helper function to call +// the unmount syscall. Not supported on Windows +func UnmountWithSyscall(dest string) { +} diff --git a/runconfig/config.go b/runconfig/config.go index 08f146d1b9..ced845f20e 100644 --- a/runconfig/config.go +++ b/runconfig/config.go @@ -2,10 +2,12 @@ package runconfig import ( "encoding/json" + "fmt" "io" "github.com/docker/docker/pkg/nat" "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/volume" ) // Config contains the configuration data about a container. @@ -44,15 +46,29 @@ type Config struct { // Be aware this function is not checking whether the resulted structs are nil, // it's your business to do so func DecodeContainerConfig(src io.Reader) (*Config, *HostConfig, error) { - decoder := json.NewDecoder(src) - var w ContainerConfigWrapper + + decoder := json.NewDecoder(src) if err := decoder.Decode(&w); err != nil { return nil, nil, err } hc := w.getHostConfig() + // Perform platform-specific processing of Volumes and Binds. + if w.Config != nil && hc != nil { + + // Initialise the volumes map if currently nil + if w.Config.Volumes == nil { + w.Config.Volumes = make(map[string]struct{}) + } + + // Now validate all the volumes and binds + if err := validateVolumesAndBindSettings(w.Config, hc); err != nil { + return nil, nil, err + } + } + // Certain parameters need daemon-side validation that cannot be done // on the client, as only the daemon knows what is valid for the platform. if err := ValidateNetMode(w.Config, hc); err != nil { @@ -61,3 +77,22 @@ func DecodeContainerConfig(src io.Reader) (*Config, *HostConfig, error) { return w.Config, hc, nil } + +// validateVolumesAndBindSettings validates each of the volumes and bind settings +// passed by the caller to ensure they are valid. +func validateVolumesAndBindSettings(c *Config, hc *HostConfig) error { + + // Ensure all volumes and binds are valid. + for spec := range c.Volumes { + if _, err := volume.ParseMountSpec(spec, hc.VolumeDriver); err != nil { + return fmt.Errorf("Invalid volume spec %q: %v", spec, err) + } + } + for _, spec := range hc.Binds { + if _, err := volume.ParseMountSpec(spec, hc.VolumeDriver); err != nil { + return fmt.Errorf("Invalid bind mount spec %q: %v", spec, err) + } + } + + return nil +} diff --git a/runconfig/config_test.go b/runconfig/config_test.go index 66def67446..b4890aeb0c 100644 --- a/runconfig/config_test.go +++ b/runconfig/config_test.go @@ -4,19 +4,36 @@ import ( "bytes" "fmt" "io/ioutil" + "runtime" "testing" "github.com/docker/docker/pkg/stringutils" ) +type f struct { + file string + entrypoint *stringutils.StrSlice +} + func TestDecodeContainerConfig(t *testing.T) { - fixtures := []struct { - file string - entrypoint *stringutils.StrSlice - }{ - {"fixtures/container_config_1_14.json", stringutils.NewStrSlice()}, - {"fixtures/container_config_1_17.json", stringutils.NewStrSlice("bash")}, - {"fixtures/container_config_1_19.json", stringutils.NewStrSlice("bash")}, + + var ( + fixtures []f + image string + ) + + if runtime.GOOS != "windows" { + image = "ubuntu" + fixtures = []f{ + {"fixtures/unix/container_config_1_14.json", stringutils.NewStrSlice()}, + {"fixtures/unix/container_config_1_17.json", stringutils.NewStrSlice("bash")}, + {"fixtures/unix/container_config_1_19.json", stringutils.NewStrSlice("bash")}, + } + } else { + image = "windows" + fixtures = []f{ + {"fixtures/windows/container_config_1_19.json", stringutils.NewStrSlice("cmd")}, + } } for _, f := range fixtures { @@ -30,15 +47,15 @@ func TestDecodeContainerConfig(t *testing.T) { t.Fatal(fmt.Errorf("Error parsing %s: %v", f, err)) } - if c.Image != "ubuntu" { - t.Fatalf("Expected ubuntu image, found %s\n", c.Image) + if c.Image != image { + t.Fatalf("Expected %s image, found %s\n", image, c.Image) } if c.Entrypoint.Len() != f.entrypoint.Len() { t.Fatalf("Expected %v, found %v\n", f.entrypoint, c.Entrypoint) } - if h.Memory != 1000 { + if h != nil && h.Memory != 1000 { t.Fatalf("Expected memory to be 1000, found %d\n", h.Memory) } } diff --git a/runconfig/fixtures/container_config_1_14.json b/runconfig/fixtures/unix/container_config_1_14.json similarity index 100% rename from runconfig/fixtures/container_config_1_14.json rename to runconfig/fixtures/unix/container_config_1_14.json diff --git a/runconfig/fixtures/container_config_1_17.json b/runconfig/fixtures/unix/container_config_1_17.json similarity index 100% rename from runconfig/fixtures/container_config_1_17.json rename to runconfig/fixtures/unix/container_config_1_17.json diff --git a/runconfig/fixtures/container_config_1_19.json b/runconfig/fixtures/unix/container_config_1_19.json similarity index 100% rename from runconfig/fixtures/container_config_1_19.json rename to runconfig/fixtures/unix/container_config_1_19.json diff --git a/runconfig/fixtures/container_hostconfig_1_14.json b/runconfig/fixtures/unix/container_hostconfig_1_14.json similarity index 100% rename from runconfig/fixtures/container_hostconfig_1_14.json rename to runconfig/fixtures/unix/container_hostconfig_1_14.json diff --git a/runconfig/fixtures/container_hostconfig_1_19.json b/runconfig/fixtures/unix/container_hostconfig_1_19.json similarity index 100% rename from runconfig/fixtures/container_hostconfig_1_19.json rename to runconfig/fixtures/unix/container_hostconfig_1_19.json diff --git a/runconfig/fixtures/windows/container_config_1_19.json b/runconfig/fixtures/windows/container_config_1_19.json new file mode 100644 index 0000000000..724320c760 --- /dev/null +++ b/runconfig/fixtures/windows/container_config_1_19.json @@ -0,0 +1,58 @@ +{ + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Entrypoint": "cmd", + "Image": "windows", + "Labels": { + "com.example.vendor": "Acme", + "com.example.license": "GPL", + "com.example.version": "1.0" + }, + "Volumes": { + "c:/windows": {} + }, + "WorkingDir": "", + "NetworkDisabled": false, + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { + "22/tcp": {} + }, + "HostConfig": { + "Binds": ["c:/windows:d:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "Memory": 1000, + "MemorySwap": 0, + "CpuShares": 512, + "CpusetCpus": "0,1", + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, + "ReadonlyRootfs": false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "DnsOptions": [""], + "ExtraHosts": null, + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "default", + "Devices": [], + "Ulimits": [{}], + "LogConfig": { "Type": "json-file", "Config": {} }, + "SecurityOpt": [""], + "CgroupParent": "" + } +} diff --git a/runconfig/hostconfig_test.go b/runconfig/hostconfig_test.go index 806f4a8a54..3e2eddd183 100644 --- a/runconfig/hostconfig_test.go +++ b/runconfig/hostconfig_test.go @@ -234,8 +234,8 @@ func TestDecodeHostConfig(t *testing.T) { fixtures := []struct { file string }{ - {"fixtures/container_hostconfig_1_14.json"}, - {"fixtures/container_hostconfig_1_19.json"}, + {"fixtures/unix/container_hostconfig_1_14.json"}, + {"fixtures/unix/container_hostconfig_1_19.json"}, } for _, f := range fixtures { diff --git a/runconfig/parse.go b/runconfig/parse.go index 7297597d57..4bc2fee6aa 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/stringutils" "github.com/docker/docker/pkg/units" + "github.com/docker/docker/volume" ) var ( @@ -46,7 +47,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe var ( // FIXME: use utils.ListOpts for attach and volumes? flAttach = opts.NewListOpts(opts.ValidateAttach) - flVolumes = opts.NewListOpts(opts.ValidatePath) + flVolumes = opts.NewListOpts(nil) flLinks = opts.NewListOpts(opts.ValidateLink) flEnv = opts.NewListOpts(opts.ValidateEnv) flLabels = opts.NewListOpts(opts.ValidateEnv) @@ -201,16 +202,11 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe var binds []string // add any bind targets to the list of container volumes for bind := range flVolumes.GetMap() { - if arr := strings.Split(bind, ":"); len(arr) > 1 { - if arr[1] == "/" { - return nil, nil, cmd, fmt.Errorf("Invalid bind mount: destination can't be '/'") - } + if arr := volume.SplitN(bind, 2); len(arr) > 1 { // after creating the bind mount we want to delete it from the flVolumes values because // we do not want bind mounts being committed to image configs binds = append(binds, bind) flVolumes.Delete(bind) - } else if bind == "/" { - return nil, nil, cmd, fmt.Errorf("Invalid volume: path can't be '/'") } } diff --git a/runconfig/parse_test.go b/runconfig/parse_test.go index 4cae1c7c69..e4420cf0c0 100644 --- a/runconfig/parse_test.go +++ b/runconfig/parse_test.go @@ -1,8 +1,12 @@ package runconfig import ( + "bytes" + "encoding/json" "fmt" "io/ioutil" + "os" + "runtime" "strings" "testing" @@ -31,17 +35,6 @@ func mustParse(t *testing.T, args string) (*Config, *HostConfig) { return config, hostConfig } -// check if (a == c && b == d) || (a == d && b == c) -// because maps are randomized -func compareRandomizedStrings(a, b, c, d string) error { - if a == c && b == d { - return nil - } - if a == d && b == c { - return nil - } - return fmt.Errorf("strings don't match") -} func TestParseRunLinks(t *testing.T) { if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" { t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links) @@ -98,81 +91,257 @@ func TestParseRunAttach(t *testing.T) { } func TestParseRunVolumes(t *testing.T) { - if config, hostConfig := mustParse(t, "-v /tmp"); hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, `-v /tmp` should not mount-bind anything. Received %v", hostConfig.Binds) - } else if _, exists := config.Volumes["/tmp"]; !exists { - t.Fatalf("Error parsing volume flags, `-v /tmp` is missing from volumes. Received %v", config.Volumes) + + // A single volume + arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { + t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) + } else if _, exists := config.Volumes[arr[0]]; !exists { + t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes) } - if config, hostConfig := mustParse(t, "-v /tmp -v /var"); hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, `-v /tmp -v /var` should not mount-bind anything. Received %v", hostConfig.Binds) - } else if _, exists := config.Volumes["/tmp"]; !exists { - t.Fatalf("Error parsing volume flags, `-v /tmp` is missing from volumes. Received %v", config.Volumes) - } else if _, exists := config.Volumes["/var"]; !exists { - t.Fatalf("Error parsing volume flags, `-v /var` is missing from volumes. Received %v", config.Volumes) + // Two volumes + arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { + t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) + } else if _, exists := config.Volumes[arr[0]]; !exists { + t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes) + } else if _, exists := config.Volumes[arr[1]]; !exists { + t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes) } - if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp"); hostConfig.Binds == nil || hostConfig.Binds[0] != "/hostTmp:/containerTmp" { - t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp` should mount-bind /hostTmp into /containerTmp. Received %v", hostConfig.Binds) + // A single bind-mount + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] { + t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes) } - if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp -v /hostVar:/containerVar"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/hostTmp:/containerTmp", "/hostVar:/containerVar") != nil { - t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp -v /hostVar:/containerVar` should mount-bind /hostTmp into /containerTmp and /hostVar into /hostContainer. Received %v", hostConfig.Binds) + // Two bind-mounts. + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`}) + if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { + t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) } - if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp:ro -v /hostVar:/containerVar:rw"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/hostTmp:/containerTmp:ro", "/hostVar:/containerVar:rw") != nil { - t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp:ro -v /hostVar:/containerVar:rw` should mount-bind /hostTmp into /containerTmp and /hostVar into /hostContainer. Received %v", hostConfig.Binds) + // Two bind-mounts, first read-only, second read-write. + // TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4 + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`}) + if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { + t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) } - if _, hostConfig := mustParse(t, "-v /containerTmp:ro -v /containerVar:rw"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/containerTmp:ro", "/containerVar:rw") != nil { - t.Fatalf("Error parsing volume flags, `-v /containerTmp:ro -v /containerVar:rw` should mount-bind /containerTmp into /ro and /containerVar into /rw. Received %v", hostConfig.Binds) + // Similar to previous test but with alternate modes which are only supported by Linux + if runtime.GOOS != "windows" { + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{}) + if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { + t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) + } + + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{}) + if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { + t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) + } } - if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp:ro,Z -v /hostVar:/containerVar:rw,Z"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/hostTmp:/containerTmp:ro,Z", "/hostVar:/containerVar:rw,Z") != nil { - t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp:ro,Z -v /hostVar:/containerVar:rw,Z` should mount-bind /hostTmp into /containerTmp and /hostVar into /hostContainer. Received %v", hostConfig.Binds) + // One bind mount and one volume + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] { + t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds) + } else if _, exists := config.Volumes[arr[1]]; !exists { + t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes) } - if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp:Z -v /hostVar:/containerVar:z"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/hostTmp:/containerTmp:Z", "/hostVar:/containerVar:z") != nil { - t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp:Z -v /hostVar:/containerVar:z` should mount-bind /hostTmp into /containerTmp and /hostVar into /hostContainer. Received %v", hostConfig.Binds) + // Root to non-c: drive letter (Windows specific) + if runtime.GOOS == "windows" { + arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 { + t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0]) + } } - if config, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp -v /containerVar"); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != "/hostTmp:/containerTmp" { - t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp -v /containerVar` should mount-bind only /hostTmp into /containerTmp. Received %v", hostConfig.Binds) - } else if _, exists := config.Volumes["/containerVar"]; !exists { - t.Fatalf("Error parsing volume flags, `-v /containerVar` is missing from volumes. Received %v", config.Volumes) +} + +// This tests the cases for binds which are generated through +// DecodeContainerConfig rather than Parse() +func TestDecodeContainerConfigVolumes(t *testing.T) { + + // Root to root + bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`}) + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) } - if config, hostConfig := mustParse(t, ""); hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, without volume, nothing should be mount-binded. Received %v", hostConfig.Binds) - } else if len(config.Volumes) != 0 { - t.Fatalf("Error parsing volume flags, without volume, no volume should be present. Received %v", config.Volumes) + // No destination path + bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`}) + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) } - if _, _, err := parse(t, "-v /"); err == nil { - t.Fatalf("Expected error, but got none") + // // No destination path or mode + bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`}) + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) } - if _, _, err := parse(t, "-v /:/"); err == nil { - t.Fatalf("Error parsing volume flags, `-v /:/` should fail but didn't") + // A whole lot of nothing + bindsOrVols = []string{`:`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) } - if _, _, err := parse(t, "-v"); err == nil { - t.Fatalf("Error parsing volume flags, `-v` should fail but didn't") + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) } - if _, _, err := parse(t, "-v /tmp:"); err == nil { - t.Fatalf("Error parsing volume flags, `-v /tmp:` should fail but didn't") + + // A whole lot of nothing with no mode + bindsOrVols = []string{`::`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) } - if _, _, err := parse(t, "-v /tmp::"); err == nil { - t.Fatalf("Error parsing volume flags, `-v /tmp::` should fail but didn't") + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) } - if _, _, err := parse(t, "-v :"); err == nil { - t.Fatalf("Error parsing volume flags, `-v :` should fail but didn't") + + // Too much including an invalid mode + wTmp := os.Getenv("TEMP") + bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp}) + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) } - if _, _, err := parse(t, "-v ::"); err == nil { - t.Fatalf("Error parsing volume flags, `-v ::` should fail but didn't") + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) } - if _, _, err := parse(t, "-v /tmp:/tmp:/tmp:/tmp"); err == nil { - t.Fatalf("Error parsing volume flags, `-v /tmp:/tmp:/tmp:/tmp` should fail but didn't") + + // Windows specific error tests + if runtime.GOOS == "windows" { + // Volume which does not include a drive letter + bindsOrVols = []string{`\tmp`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + + // Root to C-Drive + bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + + // Container path that does not include a drive letter + bindsOrVols = []string{`c:\windows:\somewhere`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } } + + // Linux-specific error tests + if runtime.GOOS != "windows" { + // Just root + bindsOrVols = []string{`/`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + + // A single volume that looks like a bind mount passed in Volumes. + // This should be handled as a bind mount, not a volume. + vols := []string{`/foo:/bar`} + if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil { + t.Fatal("Volume /foo:/bar should have succeeded as a volume name") + } else if hostConfig.Binds != nil { + t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds) + } else if _, exists := config.Volumes[vols[0]]; !exists { + t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes) + } + + } +} + +// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes +// to call DecodeContainerConfig. It effectively does what a client would +// do when calling the daemon by constructing a JSON stream of a +// ContainerConfigWrapper which is populated by the set of volume specs +// passed into it. It returns a config and a hostconfig which can be +// validated to ensure DecodeContainerConfig has manipulated the structures +// correctly. +func callDecodeContainerConfig(volumes []string, binds []string) (*Config, *HostConfig, error) { + var ( + b []byte + err error + c *Config + h *HostConfig + ) + w := ContainerConfigWrapper{ + Config: &Config{ + Volumes: map[string]struct{}{}, + }, + HostConfig: &HostConfig{ + NetworkMode: "none", + Binds: binds, + }, + } + for _, v := range volumes { + w.Config.Volumes[v] = struct{}{} + } + if b, err = json.Marshal(w); err != nil { + return nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) + } + c, h, err = DecodeContainerConfig(bytes.NewReader(b)) + if err != nil { + return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err) + } + if c == nil || h == nil { + return nil, nil, fmt.Errorf("Empty config or hostconfig") + } + + return c, h, err +} + +// check if (a == c && b == d) || (a == d && b == c) +// because maps are randomized +func compareRandomizedStrings(a, b, c, d string) error { + if a == c && b == d { + return nil + } + if a == d && b == c { + return nil + } + return fmt.Errorf("strings don't match") +} + +// setupPlatformVolume takes two arrays of volume specs - a Unix style +// spec and a Windows style spec. Depending on the platform being unit tested, +// it returns one of them, along with a volume string that would be passed +// on the docker CLI (eg -v /bar -v /foo). +func setupPlatformVolume(u []string, w []string) ([]string, string) { + var a []string + if runtime.GOOS == "windows" { + a = w + } else { + a = u + } + s := "" + for _, v := range a { + s = s + "-v " + v + " " + } + return a, s } func TestParseLxcConfOpt(t *testing.T) { @@ -438,9 +607,13 @@ func TestParseLoggingOpts(t *testing.T) { } func TestParseEnvfileVariables(t *testing.T) { + e := "open nonexistent: no such file or directory" + if runtime.GOOS == "windows" { + e = "open nonexistent: The system cannot find the file specified." + } // env ko - if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != "open nonexistent: no such file or directory" { - t.Fatalf("Expected an error with message 'open nonexistent: no such file or directory', got %v", err) + if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { + t.Fatalf("Expected an error with message '%s', got %v", e, err) } // env ok config, _, _, err := parseRun([]string{"--env-file=fixtures/valid.env", "img", "cmd"}) @@ -460,9 +633,13 @@ func TestParseEnvfileVariables(t *testing.T) { } func TestParseLabelfileVariables(t *testing.T) { + e := "open nonexistent: no such file or directory" + if runtime.GOOS == "windows" { + e = "open nonexistent: The system cannot find the file specified." + } // label ko - if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != "open nonexistent: no such file or directory" { - t.Fatalf("Expected an error with message 'open nonexistent: no such file or directory', got %v", err) + if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { + t.Fatalf("Expected an error with message '%s', got %v", e, err) } // label ok config, _, _, err := parseRun([]string{"--label-file=fixtures/valid.label", "img", "cmd"}) diff --git a/volume/drivers/extpoint_test.go b/volume/drivers/extpoint_test.go index f9824e5fed..8ab60c95e3 100644 --- a/volume/drivers/extpoint_test.go +++ b/volume/drivers/extpoint_test.go @@ -11,7 +11,6 @@ func TestGetDriver(t *testing.T) { if err == nil { t.Fatal("Expected error, was nil") } - Register(volumetestutils.FakeDriver{}, "fake") d, err := GetDriver("fake") if err != nil { diff --git a/volume/store/store.go b/volume/store/store.go index 46c44333cd..22c213fb48 100644 --- a/volume/store/store.go +++ b/volume/store/store.go @@ -14,6 +14,8 @@ var ( ErrVolumeInUse = errors.New("volume is in use") // ErrNoSuchVolume is a typed error returned if the requested volume doesn't exist in the volume store ErrNoSuchVolume = errors.New("no such volume") + // ErrInvalidName is a typed error returned when creating a volume with a name that is not valid on the platform + ErrInvalidName = errors.New("volume name is not valid on this platform") ) // New initializes a VolumeStore to keep @@ -39,13 +41,14 @@ type volumeCounter struct { // AddAll adds a list of volumes to the store func (s *VolumeStore) AddAll(vols []volume.Volume) { for _, v := range vols { - s.vols[v.Name()] = &volumeCounter{v, 0} + s.vols[normaliseVolumeName(v.Name())] = &volumeCounter{v, 0} } } // Create tries to find an existing volume with the given name or create a new one from the passed in driver func (s *VolumeStore) Create(name, driverName string, opts map[string]string) (volume.Volume, error) { s.mu.Lock() + name = normaliseVolumeName(name) if vc, exists := s.vols[name]; exists { v := vc.Volume s.mu.Unlock() @@ -59,13 +62,22 @@ func (s *VolumeStore) Create(name, driverName string, opts map[string]string) (v return nil, err } + // Validate the name in a platform-specific manner + valid, err := volume.IsVolumeNameValid(name) + if err != nil { + return nil, err + } + if !valid { + return nil, ErrInvalidName + } + v, err := vd.Create(name, opts) if err != nil { return nil, err } s.mu.Lock() - s.vols[v.Name()] = &volumeCounter{v, 0} + s.vols[normaliseVolumeName(v.Name())] = &volumeCounter{v, 0} s.mu.Unlock() return v, nil @@ -73,6 +85,7 @@ func (s *VolumeStore) Create(name, driverName string, opts map[string]string) (v // Get looks if a volume with the given name exists and returns it if so func (s *VolumeStore) Get(name string) (volume.Volume, error) { + name = normaliseVolumeName(name) s.mu.Lock() defer s.mu.Unlock() vc, exists := s.vols[name] @@ -86,7 +99,7 @@ func (s *VolumeStore) Get(name string) (volume.Volume, error) { func (s *VolumeStore) Remove(v volume.Volume) error { s.mu.Lock() defer s.mu.Unlock() - name := v.Name() + name := normaliseVolumeName(v.Name()) logrus.Debugf("Removing volume reference: driver %s, name %s", v.DriverName(), name) vc, exists := s.vols[name] if !exists { @@ -112,11 +125,12 @@ func (s *VolumeStore) Remove(v volume.Volume) error { func (s *VolumeStore) Increment(v volume.Volume) { s.mu.Lock() defer s.mu.Unlock() - logrus.Debugf("Incrementing volume reference: driver %s, name %s", v.DriverName(), v.Name()) + name := normaliseVolumeName(v.Name()) + logrus.Debugf("Incrementing volume reference: driver %s, name %s", v.DriverName(), name) - vc, exists := s.vols[v.Name()] + vc, exists := s.vols[name] if !exists { - s.vols[v.Name()] = &volumeCounter{v, 1} + s.vols[name] = &volumeCounter{v, 1} return } vc.count++ @@ -126,9 +140,10 @@ func (s *VolumeStore) Increment(v volume.Volume) { func (s *VolumeStore) Decrement(v volume.Volume) { s.mu.Lock() defer s.mu.Unlock() - logrus.Debugf("Decrementing volume reference: driver %s, name %s", v.DriverName(), v.Name()) + name := normaliseVolumeName(v.Name()) + logrus.Debugf("Decrementing volume reference: driver %s, name %s", v.DriverName(), name) - vc, exists := s.vols[v.Name()] + vc, exists := s.vols[name] if !exists { return } @@ -142,7 +157,7 @@ func (s *VolumeStore) Decrement(v volume.Volume) { func (s *VolumeStore) Count(v volume.Volume) uint { s.mu.Lock() defer s.mu.Unlock() - vc, exists := s.vols[v.Name()] + vc, exists := s.vols[normaliseVolumeName(v.Name())] if !exists { return 0 } diff --git a/volume/store/store_unix.go b/volume/store/store_unix.go new file mode 100644 index 0000000000..319c541d60 --- /dev/null +++ b/volume/store/store_unix.go @@ -0,0 +1,9 @@ +// +build linux freebsd + +package store + +// normaliseVolumeName is a platform specific function to normalise the name +// of a volume. This is a no-op on Unix-like platforms +func normaliseVolumeName(name string) string { + return name +} diff --git a/volume/store/store_windows.go b/volume/store/store_windows.go new file mode 100644 index 0000000000..a42c1f8413 --- /dev/null +++ b/volume/store/store_windows.go @@ -0,0 +1,12 @@ +package store + +import "strings" + +// normaliseVolumeName is a platform specific function to normalise the name +// of a volume. On Windows, as NTFS is case insensitive, under +// c:\ProgramData\Docker\Volumes\, the folders John and john would be synonymous. +// Hence we can't allow the volume "John" and "john" to be created as seperate +// volumes. +func normaliseVolumeName(name string) string { + return strings.ToLower(name) +} diff --git a/volume/volume.go b/volume/volume.go index 647469e50f..98f90f06d4 100644 --- a/volume/volume.go +++ b/volume/volume.go @@ -1,5 +1,15 @@ package volume +import ( + "os" + "runtime" + "strings" + + "github.com/Sirupsen/logrus" + derr "github.com/docker/docker/errors" + "github.com/docker/docker/pkg/system" +) + // DefaultDriverName is the driver name used for the driver // implemented in the local package. const DefaultDriverName string = "local" @@ -29,33 +39,134 @@ type Volume interface { Unmount() error } -// read-write modes -var rwModes = map[string]bool{ - "rw": true, - "rw,Z": true, - "rw,z": true, - "z,rw": true, - "Z,rw": true, - "Z": true, - "z": true, +// MountPoint is the intersection point between a volume and a container. It +// specifies which volume is to be used and where inside a container it should +// be mounted. +type MountPoint struct { + Source string // Container host directory + Destination string // Inside the container + RW bool // True if writable + Name string // Name set by user + Driver string // Volume driver to use + Volume Volume `json:"-"` + + // Note Mode is not used on Windows + Mode string `json:"Relabel"` // Originally field was `Relabel`" } -// read-only modes -var roModes = map[string]bool{ - "ro": true, - "ro,Z": true, - "ro,z": true, - "z,ro": true, - "Z,ro": true, +// Setup sets up a mount point by either mounting the volume if it is +// configured, or creating the source directory if supplied. +func (m *MountPoint) Setup() (string, error) { + if m.Volume != nil { + return m.Volume.Mount() + } + if len(m.Source) > 0 { + if _, err := os.Stat(m.Source); err != nil { + if !os.IsNotExist(err) { + return "", err + } + if runtime.GOOS != "windows" { // Windows does not have deprecation issues here + logrus.Warnf("Auto-creating non-existant volume host path %s, this is deprecated and will be removed soon", m.Source) + if err := system.MkdirAll(m.Source, 0755); err != nil { + return "", err + } + } + } + return m.Source, nil + } + return "", derr.ErrorCodeMountSetup +} + +// Path returns the path of a volume in a mount point. +func (m *MountPoint) Path() string { + if m.Volume != nil { + return m.Volume.Path() + } + return m.Source } // ValidMountMode will make sure the mount mode is valid. // returns if it's a valid mount mode or not. func ValidMountMode(mode string) bool { - return roModes[mode] || rwModes[mode] + 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[mode] + return rwModes[strings.ToLower(mode)] +} + +// ParseVolumesFrom ensure that the supplied volumes-from is valid. +func ParseVolumesFrom(spec string) (string, string, error) { + if len(spec) == 0 { + return "", "", derr.ErrorCodeVolumeFromBlank.WithArgs(spec) + } + + specParts := strings.SplitN(spec, ":", 2) + id := specParts[0] + mode := "rw" + + if len(specParts) == 2 { + mode = specParts[1] + if !ValidMountMode(mode) { + return "", "", derr.ErrorCodeVolumeInvalidMode.WithArgs(mode) + } + } + return id, mode, nil +} + +// SplitN splits raw into a maximum of n parts, separated by a separator colon. +// A separator colon is the last `:` character in the regex `[/:\\]?[a-zA-Z]:` (note `\\` is `\` escaped). +// This allows to correctly split strings such as `C:\foo:D:\:rw`. +func SplitN(raw string, n int) []string { + var array []string + if len(raw) == 0 || raw[0] == ':' { + // invalid + return nil + } + // numberOfParts counts the number of parts separated by a separator colon + numberOfParts := 0 + // left represents the left-most cursor in raw, updated at every `:` character considered as a separator. + left := 0 + // right represents the right-most cursor in raw incremented with the loop. Note this + // starts at index 1 as index 0 is already handle above as a special case. + for right := 1; right < len(raw); right++ { + // stop parsing if reached maximum number of parts + if n >= 0 && numberOfParts >= n { + break + } + if raw[right] != ':' { + continue + } + potentialDriveLetter := raw[right-1] + if (potentialDriveLetter >= 'A' && potentialDriveLetter <= 'Z') || (potentialDriveLetter >= 'a' && potentialDriveLetter <= 'z') { + if right > 1 { + beforePotentialDriveLetter := raw[right-2] + if beforePotentialDriveLetter != ':' && beforePotentialDriveLetter != '/' && beforePotentialDriveLetter != '\\' { + // e.g. `C:` is not preceded by any delimiter, therefore it was not a drive letter but a path ending with `C:`. + array = append(array, raw[left:right]) + left = right + 1 + numberOfParts++ + } + // else, `C:` is considered as a drive letter and not as a delimiter, so we continue parsing. + } + // if right == 1, then `C:` is the beginning of the raw string, therefore `:` is again not considered a delimiter and we continue parsing. + } else { + // if `:` is not preceded by a potential drive letter, then consider it as a delimiter. + array = append(array, raw[left:right]) + left = right + 1 + numberOfParts++ + } + } + // need to take care of the last part + if left < len(raw) { + if n >= 0 && numberOfParts >= n { + // if the maximum number of parts is reached, just append the rest to the last part + // left-1 is at the last `:` that needs to be included since not considered a separator. + array[n-1] += raw[left-1:] + } else { + array = append(array, raw[left:]) + } + } + return array } diff --git a/volume/volume_test.go b/volume/volume_test.go new file mode 100644 index 0000000000..5ce3d9fa96 --- /dev/null +++ b/volume/volume_test.go @@ -0,0 +1,261 @@ +package volume + +import ( + "runtime" + "strings" + "testing" +) + +func TestParseMountSpec(t *testing.T) { + var ( + valid []string + invalid map[string]string + ) + + if runtime.GOOS == "windows" { + valid = []string{ + `d:\`, + `d:`, + `d:\path`, + `d:\path with space`, + // TODO Windows post TP4 - readonly support `d:\pathandmode:ro`, + `c:\:d:\`, + `c:\windows\:d:`, + `c:\windows:d:\s p a c e`, + `c:\windows:d:\s p a c e:RW`, + `c:\program files:d:\s p a c e i n h o s t d i r`, + `0123456789name:d:`, + `MiXeDcAsEnAmE:d:`, + `name:D:`, + `name:D::rW`, + `name:D::RW`, + // TODO Windows post TP4 - readonly support `name:D::RO`, + `c:/:d:/forward/slashes/are/good/too`, + // TODO Windows post TP4 - readonly support `c:/:d:/including with/spaces:ro`, + `c:\Windows`, // With capital + `c:\Program Files (x86)`, // With capitals and brackets + } + invalid = map[string]string{ + ``: "Invalid volume specification: ", + `.`: "Invalid volume specification: ", + `..\`: "Invalid volume specification: ", + `c:\:..\`: "Invalid volume specification: ", + `c:\:d:\:xyzzy`: "Invalid volume specification: ", + `c:`: "cannot be c:", + `c:\`: `cannot be c:\`, + `c:\notexist:d:`: `The system cannot find the file specified`, + `c:\windows\system32\ntdll.dll:d:`: `Source 'c:\windows\system32\ntdll.dll' is not a directory`, + `name<:d:`: `Invalid volume specification`, + `name>:d:`: `Invalid volume specification`, + `name::d:`: `Invalid volume specification`, + `name":d:`: `Invalid volume specification`, + `name\:d:`: `Invalid volume specification`, + `name*:d:`: `Invalid volume specification`, + `name|:d:`: `Invalid volume specification`, + `name?:d:`: `Invalid volume specification`, + `name/:d:`: `Invalid volume specification`, + `d:\pathandmode:rw`: `Invalid volume specification`, + `con:d:`: `cannot be a reserved word for Windows filenames`, + `PRN:d:`: `cannot be a reserved word for Windows filenames`, + `aUx:d:`: `cannot be a reserved word for Windows filenames`, + `nul:d:`: `cannot be a reserved word for Windows filenames`, + `com1:d:`: `cannot be a reserved word for Windows filenames`, + `com2:d:`: `cannot be a reserved word for Windows filenames`, + `com3:d:`: `cannot be a reserved word for Windows filenames`, + `com4:d:`: `cannot be a reserved word for Windows filenames`, + `com5:d:`: `cannot be a reserved word for Windows filenames`, + `com6:d:`: `cannot be a reserved word for Windows filenames`, + `com7:d:`: `cannot be a reserved word for Windows filenames`, + `com8:d:`: `cannot be a reserved word for Windows filenames`, + `com9:d:`: `cannot be a reserved word for Windows filenames`, + `lpt1:d:`: `cannot be a reserved word for Windows filenames`, + `lpt2:d:`: `cannot be a reserved word for Windows filenames`, + `lpt3:d:`: `cannot be a reserved word for Windows filenames`, + `lpt4:d:`: `cannot be a reserved word for Windows filenames`, + `lpt5:d:`: `cannot be a reserved word for Windows filenames`, + `lpt6:d:`: `cannot be a reserved word for Windows filenames`, + `lpt7:d:`: `cannot be a reserved word for Windows filenames`, + `lpt8:d:`: `cannot be a reserved word for Windows filenames`, + `lpt9:d:`: `cannot be a reserved word for Windows filenames`, + } + + } else { + valid = []string{ + "/home", + "/home:/home", + "/home:/something/else", + "/with space", + "/home:/with space", + "relative:/absolute-path", + "hostPath:/containerPath:ro", + "/hostPath:/containerPath:rw", + "/rw:/ro", + } + invalid = map[string]string{ + "": "Invalid volume specification", + "./": "Invalid volume destination", + "../": "Invalid volume destination", + "/:../": "Invalid volume destination", + "/:path": "Invalid volume destination", + ":": "Invalid volume specification", + "/tmp:": "Invalid volume destination", + ":test": "Invalid volume specification", + ":/test": "Invalid volume specification", + "tmp:": "Invalid volume destination", + ":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: sw", + "/path:/path:rwz": "invalid mode: rwz", + } + } + + for _, path := range valid { + if _, err := ParseMountSpec(path, "local"); err != nil { + t.Fatalf("ParseMountSpec(`%q`) should succeed: error %q", path, err) + } + } + + for path, expectedError := range invalid { + if _, err := ParseMountSpec(path, "local"); err == nil { + t.Fatalf("ParseMountSpec(`%q`) should have failed validation. Err %v", path, err) + } else { + if !strings.Contains(err.Error(), expectedError) { + t.Fatalf("ParseMountSpec(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) + } + } + } +} + +func TestSplitN(t *testing.T) { + for _, x := range []struct { + input string + n int + expected []string + }{ + {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, + {`:C:\foo:d:`, -1, nil}, + {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, + {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, + {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, + + {`d:\`, -1, []string{`d:\`}}, + {`d:`, -1, []string{`d:`}}, + {`d:\path`, -1, []string{`d:\path`}}, + {`d:\path with space`, -1, []string{`d:\path with space`}}, + {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, + {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, + {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, + {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, + {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, + {`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}}, + {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, + {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, + {`name:D:`, -1, []string{`name`, `D:`}}, + {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, + {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, + {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, + {`c:\Windows`, -1, []string{`c:\Windows`}}, + {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, + + {``, -1, nil}, + {`.`, -1, []string{`.`}}, + {`..\`, -1, []string{`..\`}}, + {`c:\:..\`, -1, []string{`c:\`, `..\`}}, + {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, + } { + res := SplitN(x.input, x.n) + if len(res) < len(x.expected) { + t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) + } + for i, e := range res { + if e != x.expected[i] { + t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) + } + } + } +} + +// testParseMountSpec is a structure used by TestParseMountSpecSplit for +// specifying test cases for the ParseMountSpec() function. +type testParseMountSpec struct { + bind string + driver string + expDest string + expSource string + expName string + expDriver string + expRW bool + fail bool +} + +func TestParseMountSpecSplit(t *testing.T) { + var cases []testParseMountSpec + if runtime.GOOS == "windows" { + cases = []testParseMountSpec{ + {`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false}, + {`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false}, + // TODO Windows post TP4 - Add readonly support {`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false}, + {`c:\:d:\:rw`, "local", `d:\`, `c:\`, ``, "", true, false}, + {`c:\:d:\:foo`, "local", `d:\`, `c:\`, ``, "", false, true}, + {`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false}, + {`name:d:`, "local", `d:`, ``, `name`, "local", true, false}, + // TODO Windows post TP4 - Add readonly support {`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false}, + {`name:c:`, "", ``, ``, ``, "", true, true}, + {`driver/name:c:`, "", ``, ``, ``, "", true, true}, + } + } else { + cases = []testParseMountSpec{ + {"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false}, + {"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false}, + {"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false}, + {"/tmp:/tmp4:foo", "", "", "", "", "", false, true}, + {"name:/named1", "", "/named1", "", "name", "local", true, false}, + {"name:/named2", "external", "/named2", "", "name", "external", true, false}, + {"name:/named3:ro", "local", "/named3", "", "name", "local", false, false}, + {"local/name:/tmp:rw", "", "/tmp", "", "local/name", "local", true, false}, + {"/tmp:tmp", "", "", "", "", "", true, true}, + } + } + + for _, c := range cases { + m, err := ParseMountSpec(c.bind, c.driver) + if c.fail { + if err == nil { + t.Fatalf("Expected error, was nil, for spec %s\n", c.bind) + } + continue + } + + if m == nil || err != nil { + t.Fatalf("ParseMountSpec failed for spec %s driver %s error %v\n", c.bind, c.driver, err.Error()) + continue + } + + if m.Destination != c.expDest { + t.Fatalf("Expected destination %s, was %s, for spec %s\n", c.expDest, m.Destination, c.bind) + } + + if m.Source != c.expSource { + t.Fatalf("Expected source %s, was %s, for spec %s\n", c.expSource, m.Source, c.bind) + } + + if m.Name != c.expName { + t.Fatalf("Expected name %s, was %s for spec %s\n", c.expName, m.Name, c.bind) + } + + if m.Driver != c.expDriver { + t.Fatalf("Expected driver %s, was %s, for spec %s\n", c.expDriver, m.Driver, c.bind) + } + + if m.RW != c.expRW { + t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind) + } + } +} diff --git a/volume/volume_unix.go b/volume/volume_unix.go new file mode 100644 index 0000000000..8c98a3d95a --- /dev/null +++ b/volume/volume_unix.go @@ -0,0 +1,132 @@ +// +build linux freebsd darwin + +package volume + +import ( + "fmt" + "path/filepath" + "strings" + + derr "github.com/docker/docker/errors" +) + +// read-write modes +var rwModes = map[string]bool{ + "rw": true, + "rw,Z": true, + "rw,z": true, + "z,rw": true, + "Z,rw": true, + "Z": true, + "z": true, +} + +// read-only modes +var roModes = map[string]bool{ + "ro": true, + "ro,Z": true, + "ro,z": true, + "z,ro": true, + "Z,ro": 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 { + relPath, err := filepath.Rel(m.Destination, absolutePath) + return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator)) +} + +// ParseMountSpec validates the configuration of mount information is valid. +func ParseMountSpec(spec, volumeDriver string) (*MountPoint, error) { + spec = filepath.ToSlash(spec) + + mp := &MountPoint{ + RW: true, + } + if strings.Count(spec, ":") > 2 { + return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) + } + + arr := strings.SplitN(spec, ":", 3) + if arr[0] == "" { + return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) + } + + switch len(arr) { + case 1: + // Just a destination path in the container + mp.Destination = filepath.Clean(arr[0]) + case 2: + if isValid := ValidMountMode(arr[1]); isValid { + // Destination + Mode is not a valid volume - volumes + // cannot include a mode. eg /foo:rw + return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) + } + // Host Source Path or Name + Destination + mp.Source = arr[0] + mp.Destination = arr[1] + case 3: + // HostSourcePath+DestinationPath+Mode + mp.Source = arr[0] + mp.Destination = arr[1] + mp.Mode = arr[2] // Mode field is used by SELinux to decide whether to apply label + if !ValidMountMode(mp.Mode) { + return nil, derr.ErrorCodeVolumeInvalidMode.WithArgs(mp.Mode) + } + mp.RW = ReadWrite(mp.Mode) + default: + return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) + } + + //validate the volumes destination path + mp.Destination = filepath.Clean(mp.Destination) + if !filepath.IsAbs(mp.Destination) { + return nil, derr.ErrorCodeVolumeAbs.WithArgs(mp.Destination) + } + + // Destination cannot be "/" + if mp.Destination == "/" { + return nil, derr.ErrorCodeVolumeSlash.WithArgs(spec) + } + + name, source := ParseVolumeSource(mp.Source) + if len(source) == 0 { + mp.Source = "" // Clear it out as we previously assumed it was not a name + mp.Driver = volumeDriver + if len(mp.Driver) == 0 { + mp.Driver = DefaultDriverName + } + } else { + mp.Source = filepath.Clean(source) + } + + mp.Name = name + + return mp, nil +} + +// ParseVolumeSource parses the origin sources that's mounted into the container. +// It returns a name and a source. It looks to see if the spec passed in +// is an absolute file. If it is, it assumes the spec is a source. If not, +// it assumes the spec is a name. +func ParseVolumeSource(spec string) (string, string) { + if !filepath.IsAbs(spec) { + return spec, "" + } + return "", spec +} + +// IsVolumeNameValid checks a volume name in a platform specific manner. +func IsVolumeNameValid(name string) (bool, error) { + return true, nil +} diff --git a/volume/volume_windows.go b/volume/volume_windows.go new file mode 100644 index 0000000000..b6b3bbb0fe --- /dev/null +++ b/volume/volume_windows.go @@ -0,0 +1,181 @@ +package volume + +import ( + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/Sirupsen/logrus" + derr "github.com/docker/docker/errors" +) + +// read-write modes +var rwModes = map[string]bool{ + "rw": true, +} + +// read-only modes +var roModes = map[string]bool{ + "ro": true, +} + +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])` + + // RXSource is the combined possiblities for a source + RXSource = `((?P((` + RXHostDir + `)|(` + RXName + `))):)?` + + // Source. Can be either a host directory, a name, or omitted: + // HostDir: + // - Essentially using the folder solution from + // https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html + // but adding case insensitivity. + // - Must be an absolute path such as c:\path + // - Can include spaces such as `c:\program files` + // - And then followed by a colon which is not in the capture group + // - And can be optional + // Name: + // - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx) + // - And then followed by a colon which is not in the capture group + // - And can be optional + + // RXDestination is the regex expression for the mount destination + RXDestination = `(?P([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))` + // 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 + RXMode = `(:(?P(?i)rw))?` + // Temporarily for TP4, disabling the use of ro as it's not supported yet + // in the platform. TODO Windows: `(:(?P(?i)ro|rw))?` + // mode (optional) + // - Hopefully self explanatory in comparison to above. + // - Colon is not in the capture group + // +) + +// ParseMountSpec validates the configuration of mount information is valid. +func ParseMountSpec(spec string, volumeDriver string) (*MountPoint, error) { + var specExp = regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`) + + // Ensure in platform semantics for matching. The CLI will send in Unix semantics. + match := specExp.FindStringSubmatch(filepath.FromSlash(strings.ToLower(spec))) + + // Must have something back + if len(match) == 0 { + return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) + } + + // Pull out the sub expressions from the named capture groups + matchgroups := make(map[string]string) + for i, name := range specExp.SubexpNames() { + matchgroups[name] = strings.ToLower(match[i]) + } + + mp := &MountPoint{ + Source: matchgroups["source"], + Destination: matchgroups["destination"], + RW: true, + } + if strings.ToLower(matchgroups["mode"]) == "ro" { + mp.RW = false + } + + // Volumes cannot include an explicitly supplied mode eg c:\path:rw + if mp.Source == "" && mp.Destination != "" && matchgroups["mode"] != "" { + return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) + } + + // Note: No need to check if destination is absolute as it must be by + // definition of matching the regex. + + if filepath.VolumeName(mp.Destination) == mp.Destination { + // Ensure the destination path, if a drive letter, is not the c drive + if strings.ToLower(mp.Destination) == "c:" { + return nil, derr.ErrorCodeVolumeDestIsC.WithArgs(spec) + } + } else { + // So we know the destination is a path, not drive letter. Clean it up. + mp.Destination = filepath.Clean(mp.Destination) + // Ensure the destination path, if a path, is not the c root directory + if strings.ToLower(mp.Destination) == `c:\` { + return nil, derr.ErrorCodeVolumeDestIsCRoot.WithArgs(spec) + } + } + + // See if the source is a name instead of a host directory + if len(mp.Source) > 0 { + validName, err := IsVolumeNameValid(mp.Source) + if err != nil { + return nil, err + } + if validName { + // OK, so the source is a name. + mp.Name = mp.Source + mp.Source = "" + + // Set the driver accordingly + mp.Driver = volumeDriver + if len(mp.Driver) == 0 { + mp.Driver = DefaultDriverName + } + } else { + // OK, so the source must be a host directory. Make sure it's clean. + mp.Source = filepath.Clean(mp.Source) + } + } + + // Ensure the host path source, if supplied, exists and is a directory + if len(mp.Source) > 0 { + var fi os.FileInfo + var err error + if fi, err = os.Stat(mp.Source); err != nil { + return nil, derr.ErrorCodeVolumeSourceNotFound.WithArgs(mp.Source, err) + } + if !fi.IsDir() { + return nil, derr.ErrorCodeVolumeSourceNotDirectory.WithArgs(mp.Source) + } + } + + logrus.Debugf("MP: Source '%s', Dest '%s', RW %t, Name '%s', Driver '%s'", mp.Source, mp.Destination, mp.RW, mp.Name, mp.Driver) + return mp, nil +} + +// 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, derr.ErrorCodeVolumeNameReservedWord.WithArgs(name) + } + return true, nil +}