Windows: Add volume support

Signed-off-by: John Howard <jhoward@microsoft.com>
This commit is contained in:
John Howard 2015-09-09 19:23:06 -07:00
parent f33678d1bf
commit a7e686a779
47 changed files with 1711 additions and 732 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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 {

View file

@ -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
}

View file

@ -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.

View file

@ -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
}

View file

@ -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),
}
}

View file

@ -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
}

View file

@ -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"`

View file

@ -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"`

View file

@ -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"`

View file

@ -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

View file

@ -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.

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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{

View file

@ -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"}

View file

@ -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")

View 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

View file

@ -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",

View 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)
}

View 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) {
}

View file

@ -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
}

View file

@ -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)
}
}

View 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": ""
}
}

View file

@ -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 {

View file

@ -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 '/'")
}
}

View file

@ -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"})

View file

@ -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 {

View file

@ -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
}

View 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
}

View 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)
}

View file

@ -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
View 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
View 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
View 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
}