Windows: Add volume support
Signed-off-by: John Howard <jhoward@microsoft.com>
This commit is contained in:
parent
f33678d1bf
commit
a7e686a779
47 changed files with 1711 additions and 732 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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"}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
11
pkg/system/syscall_unix.go
Normal file
11
pkg/system/syscall_unix.go
Normal file
|
@ -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)
|
||||
}
|
6
pkg/system/syscall_windows.go
Normal file
6
pkg/system/syscall_windows.go
Normal file
|
@ -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) {
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
58
runconfig/fixtures/windows/container_config_1_19.json
Normal file
58
runconfig/fixtures/windows/container_config_1_19.json
Normal file
|
@ -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": ""
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 '/'")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
9
volume/store/store_unix.go
Normal file
9
volume/store/store_unix.go
Normal file
|
@ -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
|
||||
}
|
12
volume/store/store_windows.go
Normal file
12
volume/store/store_windows.go
Normal file
|
@ -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)
|
||||
}
|
147
volume/volume.go
147
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
|
||||
}
|
||||
|
|
261
volume/volume_test.go
Normal file
261
volume/volume_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
132
volume/volume_unix.go
Normal file
132
volume/volume_unix.go
Normal file
|
@ -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
|
||||
}
|
181
volume/volume_windows.go
Normal file
181
volume/volume_windows.go
Normal file
|
@ -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<source>((` + 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<destination>([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<mode>(?i)rw))?`
|
||||
// Temporarily for TP4, disabling the use of ro as it's not supported yet
|
||||
// in the platform. TODO Windows: `(:(?P<mode>(?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
|
||||
}
|
Loading…
Reference in a new issue