From 89782c55eb2b17e4fc72c97037f4faa900d4ce94 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 19 May 2015 13:05:25 -0700 Subject: [PATCH 1/9] Volumes refactor and external plugin implementation. Signed by all authors: Signed-off-by: Michael Crosby Signed-off-by: Arnaud Porterie Signed-off-by: David Calavera Signed-off-by: Jeff Lindsay Signed-off-by: Alexander Morozov Signed-off-by: Luke Marsden Signed-off-by: David Calavera Upstream-commit: 81fa9feb0cdc0773eff99d7393c16271e84aac08 Component: engine --- components/engine/api/client/cli.go | 17 +- components/engine/builder/internals.go | 2 +- components/engine/daemon/container.go | 175 ++++++-- components/engine/daemon/container_linux.go | 9 +- components/engine/daemon/container_windows.go | 6 - components/engine/daemon/create.go | 51 ++- components/engine/daemon/daemon.go | 93 ++-- components/engine/daemon/delete.go | 16 +- components/engine/daemon/inspect.go | 16 +- components/engine/daemon/volumes.go | 411 ++++++++---------- components/engine/daemon/volumes_linux.go | 63 +-- components/engine/daemon/volumes_unit_test.go | 146 +++++++ components/engine/daemon/volumes_windows.go | 10 +- components/engine/docs/mkdocs.yml | 2 + .../docs/sources/reference/api/README.md | 1 + .../reference/api/docker_remote_api.md | 7 + .../reference/api/docker_remote_api_v1.19.md | 7 +- .../docs/sources/reference/api/plugin_api.md | 223 ++++++++++ .../docs/sources/reference/commandline/cli.md | 18 +- .../docs/sources/userguide/dockervolumes.md | 8 + .../engine/docs/sources/userguide/index.md | 6 + .../engine/docs/sources/userguide/plugins.md | 51 +++ .../docker_api_containers_test.go | 45 +- .../integration-cli/docker_cli_daemon_test.go | 99 ----- .../integration-cli/docker_cli_run_test.go | 64 +-- .../integration-cli/docker_cli_start_test.go | 26 -- ...ocker_cli_start_volume_driver_unix_test.go | 150 +++++++ .../integration-cli/docker_test_vars.go | 1 - components/engine/pkg/plugins/client.go | 20 +- components/engine/pkg/plugins/client_test.go | 44 +- components/engine/runconfig/config.go | 1 + components/engine/runconfig/parse.go | 2 + components/engine/utils/tcp.go | 22 + components/engine/volume/drivers/adapter.go | 51 +++ components/engine/volume/drivers/api.go | 20 + components/engine/volume/drivers/extpoint.go | 61 +++ components/engine/volume/drivers/proxy.go | 65 +++ components/engine/volume/local/local.go | 126 ++++++ components/engine/volume/volume.go | 26 ++ components/engine/volumes/repository.go | 193 -------- components/engine/volumes/repository_test.go | 164 ------- components/engine/volumes/volume.go | 152 ------- components/engine/volumes/volume_test.go | 55 --- 43 files changed, 1536 insertions(+), 1189 deletions(-) create mode 100644 components/engine/daemon/volumes_unit_test.go create mode 100644 components/engine/docs/sources/reference/api/plugin_api.md create mode 100644 components/engine/docs/sources/userguide/plugins.md create mode 100644 components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go create mode 100644 components/engine/utils/tcp.go create mode 100644 components/engine/volume/drivers/adapter.go create mode 100644 components/engine/volume/drivers/api.go create mode 100644 components/engine/volume/drivers/extpoint.go create mode 100644 components/engine/volume/drivers/proxy.go create mode 100644 components/engine/volume/local/local.go create mode 100644 components/engine/volume/volume.go delete mode 100644 components/engine/volumes/repository.go delete mode 100644 components/engine/volumes/repository_test.go delete mode 100644 components/engine/volumes/volume.go delete mode 100644 components/engine/volumes/volume_test.go diff --git a/components/engine/api/client/cli.go b/components/engine/api/client/cli.go index b649537c97..f78827dfa1 100644 --- a/components/engine/api/client/cli.go +++ b/components/engine/api/client/cli.go @@ -6,19 +6,18 @@ import ( "errors" "fmt" "io" - "net" "net/http" "os" "path/filepath" "reflect" "strings" "text/template" - "time" "github.com/docker/docker/cliconfig" "github.com/docker/docker/pkg/homedir" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/term" + "github.com/docker/docker/utils" ) // DockerCli represents the docker command line client. @@ -178,19 +177,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, a tr := &http.Transport{ TLSClientConfig: tlsConfig, } - - // Why 32? See https://github.com/docker/docker/pull/8035. - timeout := 32 * time.Second - if proto == "unix" { - // No need for compression in local communications. - tr.DisableCompression = true - tr.Dial = func(_, _ string) (net.Conn, error) { - return net.DialTimeout(proto, addr, timeout) - } - } else { - tr.Proxy = http.ProxyFromEnvironment - tr.Dial = (&net.Dialer{Timeout: timeout}).Dial - } + utils.ConfigureTCPTransport(tr, proto, addr) configFile, e := cliconfig.Load(filepath.Join(homedir.Get(), ".docker")) if e != nil { diff --git a/components/engine/builder/internals.go b/components/engine/builder/internals.go index 5764c19e29..b264918c3a 100644 --- a/components/engine/builder/internals.go +++ b/components/engine/builder/internals.go @@ -773,7 +773,7 @@ func (b *Builder) clearTmp() { fmt.Fprintf(b.OutStream, "Error removing intermediate container %s: %v\n", stringid.TruncateID(c), err) return } - b.Daemon.DeleteVolumes(tmp.VolumePaths()) + b.Daemon.DeleteVolumes(tmp) delete(b.TmpContainers, c) fmt.Fprintf(b.OutStream, "Removing intermediate container %s\n", stringid.TruncateID(c)) } diff --git a/components/engine/daemon/container.go b/components/engine/daemon/container.go index 7a59bec5d7..23a63a52db 100644 --- a/components/engine/daemon/container.go +++ b/components/engine/daemon/container.go @@ -26,9 +26,11 @@ import ( "github.com/docker/docker/pkg/broadcastwriter" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/jsonlog" + "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/runconfig" + "github.com/docker/docker/volume" ) var ( @@ -48,46 +50,37 @@ type StreamConfig struct { // CommonContainer holds the settings for a container which are applicable // across all platforms supported by the daemon. type CommonContainer struct { + StreamConfig + *State `json:"State"` // Needed for remote api version <= 1.11 root string // Path to the "home" of the container, including metadata. basefs string // Path to the graphdriver mountpoint - ID string - - Created time.Time - - Path string - Args []string - - Config *runconfig.Config - ImageID string `json:"Image"` - - NetworkSettings *network.Settings - - ResolvConfPath string - HostnamePath string - HostsPath string - LogPath string - Name string - Driver string - ExecDriver string - - command *execdriver.Command - StreamConfig - - daemon *Daemon + ID string + Created time.Time + Path string + Args []string + Config *runconfig.Config + ImageID string `json:"Image"` + NetworkSettings *network.Settings + ResolvConfPath string + HostnamePath string + HostsPath string + LogPath string + Name string + Driver string + ExecDriver string MountLabel, ProcessLabel string RestartCount int UpdateDns bool + MountPoints map[string]*mountPoint - // Maps container paths to volume paths. The key in this is the path to which - // the volume is being mounted inside the container. Value is the path of the - // volume on disk - Volumes map[string]string hostConfig *runconfig.HostConfig + command *execdriver.Command monitor *containerMonitor execCommands *execStore + daemon *Daemon // logDriver for closing logDriver logger.Logger logCopier *logger.Copier @@ -259,9 +252,6 @@ func (container *Container) Start() (err error) { return err } container.verifyDaemonSettings() - if err := container.prepareVolumes(); err != nil { - return err - } linkedEnv, err := container.setupLinkedContainers() if err != nil { return err @@ -273,10 +263,13 @@ func (container *Container) Start() (err error) { if err := populateCommand(container, env); err != nil { return err } - if err := container.setupMounts(); err != nil { + + mounts, err := container.setupMounts() + if err != nil { return err } + container.command.Mounts = mounts return container.waitForStart() } @@ -571,27 +564,38 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { if err := container.Mount(); err != nil { return nil, err } - defer func() { - if err != nil { - container.Unmount() + var paths []string + unmount := func() { + for _, p := range paths { + syscall.Unmount(p, 0) } - }() - - if err = container.mountVolumes(); err != nil { - container.unmountVolumes() - return nil, err } defer func() { if err != nil { - container.unmountVolumes() + // unmount any volumes + unmount() + // unmount the container's rootfs + container.Unmount() } }() - + mounts, err := container.setupMounts() + if err != nil { + return nil, err + } + for _, m := range mounts { + dest, err := container.GetResourcePath(m.Destination) + if err != nil { + return nil, err + } + paths = append(paths, dest) + if err := mount.Mount(m.Source, dest, "bind", "rbind,ro"); err != nil { + return nil, err + } + } basePath, err := container.GetResourcePath(resource) if err != nil { return nil, err } - stat, err := os.Stat(basePath) if err != nil { return nil, err @@ -605,7 +609,6 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { filter = []string{filepath.Base(basePath)} basePath = filepath.Dir(basePath) } - archive, err := archive.TarWithOptions(basePath, &archive.TarOptions{ Compression: archive.Uncompressed, IncludeFiles: filter, @@ -613,10 +616,9 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { if err != nil { return nil, err } - return ioutils.NewReadCloserWrapper(archive, func() error { err := archive.Close() - container.unmountVolumes() + unmount() container.Unmount() return err }), @@ -1007,3 +1009,84 @@ func copyEscapable(dst io.Writer, src io.ReadCloser) (written int64, err error) } return written, err } + +func (container *Container) networkMounts() []execdriver.Mount { + var mounts []execdriver.Mount + if container.ResolvConfPath != "" { + mounts = append(mounts, execdriver.Mount{ + Source: container.ResolvConfPath, + Destination: "/etc/resolv.conf", + Writable: !container.hostConfig.ReadonlyRootfs, + Private: true, + }) + } + if container.HostnamePath != "" { + mounts = append(mounts, execdriver.Mount{ + Source: container.HostnamePath, + Destination: "/etc/hostname", + Writable: !container.hostConfig.ReadonlyRootfs, + Private: true, + }) + } + if container.HostsPath != "" { + mounts = append(mounts, execdriver.Mount{ + Source: container.HostsPath, + Destination: "/etc/hosts", + Writable: !container.hostConfig.ReadonlyRootfs, + Private: true, + }) + } + return mounts +} + +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 := createVolume(config.Name, config.Driver) + if err != nil { + return err + } + config.Volume = v + } + } + return nil +} + +func (container *Container) RemoveMountPoints() error { + for _, m := range container.MountPoints { + if m.Volume != nil { + if err := removeVolume(m.Volume); err != nil { + return err + } + } + } + return nil +} + +func (container *Container) ShouldRestart() bool { + return container.hostConfig.RestartPolicy.Name == "always" || + (container.hostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0) +} diff --git a/components/engine/daemon/container_linux.go b/components/engine/daemon/container_linux.go index 72e8409275..422da05867 100644 --- a/components/engine/daemon/container_linux.go +++ b/components/engine/daemon/container_linux.go @@ -42,14 +42,7 @@ type Container struct { // Fields below here are platform specific. AppArmorProfile string - - // Store rw/ro in a separate structure to preserve reverse-compatibility on-disk. - // Easier than migrating older container configs :) - VolumesRW map[string]bool - - AppliedVolumesFrom map[string]struct{} - - activeLinks map[string]*links.Link + activeLinks map[string]*links.Link } func killProcessDirectly(container *Container) error { diff --git a/components/engine/daemon/container_windows.go b/components/engine/daemon/container_windows.go index 5f980b5288..141c6c9ef3 100644 --- a/components/engine/daemon/container_windows.go +++ b/components/engine/daemon/container_windows.go @@ -27,12 +27,6 @@ type Container struct { // removed in subsequent PRs. AppArmorProfile string - - // Store rw/ro in a separate structure to preserve reverse-compatibility on-disk. - // Easier than migrating older container configs :) - VolumesRW map[string]bool - - AppliedVolumesFrom map[string]struct{} // ---- END OF TEMPORARY DECLARATION ---- } diff --git a/components/engine/daemon/create.go b/components/engine/daemon/create.go index d8addd3a99..bf55febe02 100644 --- a/components/engine/daemon/create.go +++ b/components/engine/daemon/create.go @@ -2,11 +2,15 @@ package daemon import ( "fmt" + "os" "path/filepath" + "strings" "github.com/docker/docker/graph" "github.com/docker/docker/image" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/runconfig" "github.com/docker/libcontainer/label" ) @@ -87,17 +91,52 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos if err := daemon.createRootfs(container); err != nil { return nil, nil, err } - if hostConfig != nil { - if err := daemon.setHostConfig(container, hostConfig); err != nil { - return nil, nil, err - } + if err := daemon.setHostConfig(container, hostConfig); err != nil { + return nil, nil, err } if err := container.Mount(); err != nil { return nil, nil, err } defer container.Unmount() - if err := container.prepareVolumes(); err != nil { - return nil, nil, err + + for spec := range config.Volumes { + var ( + name, destination string + parts = strings.Split(spec, ":") + ) + switch len(parts) { + case 2: + name, destination = parts[0], filepath.Clean(parts[1]) + default: + name = stringid.GenerateRandomID() + destination = filepath.Clean(parts[0]) + } + // Skip volumes for which we already have something mounted on that + // destination because of a --volume-from. + if container.IsDestinationMounted(destination) { + continue + } + path, err := container.GetResourcePath(destination) + if err != nil { + return nil, nil, err + } + if stat, err := os.Stat(path); err == nil && !stat.IsDir() { + return nil, nil, fmt.Errorf("cannot mount volume over existing file, file exists %s", path) + } + v, err := createVolume(name, config.VolumeDriver) + if err != nil { + return nil, nil, err + } + rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs) + if err != nil { + return nil, nil, err + } + if path, err = v.Mount(); err != nil { + return nil, nil, err + } + copyExistingContents(rootfs, path) + + container.AddMountPointWithVolume(destination, v, true) } if err := container.ToDisk(); err != nil { return nil, nil, err diff --git a/components/engine/daemon/daemon.go b/components/engine/daemon/daemon.go index 96c4e9c736..269e689b99 100644 --- a/components/engine/daemon/daemon.go +++ b/components/engine/daemon/daemon.go @@ -46,9 +46,12 @@ import ( "github.com/docker/docker/runconfig" "github.com/docker/docker/trust" "github.com/docker/docker/utils" - "github.com/docker/docker/volumes" + volumedrivers "github.com/docker/docker/volume/drivers" + "github.com/docker/docker/volume/local" ) +const defaultVolumesPathName = "volumes" + var ( validContainerNameChars = `[a-zA-Z0-9][a-zA-Z0-9_.-]` validContainerNamePattern = regexp.MustCompile(`^/?` + validContainerNameChars + `+$`) @@ -99,7 +102,6 @@ type Daemon struct { repositories *graph.TagStore idIndex *truncindex.TruncIndex sysInfo *sysinfo.SysInfo - volumes *volumes.Repository config *Config containerGraph *graphdb.Database driver graphdriver.Driver @@ -109,6 +111,7 @@ type Daemon struct { RegistryService *registry.Service EventsService *events.Events netController libnetwork.NetworkController + root string } // Get looks for a container using the provided information, which could be @@ -209,7 +212,13 @@ func (daemon *Daemon) register(container *Container, updateSuffixarray bool) err // we'll waste time if we update it for every container daemon.idIndex.Add(container.ID) - container.registerVolumes() + if err := daemon.verifyOldVolumesInfo(container); err != nil { + return err + } + + if err := container.PrepareMountPoints(); err != nil { + return err + } if container.IsRunning() { logrus.Debugf("killing old running container %s", container.ID) @@ -249,10 +258,15 @@ func (daemon *Daemon) ensureName(container *Container) error { } func (daemon *Daemon) restore() error { + type cr struct { + container *Container + registered bool + } + var ( debug = (os.Getenv("DEBUG") != "" || os.Getenv("TEST") != "") - containers = make(map[string]*Container) currentDriver = daemon.driver.String() + containers = make(map[string]*cr) ) if !debug { @@ -278,14 +292,12 @@ func (daemon *Daemon) restore() error { if (container.Driver == "" && currentDriver == "aufs") || container.Driver == currentDriver { logrus.Debugf("Loaded container %v", container.ID) - containers[container.ID] = container + containers[container.ID] = &cr{container: container} } else { logrus.Debugf("Cannot load container %s because it was created with another graph driver.", container.ID) } } - registeredContainers := []*Container{} - if entities := daemon.containerGraph.List("/", -1); entities != nil { for _, p := range entities.Paths() { if !debug && logrus.GetLevel() == logrus.InfoLevel { @@ -294,50 +306,43 @@ func (daemon *Daemon) restore() error { e := entities[p] - if container, ok := containers[e.ID()]; ok { - if err := daemon.register(container, false); err != nil { - logrus.Debugf("Failed to register container %s: %s", container.ID, err) - } - - registeredContainers = append(registeredContainers, container) - - // delete from the map so that a new name is not automatically generated - delete(containers, e.ID()) + if c, ok := containers[e.ID()]; ok { + c.registered = true } } } - // Any containers that are left over do not exist in the graph - for _, container := range containers { - // Try to set the default name for a container if it exists prior to links - container.Name, err = daemon.generateNewName(container.ID) - if err != nil { - logrus.Debugf("Setting default id - %s", err) - } + group := sync.WaitGroup{} + for _, c := range containers { + group.Add(1) - if err := daemon.register(container, false); err != nil { - logrus.Debugf("Failed to register container %s: %s", container.ID, err) - } + go func(container *Container, registered bool) { + defer group.Done() - registeredContainers = append(registeredContainers, container) - } + if !registered { + // Try to set the default name for a container if it exists prior to links + container.Name, err = daemon.generateNewName(container.ID) + if err != nil { + logrus.Debugf("Setting default id - %s", err) + } + } - // check the restart policy on the containers and restart any container with - // the restart policy of "always" - if daemon.config.AutoRestart { - logrus.Debug("Restarting containers...") + if err := daemon.register(container, false); err != nil { + logrus.Debugf("Failed to register container %s: %s", container.ID, err) + } - for _, container := range registeredContainers { - if container.hostConfig.RestartPolicy.IsAlways() || - (container.hostConfig.RestartPolicy.IsOnFailure() && container.ExitCode != 0) { + // check the restart policy on the containers and restart any container with + // the restart policy of "always" + if daemon.config.AutoRestart && container.ShouldRestart() { logrus.Debugf("Starting container %s", container.ID) if err := container.Start(); err != nil { logrus.Debugf("Failed to start container %s: %s", container.ID, err) } } - } + }(c.container, c.registered) } + group.Wait() if !debug { if logrus.GetLevel() == logrus.InfoLevel { @@ -535,6 +540,7 @@ func (daemon *Daemon) newContainer(name string, config *runconfig.Config, imgID ExecDriver: daemon.execDriver.Name(), State: NewState(), execCommands: newExecStore(), + MountPoints: map[string]*mountPoint{}, }, } container.root = daemon.containerRoot(container.ID) @@ -785,15 +791,11 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo return nil, err } - volumesDriver, err := graphdriver.GetDriver("vfs", config.Root, config.GraphOptions) - if err != nil { - return nil, err - } - - volumes, err := volumes.NewRepository(filepath.Join(config.Root, "volumes"), volumesDriver) + volumesDriver, err := local.New(filepath.Join(config.Root, defaultVolumesPathName)) if err != nil { return nil, err } + volumedrivers.Register(volumesDriver, volumesDriver.Name()) trustKey, err := api.LoadOrCreateTrustKey(config.TrustKeyPath) if err != nil { @@ -872,7 +874,6 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo d.repositories = repositories d.idIndex = truncindex.NewTruncIndex([]string{}) d.sysInfo = sysInfo - d.volumes = volumes d.config = config d.sysInitPath = sysInitPath d.execDriver = ed @@ -880,6 +881,7 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo d.defaultLogConfig = config.LogConfig d.RegistryService = registryService d.EventsService = eventsService + d.root = config.Root if err := d.restore(); err != nil { return nil, err @@ -1218,6 +1220,10 @@ func (daemon *Daemon) verifyHostConfig(hostConfig *runconfig.HostConfig) ([]stri } func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig.HostConfig) error { + if err := daemon.registerMountPoints(container, hostConfig); err != nil { + return err + } + container.Lock() defer container.Unlock() if err := parseSecurityOpt(container, hostConfig); err != nil { @@ -1231,6 +1237,5 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig. container.hostConfig = hostConfig container.toDisk() - return nil } diff --git a/components/engine/daemon/delete.go b/components/engine/daemon/delete.go index f82f612ae3..20451bd9de 100644 --- a/components/engine/daemon/delete.go +++ b/components/engine/daemon/delete.go @@ -71,21 +71,12 @@ func (daemon *Daemon) ContainerRm(name string, config *ContainerRmConfig) error } container.LogEvent("destroy") if config.RemoveVolume { - daemon.DeleteVolumes(container.VolumePaths()) + container.RemoveMountPoints() } } return nil } -func (daemon *Daemon) DeleteVolumes(volumeIDs map[string]struct{}) { - for id := range volumeIDs { - if err := daemon.volumes.Delete(id); err != nil { - logrus.Infof("%s", err) - continue - } - } -} - func (daemon *Daemon) Rm(container *Container) (err error) { return daemon.commonRm(container, false) } @@ -134,7 +125,6 @@ func (daemon *Daemon) commonRm(container *Container, forceRemove bool) (err erro } }() - container.derefVolumes() if _, err := daemon.containerGraph.Purge(container.ID); err != nil { logrus.Debugf("Unable to remove container from link graph: %s", err) } @@ -162,3 +152,7 @@ func (daemon *Daemon) commonRm(container *Container, forceRemove bool) (err erro return nil } + +func (daemon *Daemon) DeleteVolumes(c *Container) error { + return c.RemoveMountPoints() +} diff --git a/components/engine/daemon/inspect.go b/components/engine/daemon/inspect.go index 17f8ec42ed..146bd77e9e 100644 --- a/components/engine/daemon/inspect.go +++ b/components/engine/daemon/inspect.go @@ -10,6 +10,10 @@ import ( type ContainerJSONRaw struct { *Container HostConfig *runconfig.HostConfig + + // Unused fields for backward compatibility with API versions < 1.12. + Volumes map[string]string + VolumesRW map[string]bool } func (daemon *Daemon) ContainerInspect(name string) (*types.ContainerJSON, error) { @@ -48,6 +52,14 @@ func (daemon *Daemon) ContainerInspect(name string) (*types.ContainerJSON, error FinishedAt: container.State.FinishedAt, } + volumes := make(map[string]string) + volumesRW := make(map[string]bool) + + for _, m := range container.MountPoints { + volumes[m.Destination] = m.Path() + volumesRW[m.Destination] = m.RW + } + contJSON := &types.ContainerJSON{ Id: container.ID, Created: container.Created, @@ -67,8 +79,8 @@ func (daemon *Daemon) ContainerInspect(name string) (*types.ContainerJSON, error ExecDriver: container.ExecDriver, MountLabel: container.MountLabel, ProcessLabel: container.ProcessLabel, - Volumes: container.Volumes, - VolumesRW: container.VolumesRW, + Volumes: volumes, + VolumesRW: volumesRW, AppArmorProfile: container.AppArmorProfile, ExecIDs: container.GetExecIDs(), HostConfig: &hostConfig, diff --git a/components/engine/daemon/volumes.go b/components/engine/daemon/volumes.go index 794775dcbb..8ca985500b 100644 --- a/components/engine/daemon/volumes.go +++ b/components/engine/daemon/volumes.go @@ -1,213 +1,116 @@ package daemon import ( + "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" - "sort" "strings" - "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/execdriver" "github.com/docker/docker/pkg/chrootarchive" - "github.com/docker/docker/pkg/mount" - "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/runconfig" + "github.com/docker/docker/volume" + volumedrivers "github.com/docker/docker/volume/drivers" ) -type volumeMount struct { - containerPath string - hostPath string - writable bool - copyData bool - from string +var localMountErr = fmt.Errorf("Invalid driver: %s driver doesn't support named volumes", volume.DefaultDriverName) + +type mountPoint struct { + Name string + Destination string + Driver string + RW bool + Volume volume.Volume `json:"-"` + Source string } -func (container *Container) createVolumes() error { - mounts := make(map[string]*volumeMount) +func (m *mountPoint) Setup() (string, error) { + if m.Volume != nil { + return m.Volume.Mount() + } - // get the normal volumes - for path := range container.Config.Volumes { - path = filepath.Clean(path) - // skip if there is already a volume for this container path - if _, exists := container.Volumes[path]; exists { - continue - } - - realPath, err := container.GetResourcePath(path) - if err != nil { - return err - } - if stat, err := os.Stat(realPath); err == nil { - if !stat.IsDir() { - return fmt.Errorf("can't mount to container path, file exists - %s", path) + if len(m.Source) > 0 { + if _, err := os.Stat(m.Source); err != nil { + if !os.IsNotExist(err) { + return "", err + } + if err := os.MkdirAll(m.Source, 0755); err != nil { + return "", err } } - - mnt := &volumeMount{ - containerPath: path, - writable: true, - copyData: true, - } - mounts[mnt.containerPath] = mnt + return m.Source, nil } - // Get all the bind mounts - // track bind paths separately due to #10618 - bindPaths := make(map[string]struct{}) - for _, spec := range container.hostConfig.Binds { - mnt, err := parseBindMountSpec(spec) - if err != nil { - return err - } - - // #10618 - if _, exists := bindPaths[mnt.containerPath]; exists { - return fmt.Errorf("Duplicate volume mount %s", mnt.containerPath) - } - - bindPaths[mnt.containerPath] = struct{}{} - mounts[mnt.containerPath] = mnt - } - - // Get volumes from - for _, from := range container.hostConfig.VolumesFrom { - cID, mode, err := parseVolumesFromSpec(from) - if err != nil { - return err - } - if _, exists := container.AppliedVolumesFrom[cID]; exists { - // skip since it's already been applied - continue - } - - c, err := container.daemon.Get(cID) - if err != nil { - return fmt.Errorf("container %s not found, impossible to mount its volumes", cID) - } - - for _, mnt := range c.volumeMounts() { - mnt.writable = mnt.writable && (mode == "rw") - mnt.from = cID - mounts[mnt.containerPath] = mnt - } - } - - for _, mnt := range mounts { - containerMntPath, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, mnt.containerPath), container.basefs) - if err != nil { - return err - } - - // Create the actual volume - v, err := container.daemon.volumes.FindOrCreateVolume(mnt.hostPath, mnt.writable) - if err != nil { - return err - } - - container.VolumesRW[mnt.containerPath] = mnt.writable - container.Volumes[mnt.containerPath] = v.Path - v.AddContainer(container.ID) - if mnt.from != "" { - container.AppliedVolumesFrom[mnt.from] = struct{}{} - } - - if mnt.writable && mnt.copyData { - // Copy whatever is in the container at the containerPath to the volume - copyExistingContents(containerMntPath, v.Path) - } - } - - return nil + return "", fmt.Errorf("Unable to setup mount point, neither source nor volume defined") } -// sortedVolumeMounts returns the list of container volume mount points sorted in lexicographic order -func (container *Container) sortedVolumeMounts() []string { - var mountPaths []string - for path := range container.Volumes { - mountPaths = append(mountPaths, path) +func (m *mountPoint) Path() string { + if m.Volume != nil { + return m.Volume.Path() } - sort.Strings(mountPaths) - return mountPaths + return m.Source } -func (container *Container) VolumePaths() map[string]struct{} { - var paths = make(map[string]struct{}) - for _, path := range container.Volumes { - paths[path] = struct{}{} +func parseBindMount(spec string, config *runconfig.Config) (*mountPoint, error) { + bind := &mountPoint{ + RW: true, } - return paths -} - -func (container *Container) registerVolumes() { - for path := range container.VolumePaths() { - if v := container.daemon.volumes.Get(path); v != nil { - v.AddContainer(container.ID) - continue - } - - // if container was created with an old daemon, this volume may not be registered so we need to make sure it gets registered - writable := true - if rw, exists := container.VolumesRW[path]; exists { - writable = rw - } - v, err := container.daemon.volumes.FindOrCreateVolume(path, writable) - if err != nil { - logrus.Debugf("error registering volume %s: %v", path, err) - continue - } - v.AddContainer(container.ID) - } -} - -func (container *Container) derefVolumes() { - for path := range container.VolumePaths() { - vol := container.daemon.volumes.Get(path) - if vol == nil { - logrus.Debugf("Volume %s was not found and could not be dereferenced", path) - continue - } - vol.RemoveContainer(container.ID) - } -} - -func parseBindMountSpec(spec string) (*volumeMount, error) { arr := strings.Split(spec, ":") - mnt := &volumeMount{} switch len(arr) { case 2: - mnt.hostPath = arr[0] - mnt.containerPath = arr[1] - mnt.writable = true + bind.Destination = arr[1] case 3: - mnt.hostPath = arr[0] - mnt.containerPath = arr[1] - mnt.writable = validMountMode(arr[2]) && arr[2] == "rw" + bind.Destination = arr[1] + if !validMountMode(arr[2]) { + return nil, fmt.Errorf("invalid mode for volumes-from: %s", arr[2]) + } + bind.RW = arr[2] == "rw" default: return nil, fmt.Errorf("Invalid volume specification: %s", spec) } - if !filepath.IsAbs(mnt.hostPath) { - return nil, fmt.Errorf("cannot bind mount volume: %s volume paths must be absolute.", mnt.hostPath) + if !filepath.IsAbs(arr[0]) { + bind.Driver, bind.Name = parseNamedVolumeInfo(arr[0], config) + if bind.Driver == volume.DefaultDriverName { + return nil, localMountErr + } + } else { + bind.Source = filepath.Clean(arr[0]) } - mnt.hostPath = filepath.Clean(mnt.hostPath) - mnt.containerPath = filepath.Clean(mnt.containerPath) - return mnt, nil + bind.Destination = filepath.Clean(bind.Destination) + return bind, nil } -func parseVolumesFromSpec(spec string) (string, string, error) { - specParts := strings.SplitN(spec, ":", 2) - if len(specParts) == 0 { +func parseNamedVolumeInfo(info string, config *runconfig.Config) (driver string, name string) { + p := strings.SplitN(info, "/", 2) + switch len(p) { + case 2: + driver = p[0] + name = p[1] + default: + if driver = config.VolumeDriver; len(driver) == 0 { + driver = volume.DefaultDriverName + } + name = p[0] + } + + return +} + +func parseVolumesFrom(spec string) (string, string, error) { + if len(spec) == 0 { return "", "", fmt.Errorf("malformed volumes-from specification: %s", spec) } - var ( - id = specParts[0] - mode = "rw" - ) + specParts := strings.SplitN(spec, ":", 2) + id := specParts[0] + mode := "rw" + if len(specParts) == 2 { mode = specParts[1] if !validMountMode(mode) { @@ -222,7 +125,6 @@ func validMountMode(mode string) bool { "rw": true, "ro": true, } - return validModes[mode] } @@ -240,34 +142,16 @@ func (container *Container) specialMounts() []execdriver.Mount { return mounts } -func (container *Container) volumeMounts() map[string]*volumeMount { - mounts := make(map[string]*volumeMount) - - for containerPath, path := range container.Volumes { - v := container.daemon.volumes.Get(path) - if v == nil { - // This should never happen - logrus.Debugf("reference by container %s to non-existent volume path %s", container.ID, path) - continue - } - mounts[containerPath] = &volumeMount{hostPath: path, containerPath: containerPath, writable: container.VolumesRW[containerPath]} - } - - return mounts -} - 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 { @@ -275,60 +159,145 @@ func copyExistingContents(source, destination string) error { } } } - return copyOwnership(source, destination) } -func (container *Container) mountVolumes() error { - for dest, source := range container.Volumes { - v := container.daemon.volumes.Get(source) - if v == nil { - return fmt.Errorf("could not find volume for %s:%s, impossible to mount", source, dest) - } +// 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{} - destPath, err := container.GetResourcePath(dest) + // 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 } - if err := mount.Mount(source, destPath, "bind", "rbind,rw"); err != nil { - return fmt.Errorf("error while mounting volume %s: %v", source, err) - } - } - - for _, mnt := range container.specialMounts() { - destPath, err := container.GetResourcePath(mnt.Destination) + c, err := daemon.Get(containerID) if err != nil { return err } - if err := mount.Mount(mnt.Source, destPath, "bind", "bind,rw"); err != nil { - return fmt.Errorf("error while mounting volume %s: %v", mnt.Source, err) + + for _, m := range c.MountPoints { + cp := m + cp.RW = m.RW && mode != "ro" + + if len(m.Source) == 0 { + v, err := createVolume(m.Name, m.Driver) + 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, container.Config) + if err != nil { + return err + } + + if binds[bind.Destination] { + return fmt.Errorf("Duplicate bind mount %s", bind.Destination) + } + + if len(bind.Name) > 0 && len(bind.Driver) > 0 { + v, err := createVolume(bind.Name, bind.Driver) + if err != nil { + return err + } + bind.Volume = v + } + + binds[bind.Destination] = true + mountPoints[bind.Destination] = bind + } + + container.MountPoints = mountPoints + return nil } -func (container *Container) unmountVolumes() { - for dest := range container.Volumes { - destPath, err := container.GetResourcePath(dest) - if err != nil { - logrus.Errorf("error while unmounting volumes %s: %v", destPath, err) - continue +// verifyOldVolumesInfo ports volumes configured for the containers pre docker 1.7. +// It reads the container configuration and creates valid mount points for the old volumes. +func (daemon *Daemon) verifyOldVolumesInfo(container *Container) error { + jsonPath, err := container.jsonPath() + if err != nil { + return err + } + f, err := os.Open(jsonPath) + if err != nil { + if os.IsNotExist(err) { + return nil } - if err := mount.ForceUnmount(destPath); err != nil { - logrus.Errorf("error while unmounting volumes %s: %v", destPath, err) - continue + return err + } + + type oldContVolCfg struct { + Volumes map[string]string + VolumesRW map[string]bool + } + + vols := oldContVolCfg{ + Volumes: make(map[string]string), + VolumesRW: make(map[string]bool), + } + if err := json.NewDecoder(f).Decode(&vols); err != nil { + return err + } + + for destination, hostPath := range vols.Volumes { + vfsPath := filepath.Join(daemon.root, "vfs", "dir") + + if strings.HasPrefix(hostPath, vfsPath) { + id := filepath.Base(hostPath) + + container.AddLocalMountPoint(id, destination, vols.VolumesRW[destination]) } } - for _, mnt := range container.specialMounts() { - destPath, err := container.GetResourcePath(mnt.Destination) - if err != nil { - logrus.Errorf("error while unmounting volumes %s: %v", destPath, err) - continue - } - if err := mount.ForceUnmount(destPath); err != nil { - logrus.Errorf("error while unmounting volumes %s: %v", destPath, err) - } + return container.ToDisk() +} + +func createVolume(name, driverName string) (volume.Volume, error) { + vd, err := getVolumeDriver(driverName) + if err != nil { + return nil, err } + return vd.Create(name) +} + +func removeVolume(v volume.Volume) error { + vd, err := getVolumeDriver(v.DriverName()) + if err != nil { + return nil + } + return vd.Remove(v) +} + +func getVolumeDriver(name string) (volume.Driver, error) { + if name == "" { + name = volume.DefaultDriverName + } + vd := volumedrivers.Lookup(name) + if vd == nil { + return nil, fmt.Errorf("Volumes Driver %s isn't registered", name) + } + return vd, nil } diff --git a/components/engine/daemon/volumes_linux.go b/components/engine/daemon/volumes_linux.go index 8bb7b8d841..8eea5e067f 100644 --- a/components/engine/daemon/volumes_linux.go +++ b/components/engine/daemon/volumes_linux.go @@ -4,6 +4,9 @@ package daemon import ( "os" + "path/filepath" + "sort" + "strings" "github.com/docker/docker/daemon/execdriver" "github.com/docker/docker/pkg/system" @@ -24,36 +27,44 @@ func copyOwnership(source, destination string) error { return os.Chmod(destination, os.FileMode(stat.Mode())) } -func (container *Container) prepareVolumes() error { - if container.Volumes == nil || len(container.Volumes) == 0 { - container.Volumes = make(map[string]string) - container.VolumesRW = make(map[string]bool) - } +func (container *Container) setupMounts() ([]execdriver.Mount, error) { + var mounts []execdriver.Mount + for _, m := range container.MountPoints { + path, err := m.Setup() + if err != nil { + return nil, err + } - if len(container.hostConfig.VolumesFrom) > 0 && container.AppliedVolumesFrom == nil { - container.AppliedVolumesFrom = make(map[string]struct{}) - } - return container.createVolumes() -} - -func (container *Container) setupMounts() error { - mounts := []execdriver.Mount{} - - // Mount user specified volumes - // Note, these are not private because you may want propagation of (un)mounts from host - // volumes. For instance if you use -v /usr:/usr and the host later mounts /usr/share you - // want this new mount in the container - // These mounts must be ordered based on the length of the path that it is being mounted to (lexicographic) - for _, path := range container.sortedVolumeMounts() { mounts = append(mounts, execdriver.Mount{ - Source: container.Volumes[path], - Destination: path, - Writable: container.VolumesRW[path], + Source: path, + Destination: m.Destination, + Writable: m.RW, }) } - mounts = append(mounts, container.specialMounts()...) + mounts = sortMounts(mounts) + return append(mounts, container.networkMounts()...), nil +} - container.command.Mounts = mounts - return nil +func sortMounts(m []execdriver.Mount) []execdriver.Mount { + sort.Sort(mounts(m)) + return m +} + +type mounts []execdriver.Mount + +func (m mounts) Len() int { + return len(m) +} + +func (m mounts) Less(i, j int) bool { + return m.parts(i) < m.parts(j) +} + +func (m mounts) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} + +func (m mounts) parts(i int) int { + return len(strings.Split(filepath.Clean(m[i].Destination), string(os.PathSeparator))) } diff --git a/components/engine/daemon/volumes_unit_test.go b/components/engine/daemon/volumes_unit_test.go new file mode 100644 index 0000000000..ac17b292fd --- /dev/null +++ b/components/engine/daemon/volumes_unit_test.go @@ -0,0 +1,146 @@ +package daemon + +import ( + "testing" + + "github.com/docker/docker/runconfig" + "github.com/docker/docker/volume" + volumedrivers "github.com/docker/docker/volume/drivers" +) + +func TestParseNamedVolumeInfo(t *testing.T) { + cases := []struct { + driver string + name string + expDriver string + expName string + }{ + {"", "name", "local", "name"}, + {"external", "name", "external", "name"}, + {"", "external/name", "external", "name"}, + {"ignored", "external/name", "external", "name"}, + } + + for _, c := range cases { + conf := &runconfig.Config{VolumeDriver: c.driver} + driver, name := parseNamedVolumeInfo(c.name, conf) + + if driver != c.expDriver { + t.Fatalf("Expected %s, was %s\n", c.expDriver, driver) + } + + if name != c.expName { + t.Fatalf("Expected %s, was %s\n", c.expName, name) + } + } +} + +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", "", "", "", "", "", false, true}, + {"name:/tmp", "external", "/tmp", "", "name", "external", true, false}, + {"external/name:/tmp:rw", "", "/tmp", "", "name", "external", true, false}, + {"external/name:/tmp:ro", "", "/tmp", "", "name", "external", false, false}, + {"external/name:/tmp:foo", "", "/tmp", "", "name", "external", false, true}, + {"name:/tmp", "local", "", "", "", "", false, true}, + {"local/name:/tmp:rw", "", "", "", "", "", true, true}, + } + + for _, c := range cases { + conf := &runconfig.Config{VolumeDriver: c.driver} + m, err := parseBindMount(c.bind, conf) + 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) + } + } +} + +func TestParseVolumeFrom(t *testing.T) { + cases := []struct { + spec string + expId string + expMode string + fail bool + }{ + {"", "", "", true}, + {"foobar", "foobar", "rw", false}, + {"foobar:rw", "foobar", "rw", false}, + {"foobar:ro", "foobar", "ro", false}, + {"foobar:baz", "", "", true}, + } + + for _, c := range cases { + id, mode, err := parseVolumesFrom(c.spec) + if c.fail { + if err == nil { + t.Fatalf("Expected error, was nil, for spec %s\n", c.spec) + } + continue + } + + if id != c.expId { + t.Fatalf("Expected id %s, was %s, for spec %s\n", c.expId, id, c.spec) + } + if mode != c.expMode { + t.Fatalf("Expected mode %s, was %s for spec %s\n", c.expMode, mode, c.spec) + } + } +} + +type fakeDriver struct{} + +func (fakeDriver) Name() string { return "fake" } +func (fakeDriver) Create(name string) (volume.Volume, error) { return nil, nil } +func (fakeDriver) Remove(v volume.Volume) error { return nil } + +func TestGetVolumeDriver(t *testing.T) { + _, err := getVolumeDriver("missing") + if err == nil { + t.Fatal("Expected error, was nil") + } + + volumedrivers.Register(fakeDriver{}, "fake") + d, err := getVolumeDriver("fake") + if err != nil { + t.Fatal(err) + } + if d.Name() != "fake" { + t.Fatalf("Expected fake driver, got %s\n", d.Name()) + } +} diff --git a/components/engine/daemon/volumes_windows.go b/components/engine/daemon/volumes_windows.go index c910302daf..762f404883 100644 --- a/components/engine/daemon/volumes_windows.go +++ b/components/engine/daemon/volumes_windows.go @@ -2,15 +2,13 @@ package daemon +import "github.com/docker/docker/daemon/execdriver" + // Not supported on Windows func copyOwnership(source, destination string) error { - return nil + return nil, nil } -func (container *Container) prepareVolumes() error { - return nil -} - -func (container *Container) setupMounts() error { +func (container *Container) setupMounts() ([]execdriver.Mount, error) { return nil } diff --git a/components/engine/docs/mkdocs.yml b/components/engine/docs/mkdocs.yml index b501c9c398..d357983005 100644 --- a/components/engine/docs/mkdocs.yml +++ b/components/engine/docs/mkdocs.yml @@ -73,6 +73,7 @@ pages: - ['machine/index.md', 'User Guide', 'Docker Machine' ] - ['swarm/index.md', 'User Guide', 'Docker Swarm' ] - ['kitematic/userguide.md', 'User Guide', 'Kitematic'] +- ['userguide/plugins.md', 'User Guide', 'Docker Plugins'] # Docker Hub docs: - ['docker-hub/index.md', 'Docker Hub', 'Docker Hub' ] @@ -185,6 +186,7 @@ pages: - ['reference/api/docker_remote_api_v1.0.md', '**HIDDEN**'] - ['reference/api/remote_api_client_libraries.md', 'Reference', 'Docker Remote API client libraries'] - ['reference/api/docker_io_accounts_api.md', 'Reference', 'Docker Hub accounts API'] +- ['reference/api/plugin_api.md', 'Reference', 'Docker Plugin API'] - ['kitematic/faq.md', 'Reference', 'Kitematic: FAQ'] - ['kitematic/known-issues.md', 'Reference', 'Kitematic: Known issues'] diff --git a/components/engine/docs/sources/reference/api/README.md b/components/engine/docs/sources/reference/api/README.md index ec1cbcb2c3..4f72fbfe33 100644 --- a/components/engine/docs/sources/reference/api/README.md +++ b/components/engine/docs/sources/reference/api/README.md @@ -7,3 +7,4 @@ This directory holds the authoritative specifications of APIs defined and implem index for images to download * The docker.io OAuth and accounts API which 3rd party services can use to access account information + * The plugin API for Docker Plugins diff --git a/components/engine/docs/sources/reference/api/docker_remote_api.md b/components/engine/docs/sources/reference/api/docker_remote_api.md index 4c77cab8cf..79983ed580 100644 --- a/components/engine/docs/sources/reference/api/docker_remote_api.md +++ b/components/engine/docs/sources/reference/api/docker_remote_api.md @@ -73,6 +73,13 @@ are now returned as boolean instead of as an int. In addition, the end point now returns the new boolean fields `CpuCfsPeriod`, `CpuCfsQuota`, and `OomKillDisable`. +**New!** + +You can now specify a volume plugin in `/v1.19/containers/create`, for example +`"HostConfig": {"Binds": ["flocker/name:/data"]}` where `flocker` is the name +of the plugin, `name` is the user-facing name of the volume (passed to the +volume plugin) and `/data` is the mountpoint inside the container. + ## v1.18 ### Full documentation diff --git a/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md b/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md index dde8ee79a1..5175ed9782 100644 --- a/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md +++ b/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md @@ -226,8 +226,11 @@ Json Parameters: - **Binds** – A list of volume bindings for this container. Each volume binding is a string of the form `container_path` (to create a new volume for the container), `host_path:container_path` (to bind-mount - a host path into the container), or `host_path:container_path:ro` - (to make the bind-mount read-only inside the container). + a host path into the container), `host_path:container_path:ro` + (to make the bind-mount read-only inside the container), or + `volume_plugin/volume_name:container_path` (to provision a + volume named `volume_name` from a [volume plugin](/userguide/plugins) + named `volume_plugin`). - **Links** - A list of links for the container. Each link entry should be in the form of `container_name:alias`. - **LxcConf** - LXC specific configurations. These configurations will only diff --git a/components/engine/docs/sources/reference/api/plugin_api.md b/components/engine/docs/sources/reference/api/plugin_api.md new file mode 100644 index 0000000000..770f606b4c --- /dev/null +++ b/components/engine/docs/sources/reference/api/plugin_api.md @@ -0,0 +1,223 @@ +page_title: Plugin API documentation +page_description: Documentation for writing a Docker plugin. +page_keywords: docker, plugins, api, extensions + +# Docker Plugin API + +Docker plugins are out-of-process extensions which add capabilities to the +Docker Engine. + +This page is intended for people who want to develop their own Docker plugin. +If you just want to learn about or use Docker plugins, look +[here](/userguide/plugins). + +## What plugins are + +A plugin is a process running on the same docker host as the docker daemon, +which registers itself by placing a file in `/usr/share/docker/plugins` (the +"plugin directory"). + +Plugins have human-readable names, which are short, lowercase strings. For +example, `flocker` or `weave`. + +Plugins can run inside or outside containers. Currently running them outside +containers is recommended. + +## Plugin discovery + +Docker discovers plugins by looking for them in the plugin directory whenever a +user or container tries to use one by name. + +There are two types of files which can be put in the plugin directory. + +* `.sock` files are UNIX domain sockets. +* `.spec` files are text files containing a URL, such as `unix:///other.sock`. + +The name of the file (excluding the extension) determines the plugin name. + +For example, the `flocker` plugin might create a UNIX socket at +`/usr/share/docker/plugins/flocker.sock`. + +Plugins must be run locally on the same machine as the Docker daemon. UNIX +domain sockets are strongly encouraged for security reasons. + +## Plugin lifecycle + +Plugins should be started before Docker, and stopped after Docker. For +example, when packaging a plugin for a platform which supports `systemd`, you +might use [`systemd` dependencies]( +http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before=) to +manage startup and shutdown order. + +When upgrading a plugin, you should first stop the Docker daemon, upgrade the +plugin, then start Docker again. + +If a plugin is packaged as a container, this may cause issues. Plugins as +containers are currently considered experimental due to these shutdown/startup +ordering issues. These issues are mitigated by plugin retries (see below). + +## Plugin activation + +When a plugin is first referred to -- either by a user referring to it by name +(e.g. `docker run --volume-driver=foo`) or a container already configured to +use a plugin being started -- Docker looks for the named plugin in the plugin +directory and activates it with a handshake. See Handshake API below. + +Plugins are *not* activated automatically at Docker daemon startup. Rather, +they are activated only lazily, or on-demand, when they are needed. + +## API design + +The Plugin API is RPC-style JSON over HTTP, much like webhooks. + +Requests flow *from* the Docker daemon *to* the plugin. So the plugin needs to +implement an HTTP server and bind this to the UNIX socket mentioned in the +"plugin discovery" section. + +All requests are HTTP `POST` requests. + +The API is versioned via an Accept header, which currently is always set to +`application/vnd.docker.plugins.v1+json`. + +## Handshake API + +Plugins are activated via the following "handshake" API call. + +### /Plugin.Activate + +**Request:** empty body + +**Response:** +``` +{ + "Implements": ["VolumeDriver"] +} +``` + +Responds with a list of Docker subsystems which this plugin implements. +After activation, the plugin will then be sent events from this subsystem. + +## Volume API + +If a plugin registers itself as a `VolumeDriver` (see above) then it is +expected to provide writeable paths on the host filesystem for the Docker +daemon to provide to containers to consume. + +The Docker daemon handles bind-mounting the provided paths into user +containers. + +### /VolumeDriver.Create + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Instruct the plugin that the user wants to create a volume, given a user +specified volume name. The plugin does not need to actually manifest the +volume on the filesystem yet (until Mount is called). + +**Response**: +``` +{ + "Err": null +} +``` + +Respond with a string error if an error occurred. + +### /VolumeDriver.Remove + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Create a volume, given a user specified volume name. + +**Response**: +``` +{ + "Err": null +} +``` + +Respond with a string error if an error occurred. + +### /VolumeDriver.Mount + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Docker requires the plugin to provide a volume, given a user specified volume +name. This is called once per container start. + +**Response**: +``` +{ + "Mountpoint": "/path/to/directory/on/host", + "Err": null +} +``` + +Respond with the path on the host filesystem where the volume has been made +available, and/or a string error if an error occurred. + +### /VolumeDriver.Path + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Docker needs reminding of the path to the volume on the host. + +**Response**: +``` +{ + "Mountpoint": "/path/to/directory/on/host", + "Err": null +} +``` + +Respond with the path on the host filesystem where the volume has been made +available, and/or a string error if an error occurred. + +### /VolumeDriver.Unmount + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Indication that Docker no longer is using the named volume. This is called once +per container stop. Plugin may deduce that it is safe to deprovision it at +this point. + +**Response**: +``` +{ + "Err": null +} +``` + +Respond with a string error if an error occurred. + +## Plugin retries + +Attempts to call a method on a plugin are retried with an exponential backoff +for up to 30 seconds. This may help when packaging plugins as containers, since +it gives plugin containers a chance to start up before failing any user +containers which depend on them. diff --git a/components/engine/docs/sources/reference/commandline/cli.md b/components/engine/docs/sources/reference/commandline/cli.md index a4a44fbb54..e1c9f56ad3 100644 --- a/components/engine/docs/sources/reference/commandline/cli.md +++ b/components/engine/docs/sources/reference/commandline/cli.md @@ -1000,7 +1000,8 @@ Creates a new container. --security-opt=[] Security options -t, --tty=false Allocate a pseudo-TTY -u, --user="" Username or UID - -v, --volume=[] Bind mount a volume + -v, --volume=[] Bind mount a volume, or specify name for volume plugin + --volume-driver= Optional volume driver (plugin name) for the container --volumes-from=[] Mount volumes from the specified container(s) -w, --workdir="" Working directory inside the container @@ -1970,7 +1971,8 @@ To remove an image using its digest: --sig-proxy=true Proxy received signals to the process -t, --tty=false Allocate a pseudo-TTY -u, --user="" Username or UID (format: [:]) - -v, --volume=[] Bind mount a volume + -v, --volume=[] Bind mount a volume, or specify name for volume plugin + --volume-driver= Optional volume driver (plugin name) for the container --volumes-from=[] Mount volumes from the specified container(s) -w, --workdir="" Working directory inside the container @@ -2066,6 +2068,18 @@ binary (such as that provided by [https://get.docker.com]( https://get.docker.com)), you give the container the full access to create and manipulate the host's Docker daemon. + $ docker run -ti -v volumename:/data --volume-driver=flocker busybox sh + +By specifying a volume name in conjunction with a volume driver, volume plugins +such as [Flocker](https://clusterhq.com/docker-plugin/), once installed, can be +used to manage volumes external to a single host, such as those on EBS. In this +example, "volumename" is passed through to the volume plugin as a user-given +name for the volume which allows the plugin to associate it with an external +volume beyond the lifetime of a single container or container host. This can be +used, for example, to move a stateful container from one server to another. + +The `volumename` must not begin with a `/`. + $ docker run -p 127.0.0.1:80:8080 ubuntu bash This binds port `8080` of the container to port `80` on `127.0.0.1` of diff --git a/components/engine/docs/sources/userguide/dockervolumes.md b/components/engine/docs/sources/userguide/dockervolumes.md index c7126d7c33..c993d61814 100644 --- a/components/engine/docs/sources/userguide/dockervolumes.md +++ b/components/engine/docs/sources/userguide/dockervolumes.md @@ -210,6 +210,14 @@ Then un-tar the backup file in the new container's data volume. You can use the techniques above to automate backup, migration and restore testing using your preferred tools. +## Integrating Docker with external storage systems + +Docker volume plugins such as [Flocker](https://clusterhq.com/docker-plugin/) +enable Docker deployments to be integrated with external storage systems, such +as Amazon EBS, and enable data volumes to persist beyond the lifetime of a +single Docker host. See the [plugin section of the user +guide](/userguide/plugins) for more information. + # Next steps Now we've learned a bit more about how to use Docker we're going to see how to diff --git a/components/engine/docs/sources/userguide/index.md b/components/engine/docs/sources/userguide/index.md index 9cc1c6db30..f82e8c70fc 100644 --- a/components/engine/docs/sources/userguide/index.md +++ b/components/engine/docs/sources/userguide/index.md @@ -105,6 +105,12 @@ works with Docker can now transparently scale up to multiple hosts. Go to [Docker Swarm user guide](/swarm/). +## Docker Plugins + +Docker plugins allow you to extend the capabilities of the Docker Engine. + +Go to [Docker Plugins](/userguide/plugins). + ## Getting help * [Docker homepage](http://www.docker.com/) diff --git a/components/engine/docs/sources/userguide/plugins.md b/components/engine/docs/sources/userguide/plugins.md new file mode 100644 index 0000000000..5fc7864010 --- /dev/null +++ b/components/engine/docs/sources/userguide/plugins.md @@ -0,0 +1,51 @@ +page_title: Docker Plugins +page_description: Learn what Docker Plugins are and how to use them. +page_keywords: plugins, extensions, extensibility + +# Understanding Docker Plugins + +You can extend the capabilities of the Docker Engine by loading third-party +plugins. + +## Types of plugins + +Plugins extend Docker's functionality. They come in specific types. For +example, a **volume plugin** might enable Docker volumes to persist across +multiple Docker hosts. + +Currently Docker supports **volume plugins**. In the future it will support +additional plugin types. + +## Installing a plugin + +Follow the instructions in the plugin's documentation. + +## Finding a plugin + +The following plugins exist: + +* The [Flocker plugin](https://clusterhq.com/docker-plugin/) is a volume plugin + which provides multi-host portable volumes for Docker, enabling you to run + databases and other stateful containers and move them around across a cluster + of machines. + +## Using a plugin + +Depending on the plugin type, there are additional arguments to `docker` CLI +commands. + +* For example `docker run` has a [`--volume-driver` argument]( + /reference/commandline/cli/#run). + +You can also use plugins via the [Docker Remote API]( +/reference/api/docker_remote_api/). + +## Troubleshooting a plugin + +If you are having problems with Docker after loading a plugin, ask the authors +of the plugin for help. The Docker team may not be able to assist you. + +## Writing a plugin + +If you are interested in writing a plugin for Docker, or seeing how they work +under the hood, see the [docker plugins reference](/reference/api/plugin_api). diff --git a/components/engine/integration-cli/docker_api_containers_test.go b/components/engine/integration-cli/docker_api_containers_test.go index ec68f550c3..363d279120 100644 --- a/components/engine/integration-cli/docker_api_containers_test.go +++ b/components/engine/integration-cli/docker_api_containers_test.go @@ -166,7 +166,7 @@ func (s *DockerSuite) TestContainerApiStartDupVolumeBinds(c *check.C) { c.Assert(status, check.Equals, http.StatusInternalServerError) c.Assert(err, check.IsNil) - if !strings.Contains(string(body), "Duplicate volume") { + if !strings.Contains(string(body), "Duplicate bind") { c.Fatalf("Expected failure due to duplicate bind mounts to same path, instead got: %q with error: %v", string(body), err) } } @@ -210,49 +210,6 @@ func (s *DockerSuite) TestContainerApiStartVolumesFrom(c *check.C) { } } -// Ensure that volumes-from has priority over binds/anything else -// This is pretty much the same as TestRunApplyVolumesFromBeforeVolumes, except with passing the VolumesFrom and the bind on start -func (s *DockerSuite) TestVolumesFromHasPriority(c *check.C) { - volName := "voltst2" - volPath := "/tmp" - - if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", volName, "-v", volPath, "busybox")); err != nil { - c.Fatal(out, err) - } - - name := "testing" - config := map[string]interface{}{ - "Image": "busybox", - "Volumes": map[string]struct{}{volPath: {}}, - } - - status, _, err := sockRequest("POST", "/containers/create?name="+name, config) - c.Assert(status, check.Equals, http.StatusCreated) - c.Assert(err, check.IsNil) - - bindPath := randomUnixTmpDirPath("test") - config = map[string]interface{}{ - "VolumesFrom": []string{volName}, - "Binds": []string{bindPath + ":/tmp"}, - } - status, _, err = sockRequest("POST", "/containers/"+name+"/start", config) - c.Assert(status, check.Equals, http.StatusNoContent) - c.Assert(err, check.IsNil) - - pth, err := inspectFieldMap(name, "Volumes", volPath) - if err != nil { - c.Fatal(err) - } - pth2, err := inspectFieldMap(volName, "Volumes", volPath) - if err != nil { - c.Fatal(err) - } - - if pth != pth2 { - c.Fatalf("expected volume host path to be %s, got %s", pth, pth2) - } -} - func (s *DockerSuite) TestGetContainerStats(c *check.C) { var ( name = "statscontainer" diff --git a/components/engine/integration-cli/docker_cli_daemon_test.go b/components/engine/integration-cli/docker_cli_daemon_test.go index ef22c993bb..e6c1255d4c 100644 --- a/components/engine/integration-cli/docker_cli_daemon_test.go +++ b/components/engine/integration-cli/docker_cli_daemon_test.go @@ -284,35 +284,6 @@ func (s *DockerDaemonSuite) TestDaemonAllocatesListeningPort(c *check.C) { } } -// #9629 -func (s *DockerDaemonSuite) TestDaemonVolumesBindsRefs(c *check.C) { - if err := s.d.StartWithBusybox(); err != nil { - c.Fatal(err) - } - - tmp, err := ioutil.TempDir(os.TempDir(), "") - if err != nil { - c.Fatal(err) - } - defer os.RemoveAll(tmp) - - if err := ioutil.WriteFile(tmp+"/test", []byte("testing"), 0655); err != nil { - c.Fatal(err) - } - - if out, err := s.d.Cmd("create", "-v", tmp+":/foo", "--name=voltest", "busybox"); err != nil { - c.Fatal(err, out) - } - - if err := s.d.Restart(); err != nil { - c.Fatal(err) - } - - if out, err := s.d.Cmd("run", "--volumes-from=voltest", "--name=consumer", "busybox", "/bin/sh", "-c", "[ -f /foo/test ]"); err != nil { - c.Fatal(err, out) - } -} - func (s *DockerDaemonSuite) TestDaemonKeyGeneration(c *check.C) { // TODO: skip or update for Windows daemon os.Remove("/etc/docker/key.json") @@ -360,76 +331,6 @@ func (s *DockerDaemonSuite) TestDaemonKeyMigration(c *check.C) { } } -// Simulate an older daemon (pre 1.3) coming up with volumes specified in containers -// without corresponding volume json -func (s *DockerDaemonSuite) TestDaemonUpgradeWithVolumes(c *check.C) { - graphDir := filepath.Join(os.TempDir(), "docker-test") - defer os.RemoveAll(graphDir) - if err := s.d.StartWithBusybox("-g", graphDir); err != nil { - c.Fatal(err) - } - - tmpDir := filepath.Join(os.TempDir(), "test") - defer os.RemoveAll(tmpDir) - - if out, err := s.d.Cmd("create", "-v", tmpDir+":/foo", "--name=test", "busybox"); err != nil { - c.Fatal(err, out) - } - - if err := s.d.Stop(); err != nil { - c.Fatal(err) - } - - // Remove this since we're expecting the daemon to re-create it too - if err := os.RemoveAll(tmpDir); err != nil { - c.Fatal(err) - } - - configDir := filepath.Join(graphDir, "volumes") - - if err := os.RemoveAll(configDir); err != nil { - c.Fatal(err) - } - - if err := s.d.Start("-g", graphDir); err != nil { - c.Fatal(err) - } - - if _, err := os.Stat(tmpDir); os.IsNotExist(err) { - c.Fatalf("expected volume path %s to exist but it does not", tmpDir) - } - - dir, err := ioutil.ReadDir(configDir) - if err != nil { - c.Fatal(err) - } - if len(dir) == 0 { - c.Fatalf("expected volumes config dir to contain data for new volume") - } - - // Now with just removing the volume config and not the volume data - if err := s.d.Stop(); err != nil { - c.Fatal(err) - } - - if err := os.RemoveAll(configDir); err != nil { - c.Fatal(err) - } - - if err := s.d.Start("-g", graphDir); err != nil { - c.Fatal(err) - } - - dir, err = ioutil.ReadDir(configDir) - if err != nil { - c.Fatal(err) - } - - if len(dir) == 0 { - c.Fatalf("expected volumes config dir to contain data for new volume") - } -} - // GH#11320 - verify that the daemon exits on failure properly // Note that this explicitly tests the conflict of {-b,--bridge} and {--bip} options as the means // to get a daemon init failure; no other tests for -b/--bip conflict are therefore required diff --git a/components/engine/integration-cli/docker_cli_run_test.go b/components/engine/integration-cli/docker_cli_run_test.go index 0051f280e1..7e60675045 100644 --- a/components/engine/integration-cli/docker_cli_run_test.go +++ b/components/engine/integration-cli/docker_cli_run_test.go @@ -395,21 +395,6 @@ func (s *DockerSuite) TestRunModeNetContainerHostname(c *check.C) { } } -// Regression test for #4741 -func (s *DockerSuite) TestRunWithVolumesAsFiles(c *check.C) { - runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "--volume", "/etc/hosts:/target-file", "busybox", "true") - out, stderr, exitCode, err := runCommandWithStdoutStderr(runCmd) - if err != nil && exitCode != 0 { - c.Fatal("1", out, stderr, err) - } - - runCmd = exec.Command(dockerBinary, "run", "--volumes-from", "test-data", "busybox", "cat", "/target-file") - out, stderr, exitCode, err = runCommandWithStdoutStderr(runCmd) - if err != nil && exitCode != 0 { - c.Fatal("2", out, stderr, err) - } -} - // Regression test for #4979 func (s *DockerSuite) TestRunWithVolumesFromExited(c *check.C) { runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "--volume", "/some/dir", "busybox", "touch", "/some/dir/file") @@ -536,7 +521,7 @@ func (s *DockerSuite) TestRunNoDupVolumes(c *check.C) { if out, _, err := runCommandWithOutput(cmd); err == nil { c.Fatal("Expected error about duplicate volume definitions") } else { - if !strings.Contains(out, "Duplicate volume") { + if !strings.Contains(out, "Duplicate bind mount") { c.Fatalf("Expected 'duplicate volume' error, got %v", err) } } @@ -2333,7 +2318,13 @@ func (s *DockerSuite) TestRunMountOrdering(c *check.C) { c.Fatal(err) } - cmd := exec.Command(dockerBinary, "run", "-v", fmt.Sprintf("%s:/tmp", tmpDir), "-v", fmt.Sprintf("%s:/tmp/foo", fooDir), "-v", fmt.Sprintf("%s:/tmp/tmp2", tmpDir2), "-v", fmt.Sprintf("%s:/tmp/tmp2/foo", fooDir), "busybox:latest", "sh", "-c", "ls /tmp/touch-me && ls /tmp/foo/touch-me && ls /tmp/tmp2/touch-me && ls /tmp/tmp2/foo/touch-me") + cmd := exec.Command(dockerBinary, "run", + "-v", fmt.Sprintf("%s:/tmp", tmpDir), + "-v", fmt.Sprintf("%s:/tmp/foo", fooDir), + "-v", fmt.Sprintf("%s:/tmp/tmp2", tmpDir2), + "-v", fmt.Sprintf("%s:/tmp/tmp2/foo", fooDir), + "busybox:latest", "sh", "-c", + "ls /tmp/touch-me && ls /tmp/foo/touch-me && ls /tmp/tmp2/touch-me && ls /tmp/tmp2/foo/touch-me") out, _, err := runCommandWithOutput(cmd) if err != nil { c.Fatal(out, err) @@ -2427,41 +2418,6 @@ func (s *DockerSuite) TestVolumesNoCopyData(c *check.C) { } } -func (s *DockerSuite) TestRunVolumesNotRecreatedOnStart(c *check.C) { - testRequires(c, SameHostDaemon) - - // Clear out any remnants from other tests - info, err := ioutil.ReadDir(volumesConfigPath) - if err != nil { - c.Fatal(err) - } - if len(info) > 0 { - for _, f := range info { - if err := os.RemoveAll(volumesConfigPath + "/" + f.Name()); err != nil { - c.Fatal(err) - } - } - } - - cmd := exec.Command(dockerBinary, "run", "-v", "/foo", "--name", "lone_starr", "busybox") - if _, err := runCommand(cmd); err != nil { - c.Fatal(err) - } - - cmd = exec.Command(dockerBinary, "start", "lone_starr") - if _, err := runCommand(cmd); err != nil { - c.Fatal(err) - } - - info, err = ioutil.ReadDir(volumesConfigPath) - if err != nil { - c.Fatal(err) - } - if len(info) != 1 { - c.Fatalf("Expected only 1 volume have %v", len(info)) - } -} - func (s *DockerSuite) TestRunNoOutputFromPullInStdout(c *check.C) { // just run with unknown image cmd := exec.Command(dockerBinary, "run", "asdfsg") @@ -2496,7 +2452,7 @@ func (s *DockerSuite) TestRunVolumesCleanPaths(c *check.C) { out, err = inspectFieldMap("dark_helmet", "Volumes", "/foo") c.Assert(err, check.IsNil) - if !strings.Contains(out, volumesStoragePath) { + if !strings.Contains(out, volumesConfigPath) { c.Fatalf("Volume was not defined for /foo\n%q", out) } @@ -2507,7 +2463,7 @@ func (s *DockerSuite) TestRunVolumesCleanPaths(c *check.C) { } out, err = inspectFieldMap("dark_helmet", "Volumes", "/bar") c.Assert(err, check.IsNil) - if !strings.Contains(out, volumesStoragePath) { + if !strings.Contains(out, volumesConfigPath) { c.Fatalf("Volume was not defined for /bar\n%q", out) } } diff --git a/components/engine/integration-cli/docker_cli_start_test.go b/components/engine/integration-cli/docker_cli_start_test.go index caf8428738..0475826738 100644 --- a/components/engine/integration-cli/docker_cli_start_test.go +++ b/components/engine/integration-cli/docker_cli_start_test.go @@ -126,32 +126,6 @@ func (s *DockerSuite) TestStartRecordError(c *check.C) { } -// gh#8726: a failed Start() breaks --volumes-from on subsequent Start()'s -func (s *DockerSuite) TestStartVolumesFromFailsCleanly(c *check.C) { - - // Create the first data volume - dockerCmd(c, "run", "-d", "--name", "data_before", "-v", "/foo", "busybox") - - // Expect this to fail because the data test after contaienr doesn't exist yet - if _, err := runCommand(exec.Command(dockerBinary, "run", "-d", "--name", "consumer", "--volumes-from", "data_before", "--volumes-from", "data_after", "busybox")); err == nil { - c.Fatal("Expected error but got none") - } - - // Create the second data volume - dockerCmd(c, "run", "-d", "--name", "data_after", "-v", "/bar", "busybox") - - // Now, all the volumes should be there - dockerCmd(c, "start", "consumer") - - // Check that we have the volumes we want - out, _ := dockerCmd(c, "inspect", "--format='{{ len .Volumes }}'", "consumer") - nVolumes := strings.Trim(out, " \r\n'") - if nVolumes != "2" { - c.Fatalf("Missing volumes: expected 2, got %s", nVolumes) - } - -} - func (s *DockerSuite) TestStartPausedContainer(c *check.C) { defer unpauseAllContainers() diff --git a/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go b/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go new file mode 100644 index 0000000000..4d6d44a537 --- /dev/null +++ b/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go @@ -0,0 +1,150 @@ +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/go-check/check" +) + +func init() { + check.Suite(&ExternalVolumeSuite{ + ds: &DockerSuite{}, + }) +} + +type ExternalVolumeSuite struct { + server *httptest.Server + ds *DockerSuite +} + +func (s *ExternalVolumeSuite) SetUpTest(c *check.C) { + s.ds.SetUpTest(c) +} + +func (s *ExternalVolumeSuite) TearDownTest(c *check.C) { + s.ds.TearDownTest(c) +} + +func (s *ExternalVolumeSuite) SetUpSuite(c *check.C) { + mux := http.NewServeMux() + s.server = httptest.NewServer(mux) + + type pluginRequest struct { + name string + } + + hostVolumePath := func(name string) string { + return fmt.Sprintf("/var/lib/docker/volumes/%s", name) + } + + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{"Implements": ["VolumeDriver"]}`) + }) + + mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{}`) + }) + + mux.HandleFunc("/VolumeDriver.Remove", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{}`) + }) + + mux.HandleFunc("/VolumeDriver.Path", func(w http.ResponseWriter, r *http.Request) { + var pr pluginRequest + if err := json.NewDecoder(r.Body).Decode(&pr); err != nil { + http.Error(w, err.Error(), 500) + } + + p := hostVolumePath(pr.name) + + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, fmt.Sprintf("{\"Mountpoint\": \"%s\"}", p)) + }) + + mux.HandleFunc("/VolumeDriver.Mount", func(w http.ResponseWriter, r *http.Request) { + var pr pluginRequest + if err := json.NewDecoder(r.Body).Decode(&pr); err != nil { + http.Error(w, err.Error(), 500) + } + + p := hostVolumePath(pr.name) + if err := os.MkdirAll(p, 0755); err != nil { + http.Error(w, err.Error(), 500) + } + + if err := ioutil.WriteFile(filepath.Join(p, "test"), []byte(s.server.URL), 0644); err != nil { + http.Error(w, err.Error(), 500) + } + + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, fmt.Sprintf("{\"Mountpoint\": \"%s\"}", p)) + }) + + mux.HandleFunc("/VolumeDriver.Umount", func(w http.ResponseWriter, r *http.Request) { + var pr pluginRequest + if err := json.NewDecoder(r.Body).Decode(&pr); err != nil { + http.Error(w, err.Error(), 500) + } + + p := hostVolumePath(pr.name) + if err := os.RemoveAll(p); err != nil { + http.Error(w, err.Error(), 500) + } + + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") + fmt.Fprintln(w, `{}`) + }) + + if err := os.MkdirAll("/usr/share/docker/plugins", 0755); err != nil { + c.Fatal(err) + } + + if err := ioutil.WriteFile("/usr/share/docker/plugins/test-external-volume-driver.spec", []byte(s.server.URL), 0644); err != nil { + c.Fatal(err) + } +} + +func (s *ExternalVolumeSuite) TearDownSuite(c *check.C) { + s.server.Close() + + if err := os.RemoveAll("/usr/share/docker/plugins"); err != nil { + c.Fatal(err) + } +} + +func (s *ExternalVolumeSuite) TestStartExternalVolumeDriver(c *check.C) { + runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", "test-external-volume-driver", "busybox:latest", "cat", "/tmp/external-volume-test/test") + out, stderr, exitCode, err := runCommandWithStdoutStderr(runCmd) + if err != nil && exitCode != 0 { + c.Fatal(out, stderr, err) + } + + if !strings.Contains(out, s.server.URL) { + c.Fatalf("External volume mount failed. Output: %s\n", out) + } +} + +func (s *ExternalVolumeSuite) TestStartExternalVolumeNamedDriver(c *check.C) { + runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "-v", "test-external-volume-driver/volume-1:/tmp/external-volume-test", "busybox:latest", "cat", "/tmp/external-volume-test/test") + out, stderr, exitCode, err := runCommandWithStdoutStderr(runCmd) + if err != nil && exitCode != 0 { + c.Fatal(out, stderr, err) + } + + if !strings.Contains(out, s.server.URL) { + c.Fatalf("External volume mount failed. Output: %s\n", out) + } +} diff --git a/components/engine/integration-cli/docker_test_vars.go b/components/engine/integration-cli/docker_test_vars.go index 9cb28b274e..ed394d26dd 100644 --- a/components/engine/integration-cli/docker_test_vars.go +++ b/components/engine/integration-cli/docker_test_vars.go @@ -18,7 +18,6 @@ var ( dockerBasePath = "/var/lib/docker" volumesConfigPath = dockerBasePath + "/volumes" - volumesStoragePath = dockerBasePath + "/vfs/dir" containerStoragePath = dockerBasePath + "/containers" runtimePath = "/var/run/docker" diff --git a/components/engine/pkg/plugins/client.go b/components/engine/pkg/plugins/client.go index 00ca105cd2..d531fa46fb 100644 --- a/components/engine/pkg/plugins/client.go +++ b/components/engine/pkg/plugins/client.go @@ -31,6 +31,10 @@ type Client struct { } func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) error { + return c.callWithRetry(serviceMethod, args, ret, true) +} + +func (c *Client) callWithRetry(serviceMethod string, args interface{}, ret interface{}, retry bool) error { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(args); err != nil { return err @@ -50,12 +54,16 @@ func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) e for { resp, err := c.http.Do(req) if err != nil { + if !retry { + return err + } + timeOff := backoff(retries) - if timeOff+time.Since(start) > defaultTimeOut { + if abort(start, timeOff) { return err } retries++ - logrus.Warn("Unable to connect to plugin: %s, retrying in %ds\n", c.addr, timeOff) + logrus.Warnf("Unable to connect to plugin: %s, retrying in %v", c.addr, timeOff) time.Sleep(timeOff) continue } @@ -73,7 +81,7 @@ func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) e } func backoff(retries int) time.Duration { - b, max := float64(1), float64(defaultTimeOut) + b, max := 1, defaultTimeOut for b < max && retries > 0 { b *= 2 retries-- @@ -81,7 +89,11 @@ func backoff(retries int) time.Duration { if b > max { b = max } - return time.Duration(b) + return time.Duration(b) * time.Second +} + +func abort(start time.Time, timeOff time.Duration) bool { + return timeOff+time.Since(start) > time.Duration(defaultTimeOut)*time.Second } func configureTCPTransport(tr *http.Transport, proto, addr string) { diff --git a/components/engine/pkg/plugins/client_test.go b/components/engine/pkg/plugins/client_test.go index b414ecb5d9..0f7cd34dfa 100644 --- a/components/engine/pkg/plugins/client_test.go +++ b/components/engine/pkg/plugins/client_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "reflect" "testing" + "time" ) var ( @@ -27,7 +28,7 @@ func teardownRemotePluginServer() { func TestFailedConnection(t *testing.T) { c := NewClient("tcp://127.0.0.1:1") - err := c.Call("Service.Method", nil, nil) + err := c.callWithRetry("Service.Method", nil, nil, false) if err == nil { t.Fatal("Unexpected successful connection") } @@ -61,3 +62,44 @@ func TestEchoInputOutput(t *testing.T) { t.Fatalf("Expected %v, was %v\n", m, output) } } + +func TestBackoff(t *testing.T) { + cases := []struct { + retries int + expTimeOff time.Duration + }{ + {0, time.Duration(1)}, + {1, time.Duration(2)}, + {2, time.Duration(4)}, + {4, time.Duration(16)}, + {6, time.Duration(30)}, + {10, time.Duration(30)}, + } + + for _, c := range cases { + s := c.expTimeOff * time.Second + if d := backoff(c.retries); d != s { + t.Fatalf("Retry %v, expected %v, was %v\n", c.retries, s, d) + } + } +} + +func TestAbortRetry(t *testing.T) { + cases := []struct { + timeOff time.Duration + expAbort bool + }{ + {time.Duration(1), false}, + {time.Duration(2), false}, + {time.Duration(10), false}, + {time.Duration(30), true}, + {time.Duration(40), true}, + } + + for _, c := range cases { + s := c.timeOff * time.Second + if a := abort(time.Now(), s); a != c.expAbort { + t.Fatalf("Duration %v, expected %v, was %v\n", c.timeOff, s, a) + } + } +} diff --git a/components/engine/runconfig/config.go b/components/engine/runconfig/config.go index 8778d26125..13d7189569 100644 --- a/components/engine/runconfig/config.go +++ b/components/engine/runconfig/config.go @@ -122,6 +122,7 @@ type Config struct { Cmd *Command Image string // Name of the image as it was passed by the operator (eg. could be symbolic) Volumes map[string]struct{} + VolumeDriver string WorkingDir string Entrypoint *Entrypoint NetworkDisabled bool diff --git a/components/engine/runconfig/parse.go b/components/engine/runconfig/parse.go index f6967ab444..e9f0b51577 100644 --- a/components/engine/runconfig/parse.go +++ b/components/engine/runconfig/parse.go @@ -77,6 +77,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flReadonlyRootfs = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only") flLoggingDriver = cmd.String([]string{"-log-driver"}, "", "Logging driver for container") flCgroupParent = cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") + flVolumeDriver = cmd.String([]string{"-volume-driver"}, "", "Optional volume driver for the container") ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR") @@ -317,6 +318,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe Entrypoint: entrypoint, WorkingDir: *flWorkingDir, Labels: convertKVStringsToMap(labels), + VolumeDriver: *flVolumeDriver, } hostConfig := &HostConfig{ diff --git a/components/engine/utils/tcp.go b/components/engine/utils/tcp.go new file mode 100644 index 0000000000..75980ff69a --- /dev/null +++ b/components/engine/utils/tcp.go @@ -0,0 +1,22 @@ +package utils + +import ( + "net" + "net/http" + "time" +) + +func ConfigureTCPTransport(tr *http.Transport, proto, addr string) { + // Why 32? See https://github.com/docker/docker/pull/8035. + timeout := 32 * time.Second + if proto == "unix" { + // No need for compression in local communications. + tr.DisableCompression = true + tr.Dial = func(_, _ string) (net.Conn, error) { + return net.DialTimeout(proto, addr, timeout) + } + } else { + tr.Proxy = http.ProxyFromEnvironment + tr.Dial = (&net.Dialer{Timeout: timeout}).Dial + } +} diff --git a/components/engine/volume/drivers/adapter.go b/components/engine/volume/drivers/adapter.go new file mode 100644 index 0000000000..e849fb8083 --- /dev/null +++ b/components/engine/volume/drivers/adapter.go @@ -0,0 +1,51 @@ +package volumedrivers + +import "github.com/docker/docker/volume" + +type volumeDriverAdapter struct { + name string + proxy *volumeDriverProxy +} + +func (a *volumeDriverAdapter) Name() string { + return a.name +} + +func (a *volumeDriverAdapter) Create(name string) (volume.Volume, error) { + err := a.proxy.Create(name) + if err != nil { + return nil, err + } + return &volumeAdapter{a.proxy, name, a.name}, nil +} + +func (a *volumeDriverAdapter) Remove(v volume.Volume) error { + return a.proxy.Remove(v.Name()) +} + +type volumeAdapter struct { + proxy *volumeDriverProxy + name string + driverName string +} + +func (a *volumeAdapter) Name() string { + return a.name +} + +func (a *volumeAdapter) DriverName() string { + return a.driverName +} + +func (a *volumeAdapter) Path() string { + m, _ := a.proxy.Path(a.name) + return m +} + +func (a *volumeAdapter) Mount() (string, error) { + return a.proxy.Mount(a.name) +} + +func (a *volumeAdapter) Unmount() error { + return a.proxy.Unmount(a.name) +} diff --git a/components/engine/volume/drivers/api.go b/components/engine/volume/drivers/api.go new file mode 100644 index 0000000000..1b98fa7fc5 --- /dev/null +++ b/components/engine/volume/drivers/api.go @@ -0,0 +1,20 @@ +package volumedrivers + +import "github.com/docker/docker/volume" + +type client interface { + Call(string, interface{}, interface{}) error +} + +func NewVolumeDriver(name string, c client) volume.Driver { + proxy := &volumeDriverProxy{c} + return &volumeDriverAdapter{name, proxy} +} + +type VolumeDriver interface { + Create(name string) (err error) + Remove(name string) (err error) + Path(name string) (mountpoint string, err error) + Mount(name string) (mountpoint string, err error) + Unmount(name string) (err error) +} diff --git a/components/engine/volume/drivers/extpoint.go b/components/engine/volume/drivers/extpoint.go new file mode 100644 index 0000000000..228b337be3 --- /dev/null +++ b/components/engine/volume/drivers/extpoint.go @@ -0,0 +1,61 @@ +package volumedrivers + +import ( + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/plugins" + "github.com/docker/docker/volume" +) + +// currently created by hand. generation tool would generate this like: +// $ extpoint-gen Driver > volume/extpoint.go + +var drivers = &driverExtpoint{extensions: make(map[string]volume.Driver)} + +type driverExtpoint struct { + extensions map[string]volume.Driver + sync.Mutex +} + +func Register(extension volume.Driver, name string) bool { + drivers.Lock() + defer drivers.Unlock() + if name == "" { + return false + } + _, exists := drivers.extensions[name] + if exists { + return false + } + drivers.extensions[name] = extension + return true +} + +func Unregister(name string) bool { + drivers.Lock() + defer drivers.Unlock() + _, exists := drivers.extensions[name] + if !exists { + return false + } + delete(drivers.extensions, name) + return true +} + +func Lookup(name string) volume.Driver { + drivers.Lock() + defer drivers.Unlock() + ext, ok := drivers.extensions[name] + if ok { + return ext + } + pl, err := plugins.Get(name, "VolumeDriver") + if err != nil { + logrus.Errorf("Error: %v", err) + return nil + } + d := NewVolumeDriver(name, pl.Client) + drivers.extensions[name] = d + return d +} diff --git a/components/engine/volume/drivers/proxy.go b/components/engine/volume/drivers/proxy.go new file mode 100644 index 0000000000..1bc1586791 --- /dev/null +++ b/components/engine/volume/drivers/proxy.go @@ -0,0 +1,65 @@ +package volumedrivers + +// currently created by hand. generation tool would generate this like: +// $ rpc-gen volume/drivers/api.go VolumeDriver > volume/drivers/proxy.go + +type volumeDriverRequest struct { + Name string +} + +type volumeDriverResponse struct { + Mountpoint string `json:",ommitempty"` + Err error `json:",ommitempty"` +} + +type volumeDriverProxy struct { + c client +} + +func (pp *volumeDriverProxy) Create(name string) error { + args := volumeDriverRequest{name} + var ret volumeDriverResponse + err := pp.c.Call("VolumeDriver.Create", args, &ret) + if err != nil { + return err + } + return ret.Err +} + +func (pp *volumeDriverProxy) Remove(name string) error { + args := volumeDriverRequest{name} + var ret volumeDriverResponse + err := pp.c.Call("VolumeDriver.Remove", args, &ret) + if err != nil { + return err + } + return ret.Err +} + +func (pp *volumeDriverProxy) Path(name string) (string, error) { + args := volumeDriverRequest{name} + var ret volumeDriverResponse + if err := pp.c.Call("VolumeDriver.Path", args, &ret); err != nil { + return "", err + } + return ret.Mountpoint, ret.Err +} + +func (pp *volumeDriverProxy) Mount(name string) (string, error) { + args := volumeDriverRequest{name} + var ret volumeDriverResponse + if err := pp.c.Call("VolumeDriver.Mount", args, &ret); err != nil { + return "", err + } + return ret.Mountpoint, ret.Err +} + +func (pp *volumeDriverProxy) Unmount(name string) error { + args := volumeDriverRequest{name} + var ret volumeDriverResponse + err := pp.c.Call("VolumeDriver.Unmount", args, &ret) + if err != nil { + return err + } + return ret.Err +} diff --git a/components/engine/volume/local/local.go b/components/engine/volume/local/local.go new file mode 100644 index 0000000000..3082e72bd0 --- /dev/null +++ b/components/engine/volume/local/local.go @@ -0,0 +1,126 @@ +package local + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + + "github.com/docker/docker/volume" +) + +func New(rootDirectory string) (*Root, error) { + if err := os.MkdirAll(rootDirectory, 0700); err != nil { + return nil, err + } + r := &Root{ + path: rootDirectory, + volumes: make(map[string]*Volume), + } + dirs, err := ioutil.ReadDir(rootDirectory) + if err != nil { + return nil, err + } + for _, d := range dirs { + name := filepath.Base(d.Name()) + r.volumes[name] = &Volume{ + driverName: r.Name(), + name: name, + path: filepath.Join(rootDirectory, name), + } + } + return r, nil +} + +type Root struct { + m sync.Mutex + path string + volumes map[string]*Volume +} + +func (r *Root) Name() string { + return "local" +} + +func (r *Root) Create(name string) (volume.Volume, error) { + r.m.Lock() + defer r.m.Unlock() + v, exists := r.volumes[name] + if !exists { + path := filepath.Join(r.path, name) + if err := os.Mkdir(path, 0755); err != nil { + if os.IsExist(err) { + return nil, fmt.Errorf("volume already exists under %s", path) + } + return nil, err + } + v = &Volume{ + driverName: r.Name(), + name: name, + path: path, + } + r.volumes[name] = v + } + v.use() + return v, nil +} + +func (r *Root) Remove(v volume.Volume) error { + r.m.Lock() + defer r.m.Unlock() + lv, ok := v.(*Volume) + if !ok { + return errors.New("unknown volume type") + } + lv.release() + if lv.usedCount == 0 { + delete(r.volumes, lv.name) + return os.RemoveAll(lv.path) + } + return nil +} + +type Volume struct { + m sync.Mutex + usedCount int + // unique name of the volume + name string + // path is the path on the host where the data lives + path string + // driverName is the name of the driver that created the volume. + driverName string +} + +func (v *Volume) Name() string { + return v.name +} + +func (v *Volume) DriverName() string { + return v.driverName +} + +func (v *Volume) Path() string { + return v.path +} + +func (v *Volume) Mount() (string, error) { + return v.path, nil +} + +func (v *Volume) Unmount() error { + return nil +} + +func (v *Volume) use() { + v.m.Lock() + v.usedCount++ + v.m.Unlock() +} + +func (v *Volume) release() { + v.m.Lock() + v.usedCount-- + v.m.Unlock() +} diff --git a/components/engine/volume/volume.go b/components/engine/volume/volume.go new file mode 100644 index 0000000000..6edcae3c21 --- /dev/null +++ b/components/engine/volume/volume.go @@ -0,0 +1,26 @@ +package volume + +const DefaultDriverName = "local" + +type Driver interface { + // Name returns the name of the volume driver. + Name() string + // Create makes a new volume with the given id. + Create(string) (Volume, error) + // Remove deletes the volume. + Remove(Volume) error +} + +type Volume interface { + // Name returns the name of the volume + Name() string + // DriverName returns the name of the driver which owns this volume. + DriverName() string + // Path returns the absolute path to the volume. + Path() string + // Mount mounts the volume and returns the absolute path to + // where it can be consumed. + Mount() (string, error) + // Unmount unmounts the volume when it is no longer in use. + Unmount() error +} diff --git a/components/engine/volumes/repository.go b/components/engine/volumes/repository.go deleted file mode 100644 index 71d6c0ad60..0000000000 --- a/components/engine/volumes/repository.go +++ /dev/null @@ -1,193 +0,0 @@ -package volumes - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sync" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/daemon/graphdriver" - "github.com/docker/docker/pkg/stringid" -) - -type Repository struct { - configPath string - driver graphdriver.Driver - volumes map[string]*Volume - lock sync.Mutex -} - -func NewRepository(configPath string, driver graphdriver.Driver) (*Repository, error) { - abspath, err := filepath.Abs(configPath) - if err != nil { - return nil, err - } - - // Create the config path - if err := os.MkdirAll(abspath, 0700); err != nil && !os.IsExist(err) { - return nil, err - } - - repo := &Repository{ - driver: driver, - configPath: abspath, - volumes: make(map[string]*Volume), - } - - return repo, repo.restore() -} - -func (r *Repository) newVolume(path string, writable bool) (*Volume, error) { - var ( - isBindMount bool - err error - id = stringid.GenerateRandomID() - ) - if path != "" { - isBindMount = true - } - - if path == "" { - path, err = r.createNewVolumePath(id) - if err != nil { - return nil, err - } - } - path = filepath.Clean(path) - - // Ignore the error here since the path may not exist - // Really just want to make sure the path we are using is real(or nonexistent) - if cleanPath, err := filepath.EvalSymlinks(path); err == nil { - path = cleanPath - } - - v := &Volume{ - ID: id, - Path: path, - repository: r, - Writable: writable, - containers: make(map[string]struct{}), - configPath: r.configPath + "/" + id, - IsBindMount: isBindMount, - } - - if err := v.initialize(); err != nil { - return nil, err - } - - r.add(v) - return v, nil -} - -func (r *Repository) restore() error { - dir, err := ioutil.ReadDir(r.configPath) - if err != nil { - return err - } - - for _, v := range dir { - id := v.Name() - vol := &Volume{ - ID: id, - configPath: r.configPath + "/" + id, - containers: make(map[string]struct{}), - } - if err := vol.FromDisk(); err != nil { - if !os.IsNotExist(err) { - logrus.Debugf("Error restoring volume: %v", err) - continue - } - if err := vol.initialize(); err != nil { - logrus.Debugf("%s", err) - continue - } - } - r.add(vol) - } - return nil -} - -func (r *Repository) Get(path string) *Volume { - r.lock.Lock() - vol := r.get(path) - r.lock.Unlock() - return vol -} - -func (r *Repository) get(path string) *Volume { - path, err := filepath.EvalSymlinks(path) - if err != nil { - return nil - } - return r.volumes[filepath.Clean(path)] -} - -func (r *Repository) add(volume *Volume) { - if vol := r.get(volume.Path); vol != nil { - return - } - r.volumes[volume.Path] = volume -} - -func (r *Repository) Delete(path string) error { - r.lock.Lock() - defer r.lock.Unlock() - path, err := filepath.EvalSymlinks(path) - if err != nil { - return err - } - volume := r.get(filepath.Clean(path)) - if volume == nil { - return fmt.Errorf("Volume %s does not exist", path) - } - - containers := volume.Containers() - if len(containers) > 0 { - return fmt.Errorf("Volume %s is being used and cannot be removed: used by containers %s", volume.Path, containers) - } - - if err := os.RemoveAll(volume.configPath); err != nil { - return err - } - - if !volume.IsBindMount { - if err := r.driver.Remove(volume.ID); err != nil { - if !os.IsNotExist(err) { - return err - } - } - } - - delete(r.volumes, volume.Path) - return nil -} - -func (r *Repository) createNewVolumePath(id string) (string, error) { - if err := r.driver.Create(id, ""); err != nil { - return "", err - } - - path, err := r.driver.Get(id, "") - if err != nil { - return "", fmt.Errorf("Driver %s failed to get volume rootfs %s: %v", r.driver, id, err) - } - - return path, nil -} - -func (r *Repository) FindOrCreateVolume(path string, writable bool) (*Volume, error) { - r.lock.Lock() - defer r.lock.Unlock() - - if path == "" { - return r.newVolume(path, writable) - } - - if v := r.get(path); v != nil { - return v, nil - } - - return r.newVolume(path, writable) -} diff --git a/components/engine/volumes/repository_test.go b/components/engine/volumes/repository_test.go deleted file mode 100644 index 801c225f75..0000000000 --- a/components/engine/volumes/repository_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package volumes - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/docker/docker/daemon/graphdriver" - _ "github.com/docker/docker/daemon/graphdriver/vfs" -) - -func TestRepositoryFindOrCreate(t *testing.T) { - root, err := ioutil.TempDir(os.TempDir(), "volumes") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(root) - repo, err := newRepo(root) - if err != nil { - t.Fatal(err) - } - - // no path - v, err := repo.FindOrCreateVolume("", true) - if err != nil { - t.Fatal(err) - } - - // FIXME: volumes are heavily dependent on the vfs driver, but this should not be so! - expected := filepath.Join(root, "repo-graph", "vfs", "dir", v.ID) - if v.Path != expected { - t.Fatalf("expected new path to be created in %s, got %s", expected, v.Path) - } - - // with a non-existant path - dir := filepath.Join(root, "doesntexist") - v, err = repo.FindOrCreateVolume(dir, true) - if err != nil { - t.Fatal(err) - } - - if v.Path != dir { - t.Fatalf("expected new path to be created in %s, got %s", dir, v.Path) - } - - if _, err := os.Stat(v.Path); err != nil { - t.Fatal(err) - } - - // with a pre-existing path - // can just use the same path from above since it now exists - v, err = repo.FindOrCreateVolume(dir, true) - if err != nil { - t.Fatal(err) - } - if v.Path != dir { - t.Fatalf("expected new path to be created in %s, got %s", dir, v.Path) - } - -} - -func TestRepositoryGet(t *testing.T) { - root, err := ioutil.TempDir(os.TempDir(), "volumes") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(root) - repo, err := newRepo(root) - if err != nil { - t.Fatal(err) - } - - v, err := repo.FindOrCreateVolume("", true) - if err != nil { - t.Fatal(err) - } - - v2 := repo.Get(v.Path) - if v2 == nil { - t.Fatalf("expected to find volume but didn't") - } - if v2 != v { - t.Fatalf("expected get to return same volume") - } -} - -func TestRepositoryDelete(t *testing.T) { - root, err := ioutil.TempDir(os.TempDir(), "volumes") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(root) - repo, err := newRepo(root) - if err != nil { - t.Fatal(err) - } - - // with a normal volume - v, err := repo.FindOrCreateVolume("", true) - if err != nil { - t.Fatal(err) - } - - if err := repo.Delete(v.Path); err != nil { - t.Fatal(err) - } - - if v := repo.Get(v.Path); v != nil { - t.Fatalf("expected volume to not exist") - } - - if _, err := os.Stat(v.Path); err == nil { - t.Fatalf("expected volume files to be removed") - } - - // with a bind mount - dir := filepath.Join(root, "test") - v, err = repo.FindOrCreateVolume(dir, true) - if err != nil { - t.Fatal(err) - } - - if err := repo.Delete(v.Path); err != nil { - t.Fatal(err) - } - - if v := repo.Get(v.Path); v != nil { - t.Fatalf("expected volume to not exist") - } - - if _, err := os.Stat(v.Path); err != nil && os.IsNotExist(err) { - t.Fatalf("expected bind volume data to persist after destroying volume") - } - - // with container refs - dir = filepath.Join(root, "test") - v, err = repo.FindOrCreateVolume(dir, true) - if err != nil { - t.Fatal(err) - } - v.AddContainer("1234") - - if err := repo.Delete(v.Path); err == nil { - t.Fatalf("expected volume delete to fail due to container refs") - } - - v.RemoveContainer("1234") - if err := repo.Delete(v.Path); err != nil { - t.Fatal(err) - } - -} - -func newRepo(root string) (*Repository, error) { - configPath := filepath.Join(root, "repo-config") - graphDir := filepath.Join(root, "repo-graph") - - driver, err := graphdriver.GetDriver("vfs", graphDir, []string{}) - if err != nil { - return nil, err - } - return NewRepository(configPath, driver) -} diff --git a/components/engine/volumes/volume.go b/components/engine/volumes/volume.go deleted file mode 100644 index 5b3b646018..0000000000 --- a/components/engine/volumes/volume.go +++ /dev/null @@ -1,152 +0,0 @@ -package volumes - -import ( - "encoding/json" - "os" - "path/filepath" - "sync" - - "github.com/docker/docker/pkg/symlink" -) - -type Volume struct { - ID string - Path string - IsBindMount bool - Writable bool - containers map[string]struct{} - configPath string - repository *Repository - lock sync.Mutex -} - -func (v *Volume) IsDir() (bool, error) { - stat, err := os.Stat(v.Path) - if err != nil { - return false, err - } - - return stat.IsDir(), nil -} - -func (v *Volume) Containers() []string { - v.lock.Lock() - - var containers []string - for c := range v.containers { - containers = append(containers, c) - } - - v.lock.Unlock() - return containers -} - -func (v *Volume) RemoveContainer(containerId string) { - v.lock.Lock() - delete(v.containers, containerId) - v.lock.Unlock() -} - -func (v *Volume) AddContainer(containerId string) { - v.lock.Lock() - v.containers[containerId] = struct{}{} - v.lock.Unlock() -} - -func (v *Volume) initialize() error { - v.lock.Lock() - defer v.lock.Unlock() - - if _, err := os.Stat(v.Path); err != nil { - if !os.IsNotExist(err) { - return err - } - if err := os.MkdirAll(v.Path, 0755); err != nil { - return err - } - } - - if err := os.MkdirAll(v.configPath, 0755); err != nil { - return err - } - - return v.toDisk() -} - -func (v *Volume) ToDisk() error { - v.lock.Lock() - defer v.lock.Unlock() - return v.toDisk() -} - -func (v *Volume) toDisk() error { - jsonPath, err := v.jsonPath() - if err != nil { - return err - } - f, err := os.OpenFile(jsonPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return err - } - if err := json.NewEncoder(f).Encode(v); err != nil { - f.Close() - return err - } - return f.Close() -} - -func (v *Volume) FromDisk() error { - v.lock.Lock() - defer v.lock.Unlock() - pth, err := v.jsonPath() - if err != nil { - return err - } - - jsonSource, err := os.Open(pth) - if err != nil { - return err - } - defer jsonSource.Close() - - dec := json.NewDecoder(jsonSource) - - return dec.Decode(v) -} - -func (v *Volume) jsonPath() (string, error) { - return v.GetRootResourcePath("config.json") -} - -// Evalutes `path` in the scope of the volume's root path, with proper path -// sanitisation. Symlinks are all scoped to the root of the volume, as -// though the volume's root was `/`. -// -// The volume's root path is the host-facing path of the root of the volume's -// mountpoint inside a container. -// -// NOTE: The returned path is *only* safely scoped inside the volume's root -// if no component of the returned path changes (such as a component -// symlinking to a different path) between using this method and using the -// path. See symlink.FollowSymlinkInScope for more details. -func (v *Volume) GetResourcePath(path string) (string, error) { - cleanPath := filepath.Join("/", path) - return symlink.FollowSymlinkInScope(filepath.Join(v.Path, cleanPath), v.Path) -} - -// Evalutes `path` in the scope of the volume's config path, with proper path -// sanitisation. Symlinks are all scoped to the root of the config path, as -// though the config path was `/`. -// -// The config path of a volume is not exposed to the container and is just used -// to store volume configuration options and other internal information. If in -// doubt, you probably want to just use v.GetResourcePath. -// -// NOTE: The returned path is *only* safely scoped inside the volume's config -// path if no component of the returned path changes (such as a component -// symlinking to a different path) between using this method and using the -// path. See symlink.FollowSymlinkInScope for more details. -func (v *Volume) GetRootResourcePath(path string) (string, error) { - cleanPath := filepath.Join("/", path) - return symlink.FollowSymlinkInScope(filepath.Join(v.configPath, cleanPath), v.configPath) -} diff --git a/components/engine/volumes/volume_test.go b/components/engine/volumes/volume_test.go deleted file mode 100644 index b30549d379..0000000000 --- a/components/engine/volumes/volume_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package volumes - -import ( - "os" - "testing" - - "github.com/docker/docker/pkg/stringutils" -) - -func TestContainers(t *testing.T) { - v := &Volume{containers: make(map[string]struct{})} - id := "1234" - - v.AddContainer(id) - - if v.Containers()[0] != id { - t.Fatalf("adding a container ref failed") - } - - v.RemoveContainer(id) - if len(v.Containers()) != 0 { - t.Fatalf("removing container failed") - } -} - -// os.Stat(v.Path) is returning ErrNotExist, initialize catch it and try to -// mkdir v.Path but it dies and correctly returns the error -func TestInitializeCannotMkdirOnNonExistentPath(t *testing.T) { - v := &Volume{Path: "nonexistentpath"} - - err := v.initialize() - if err == nil { - t.Fatal("Expected not to initialize volume with a non existent path") - } - - if !os.IsNotExist(err) { - t.Fatalf("Expected to get ErrNotExist error, got %s", err) - } -} - -// os.Stat(v.Path) is NOT returning ErrNotExist so skip and return error from -// initialize -func TestInitializeCannotStatPathFileNameTooLong(t *testing.T) { - // ENAMETOOLONG - v := &Volume{Path: stringutils.GenerateRandomAlphaOnlyString(300)} - - err := v.initialize() - if err == nil { - t.Fatal("Expected not to initialize volume with a non existent path") - } - - if os.IsNotExist(err) { - t.Fatal("Expected to not get ErrNotExist") - } -} From 3dacf0ed879f038a5c64cbe4645d475908437743 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Tue, 19 May 2015 18:37:01 -0700 Subject: [PATCH 2/9] Disable -v overloading Signed-off-by: Arnaud Porterie (cherry picked from commit ec5a362fb93358244305067419589f602fd33807) Upstream-commit: 7da5a7eb6637222a313e9d6e7fbcd5dd472154e3 Component: engine --- components/engine/daemon/volumes.go | 26 ++------------- components/engine/daemon/volumes_unit_test.go | 33 ++----------------- 2 files changed, 5 insertions(+), 54 deletions(-) diff --git a/components/engine/daemon/volumes.go b/components/engine/daemon/volumes.go index 8ca985500b..c6220249b5 100644 --- a/components/engine/daemon/volumes.go +++ b/components/engine/daemon/volumes.go @@ -15,8 +15,6 @@ import ( volumedrivers "github.com/docker/docker/volume/drivers" ) -var localMountErr = fmt.Errorf("Invalid driver: %s driver doesn't support named volumes", volume.DefaultDriverName) - type mountPoint struct { Name string Destination string @@ -74,34 +72,14 @@ func parseBindMount(spec string, config *runconfig.Config) (*mountPoint, error) } if !filepath.IsAbs(arr[0]) { - bind.Driver, bind.Name = parseNamedVolumeInfo(arr[0], config) - if bind.Driver == volume.DefaultDriverName { - return nil, localMountErr - } - } else { - bind.Source = filepath.Clean(arr[0]) + return nil, fmt.Errorf("cannot bind mount volume: %s volume paths must be absolute.", spec) } + bind.Source = filepath.Clean(arr[0]) bind.Destination = filepath.Clean(bind.Destination) return bind, nil } -func parseNamedVolumeInfo(info string, config *runconfig.Config) (driver string, name string) { - p := strings.SplitN(info, "/", 2) - switch len(p) { - case 2: - driver = p[0] - name = p[1] - default: - if driver = config.VolumeDriver; len(driver) == 0 { - driver = volume.DefaultDriverName - } - name = p[0] - } - - return -} - func parseVolumesFrom(spec string) (string, string, error) { if len(spec) == 0 { return "", "", fmt.Errorf("malformed volumes-from specification: %s", spec) diff --git a/components/engine/daemon/volumes_unit_test.go b/components/engine/daemon/volumes_unit_test.go index ac17b292fd..dba8b9d844 100644 --- a/components/engine/daemon/volumes_unit_test.go +++ b/components/engine/daemon/volumes_unit_test.go @@ -8,33 +8,6 @@ import ( volumedrivers "github.com/docker/docker/volume/drivers" ) -func TestParseNamedVolumeInfo(t *testing.T) { - cases := []struct { - driver string - name string - expDriver string - expName string - }{ - {"", "name", "local", "name"}, - {"external", "name", "external", "name"}, - {"", "external/name", "external", "name"}, - {"ignored", "external/name", "external", "name"}, - } - - for _, c := range cases { - conf := &runconfig.Config{VolumeDriver: c.driver} - driver, name := parseNamedVolumeInfo(c.name, conf) - - if driver != c.expDriver { - t.Fatalf("Expected %s, was %s\n", c.expDriver, driver) - } - - if name != c.expName { - t.Fatalf("Expected %s, was %s\n", c.expName, name) - } - } -} - func TestParseBindMount(t *testing.T) { cases := []struct { bind string @@ -51,9 +24,9 @@ func TestParseBindMount(t *testing.T) { {"/tmp:/tmp:rw", "", "/tmp", "/tmp", "", "", true, false}, {"/tmp:/tmp:foo", "", "/tmp", "/tmp", "", "", false, true}, {"name:/tmp", "", "", "", "", "", false, true}, - {"name:/tmp", "external", "/tmp", "", "name", "external", true, false}, - {"external/name:/tmp:rw", "", "/tmp", "", "name", "external", true, false}, - {"external/name:/tmp:ro", "", "/tmp", "", "name", "external", false, false}, + {"name:/tmp", "external", "/tmp", "", "name", "external", true, true}, + {"external/name:/tmp:rw", "", "/tmp", "", "name", "external", true, true}, + {"external/name:/tmp:ro", "", "/tmp", "", "name", "external", false, true}, {"external/name:/tmp:foo", "", "/tmp", "", "name", "external", false, true}, {"name:/tmp", "local", "", "", "", "", false, true}, {"local/name:/tmp:rw", "", "", "", "", "", true, true}, From e89556546bf38acd90d0674f1a214ba5115e28e9 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Tue, 19 May 2015 18:37:44 -0700 Subject: [PATCH 3/9] Mark --volume-driver as experimental Signed-off-by: Arnaud Porterie Upstream-commit: 4fc37a1ede58153bfa6aa5c9aa0f2903f18d572f Component: engine --- .../reference/api/docker_remote_api_v1.19.md | 7 ++++--- ...ocker_cli_start_volume_driver_unix_test.go | 13 +------------ components/engine/runconfig/parse.go | 6 ++++-- .../engine/runconfig/parse_experimental.go | 19 +++++++++++++++++++ components/engine/runconfig/parse_stub.go | 14 ++++++++++++++ 5 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 components/engine/runconfig/parse_experimental.go create mode 100644 components/engine/runconfig/parse_stub.go diff --git a/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md b/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md index 5175ed9782..c252f6211a 100644 --- a/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md +++ b/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md @@ -142,6 +142,7 @@ Create a container "ExposedPorts": { "22/tcp": {} }, + VolumeDriver: "local", "HostConfig": { "Binds": ["/tmp:/tmp"], "Links": ["redis3:redis"], @@ -222,15 +223,15 @@ Json Parameters: container - **ExposedPorts** - An object mapping ports to an empty object in the form of: `"ExposedPorts": { "/: {}" }` +- **VolumeDriver** - A string value containing the volume driver to use, `local` by default. - **HostConfig** - **Binds** – A list of volume bindings for this container. Each volume binding is a string of the form `container_path` (to create a new volume for the container), `host_path:container_path` (to bind-mount a host path into the container), `host_path:container_path:ro` (to make the bind-mount read-only inside the container), or - `volume_plugin/volume_name:container_path` (to provision a - volume named `volume_name` from a [volume plugin](/userguide/plugins) - named `volume_plugin`). + `volume_name:container_path` (to provision a volume named `volume_name` + from a [volume plugin](/userguide/plugins)). - **Links** - A list of links for the container. Each link entry should be in the form of `container_name:alias`. - **LxcConf** - LXC specific configurations. These configurations will only diff --git a/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go b/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go index 4d6d44a537..5b4223dd8a 100644 --- a/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go +++ b/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go @@ -1,3 +1,4 @@ +// +build experimental // +build !windows package main @@ -136,15 +137,3 @@ func (s *ExternalVolumeSuite) TestStartExternalVolumeDriver(c *check.C) { c.Fatalf("External volume mount failed. Output: %s\n", out) } } - -func (s *ExternalVolumeSuite) TestStartExternalVolumeNamedDriver(c *check.C) { - runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "-v", "test-external-volume-driver/volume-1:/tmp/external-volume-test", "busybox:latest", "cat", "/tmp/external-volume-test/test") - out, stderr, exitCode, err := runCommandWithStdoutStderr(runCmd) - if err != nil && exitCode != 0 { - c.Fatal(out, stderr, err) - } - - if !strings.Contains(out, s.server.URL) { - c.Fatalf("External volume mount failed. Output: %s\n", out) - } -} diff --git a/components/engine/runconfig/parse.go b/components/engine/runconfig/parse.go index e9f0b51577..46ec267851 100644 --- a/components/engine/runconfig/parse.go +++ b/components/engine/runconfig/parse.go @@ -77,7 +77,6 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flReadonlyRootfs = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only") flLoggingDriver = cmd.String([]string{"-log-driver"}, "", "Logging driver for container") flCgroupParent = cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") - flVolumeDriver = cmd.String([]string{"-volume-driver"}, "", "Optional volume driver for the container") ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR") @@ -101,6 +100,8 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options") cmd.Var(&flLoggingOpts, []string{"-log-opt"}, "Log driver options") + expFlags := attachExperimentalFlags(cmd) + cmd.Require(flag.Min, 1) if err := cmd.ParseFlags(args, true); err != nil { @@ -318,7 +319,6 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe Entrypoint: entrypoint, WorkingDir: *flWorkingDir, Labels: convertKVStringsToMap(labels), - VolumeDriver: *flVolumeDriver, } hostConfig := &HostConfig{ @@ -357,6 +357,8 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe CgroupParent: *flCgroupParent, } + applyExperimentalFlags(expFlags, config, hostConfig) + // When allocating stdin in attached mode, close stdin at client disconnect if config.OpenStdin && config.AttachStdin { config.StdinOnce = true diff --git a/components/engine/runconfig/parse_experimental.go b/components/engine/runconfig/parse_experimental.go new file mode 100644 index 0000000000..886b377fa8 --- /dev/null +++ b/components/engine/runconfig/parse_experimental.go @@ -0,0 +1,19 @@ +// +build experimental + +package runconfig + +import flag "github.com/docker/docker/pkg/mflag" + +type experimentalFlags struct { + flags map[string]interface{} +} + +func attachExperimentalFlags(cmd *flag.FlagSet) *experimentalFlags { + flags := make(map[string]interface{}) + flags["volume-driver"] = cmd.String([]string{"-volume-driver"}, "", "Optional volume driver for the container") + return &experimentalFlags{flags: flags} +} + +func applyExperimentalFlags(exp *experimentalFlags, config *Config, hostConfig *HostConfig) { + config.VolumeDriver = *(exp.flags["volume-driver"]).(*string) +} diff --git a/components/engine/runconfig/parse_stub.go b/components/engine/runconfig/parse_stub.go new file mode 100644 index 0000000000..391b6ed43b --- /dev/null +++ b/components/engine/runconfig/parse_stub.go @@ -0,0 +1,14 @@ +// +build !experimental + +package runconfig + +import flag "github.com/docker/docker/pkg/mflag" + +type experimentalFlags struct{} + +func attachExperimentalFlags(cmd *flag.FlagSet) *experimentalFlags { + return nil +} + +func applyExperimentalFlags(flags *experimentalFlags, config *Config, hostConfig *HostConfig) { +} From 778241cd3a9a5cd1724bb94a3f4c1bd3bff596f4 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Wed, 20 May 2015 14:53:53 -0700 Subject: [PATCH 4/9] Make API volume-driver dependent on 'experimental' Signed-off-by: Arnaud Porterie Upstream-commit: 2653c7c16cf87029c7e2e35427fbd1c83a0c96ba Component: engine --- components/engine/daemon/container.go | 12 +++---- components/engine/daemon/create.go | 4 +-- components/engine/daemon/daemon.go | 4 +-- components/engine/daemon/delete.go | 4 +-- components/engine/daemon/volumes.go | 14 +------- .../engine/daemon/volumes_experimental.go | 15 ++++++++ .../daemon/volumes_experimental_unit_test.go | 32 +++++++++++++++++ components/engine/daemon/volumes_stubs.go | 12 +++++++ .../engine/daemon/volumes_stubs_unit_test.go | 35 +++++++++++++++++++ components/engine/daemon/volumes_unit_test.go | 24 ------------- components/engine/volume/drivers/extpoint.go | 8 ++--- 11 files changed, 111 insertions(+), 53 deletions(-) create mode 100644 components/engine/daemon/volumes_experimental.go create mode 100644 components/engine/daemon/volumes_experimental_unit_test.go create mode 100644 components/engine/daemon/volumes_stubs.go create mode 100644 components/engine/daemon/volumes_stubs_unit_test.go diff --git a/components/engine/daemon/container.go b/components/engine/daemon/container.go index 23a63a52db..468dc23044 100644 --- a/components/engine/daemon/container.go +++ b/components/engine/daemon/container.go @@ -1039,7 +1039,7 @@ func (container *Container) networkMounts() []execdriver.Mount { return mounts } -func (container *Container) AddLocalMountPoint(name, destination string, rw bool) { +func (container *Container) addLocalMountPoint(name, destination string, rw bool) { container.MountPoints[destination] = &mountPoint{ Name: name, Driver: volume.DefaultDriverName, @@ -1048,7 +1048,7 @@ func (container *Container) AddLocalMountPoint(name, destination string, rw bool } } -func (container *Container) AddMountPointWithVolume(destination string, vol volume.Volume, rw bool) { +func (container *Container) addMountPointWithVolume(destination string, vol volume.Volume, rw bool) { container.MountPoints[destination] = &mountPoint{ Name: vol.Name(), Driver: vol.DriverName(), @@ -1058,11 +1058,11 @@ func (container *Container) AddMountPointWithVolume(destination string, vol volu } } -func (container *Container) IsDestinationMounted(destination string) bool { +func (container *Container) isDestinationMounted(destination string) bool { return container.MountPoints[destination] != nil } -func (container *Container) PrepareMountPoints() error { +func (container *Container) prepareMountPoints() error { for _, config := range container.MountPoints { if len(config.Driver) > 0 { v, err := createVolume(config.Name, config.Driver) @@ -1075,7 +1075,7 @@ func (container *Container) PrepareMountPoints() error { return nil } -func (container *Container) RemoveMountPoints() error { +func (container *Container) removeMountPoints() error { for _, m := range container.MountPoints { if m.Volume != nil { if err := removeVolume(m.Volume); err != nil { @@ -1086,7 +1086,7 @@ func (container *Container) RemoveMountPoints() error { return nil } -func (container *Container) ShouldRestart() bool { +func (container *Container) shouldRestart() bool { return container.hostConfig.RestartPolicy.Name == "always" || (container.hostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0) } diff --git a/components/engine/daemon/create.go b/components/engine/daemon/create.go index bf55febe02..0020226dd3 100644 --- a/components/engine/daemon/create.go +++ b/components/engine/daemon/create.go @@ -113,7 +113,7 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos } // Skip volumes for which we already have something mounted on that // destination because of a --volume-from. - if container.IsDestinationMounted(destination) { + if container.isDestinationMounted(destination) { continue } path, err := container.GetResourcePath(destination) @@ -136,7 +136,7 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos } copyExistingContents(rootfs, path) - container.AddMountPointWithVolume(destination, v, true) + container.addMountPointWithVolume(destination, v, true) } if err := container.ToDisk(); err != nil { return nil, nil, err diff --git a/components/engine/daemon/daemon.go b/components/engine/daemon/daemon.go index 269e689b99..22bbf8d7cd 100644 --- a/components/engine/daemon/daemon.go +++ b/components/engine/daemon/daemon.go @@ -216,7 +216,7 @@ func (daemon *Daemon) register(container *Container, updateSuffixarray bool) err return err } - if err := container.PrepareMountPoints(); err != nil { + if err := container.prepareMountPoints(); err != nil { return err } @@ -333,7 +333,7 @@ func (daemon *Daemon) restore() error { // check the restart policy on the containers and restart any container with // the restart policy of "always" - if daemon.config.AutoRestart && container.ShouldRestart() { + if daemon.config.AutoRestart && container.shouldRestart() { logrus.Debugf("Starting container %s", container.ID) if err := container.Start(); err != nil { diff --git a/components/engine/daemon/delete.go b/components/engine/daemon/delete.go index 20451bd9de..02e6c9814e 100644 --- a/components/engine/daemon/delete.go +++ b/components/engine/daemon/delete.go @@ -71,7 +71,7 @@ func (daemon *Daemon) ContainerRm(name string, config *ContainerRmConfig) error } container.LogEvent("destroy") if config.RemoveVolume { - container.RemoveMountPoints() + container.removeMountPoints() } } return nil @@ -154,5 +154,5 @@ func (daemon *Daemon) commonRm(container *Container, forceRemove bool) (err erro } func (daemon *Daemon) DeleteVolumes(c *Container) error { - return c.RemoveMountPoints() + return c.removeMountPoints() } diff --git a/components/engine/daemon/volumes.go b/components/engine/daemon/volumes.go index c6220249b5..bc96b345ff 100644 --- a/components/engine/daemon/volumes.go +++ b/components/engine/daemon/volumes.go @@ -12,7 +12,6 @@ import ( "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" - volumedrivers "github.com/docker/docker/volume/drivers" ) type mountPoint struct { @@ -246,7 +245,7 @@ func (daemon *Daemon) verifyOldVolumesInfo(container *Container) error { if strings.HasPrefix(hostPath, vfsPath) { id := filepath.Base(hostPath) - container.AddLocalMountPoint(id, destination, vols.VolumesRW[destination]) + container.addLocalMountPoint(id, destination, vols.VolumesRW[destination]) } } @@ -268,14 +267,3 @@ func removeVolume(v volume.Volume) error { } return vd.Remove(v) } - -func getVolumeDriver(name string) (volume.Driver, error) { - if name == "" { - name = volume.DefaultDriverName - } - vd := volumedrivers.Lookup(name) - if vd == nil { - return nil, fmt.Errorf("Volumes Driver %s isn't registered", name) - } - return vd, nil -} diff --git a/components/engine/daemon/volumes_experimental.go b/components/engine/daemon/volumes_experimental.go new file mode 100644 index 0000000000..a4f5a9cbe8 --- /dev/null +++ b/components/engine/daemon/volumes_experimental.go @@ -0,0 +1,15 @@ +// +build experimental + +package daemon + +import ( + "github.com/docker/docker/volume" + "github.com/docker/docker/volume/drivers" +) + +func getVolumeDriver(name string) (volume.Driver, error) { + if name == "" { + name = volume.DefaultDriverName + } + return volumedrivers.Lookup(name) +} diff --git a/components/engine/daemon/volumes_experimental_unit_test.go b/components/engine/daemon/volumes_experimental_unit_test.go new file mode 100644 index 0000000000..89ef5e872d --- /dev/null +++ b/components/engine/daemon/volumes_experimental_unit_test.go @@ -0,0 +1,32 @@ +// +build experimental + +package daemon + +import ( + "testing" + + "github.com/docker/docker/volume" + "github.com/docker/docker/volume/drivers" +) + +type fakeDriver struct{} + +func (fakeDriver) Name() string { return "fake" } +func (fakeDriver) Create(name string) (volume.Volume, error) { return nil, nil } +func (fakeDriver) Remove(v volume.Volume) error { return nil } + +func TestGetVolumeDriver(t *testing.T) { + _, err := getVolumeDriver("missing") + if err == nil { + t.Fatal("Expected error, was nil") + } + + volumedrivers.Register(fakeDriver{}, "fake") + d, err := getVolumeDriver("fake") + if err != nil { + t.Fatal(err) + } + if d.Name() != "fake" { + t.Fatalf("Expected fake driver, got %s\n", d.Name()) + } +} diff --git a/components/engine/daemon/volumes_stubs.go b/components/engine/daemon/volumes_stubs.go new file mode 100644 index 0000000000..41f8b67343 --- /dev/null +++ b/components/engine/daemon/volumes_stubs.go @@ -0,0 +1,12 @@ +// +build !experimental + +package daemon + +import ( + "github.com/docker/docker/volume" + "github.com/docker/docker/volume/drivers" +) + +func getVolumeDriver(_ string) (volume.Driver, error) { + return volumedrivers.Lookup(volume.DefaultDriverName) +} diff --git a/components/engine/daemon/volumes_stubs_unit_test.go b/components/engine/daemon/volumes_stubs_unit_test.go new file mode 100644 index 0000000000..655f5f4b79 --- /dev/null +++ b/components/engine/daemon/volumes_stubs_unit_test.go @@ -0,0 +1,35 @@ +// +build !experimental + +package daemon + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/docker/docker/volume" + "github.com/docker/docker/volume/drivers" + "github.com/docker/docker/volume/local" +) + +func TestGetVolumeDefaultDriver(t *testing.T) { + tmp, err := ioutil.TempDir("", "volume-test-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + l, err := local.New(tmp) + if err != nil { + t.Fatal(err) + } + volumedrivers.Register(l, volume.DefaultDriverName) + d, err := getVolumeDriver("missing") + if err != nil { + t.Fatal(err) + } + + if d.Name() != volume.DefaultDriverName { + t.Fatalf("Expected local driver, was %s\n", d.Name) + } +} diff --git a/components/engine/daemon/volumes_unit_test.go b/components/engine/daemon/volumes_unit_test.go index dba8b9d844..6578aa2043 100644 --- a/components/engine/daemon/volumes_unit_test.go +++ b/components/engine/daemon/volumes_unit_test.go @@ -4,8 +4,6 @@ import ( "testing" "github.com/docker/docker/runconfig" - "github.com/docker/docker/volume" - volumedrivers "github.com/docker/docker/volume/drivers" ) func TestParseBindMount(t *testing.T) { @@ -95,25 +93,3 @@ func TestParseVolumeFrom(t *testing.T) { } } } - -type fakeDriver struct{} - -func (fakeDriver) Name() string { return "fake" } -func (fakeDriver) Create(name string) (volume.Volume, error) { return nil, nil } -func (fakeDriver) Remove(v volume.Volume) error { return nil } - -func TestGetVolumeDriver(t *testing.T) { - _, err := getVolumeDriver("missing") - if err == nil { - t.Fatal("Expected error, was nil") - } - - volumedrivers.Register(fakeDriver{}, "fake") - d, err := getVolumeDriver("fake") - if err != nil { - t.Fatal(err) - } - if d.Name() != "fake" { - t.Fatalf("Expected fake driver, got %s\n", d.Name()) - } -} diff --git a/components/engine/volume/drivers/extpoint.go b/components/engine/volume/drivers/extpoint.go index 228b337be3..0e90843517 100644 --- a/components/engine/volume/drivers/extpoint.go +++ b/components/engine/volume/drivers/extpoint.go @@ -43,19 +43,19 @@ func Unregister(name string) bool { return true } -func Lookup(name string) volume.Driver { +func Lookup(name string) (volume.Driver, error) { drivers.Lock() defer drivers.Unlock() ext, ok := drivers.extensions[name] if ok { - return ext + return ext, nil } pl, err := plugins.Get(name, "VolumeDriver") if err != nil { logrus.Errorf("Error: %v", err) - return nil + return nil, err } d := NewVolumeDriver(name, pl.Client) drivers.extensions[name] = d - return d + return d, nil } From 39e76a661b4b6ca126d0ced9f2197b645a6dd49b Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 21 May 2015 10:57:59 -0700 Subject: [PATCH 5/9] Allow named volumes for external drivers. Signed-off-by: David Calavera Upstream-commit: 3bf0ca31cf61e445554326ba405cc32c7ef64189 Component: engine --- components/engine/daemon/volumes.go | 20 ++++-- .../engine/daemon/volumes_experimental.go | 11 ++++ .../daemon/volumes_experimental_unit_test.go | 54 ++++++++++++++++ components/engine/daemon/volumes_stubs.go | 12 ++++ .../engine/daemon/volumes_stubs_unit_test.go | 46 ++++++++++++++ components/engine/daemon/volumes_unit_test.go | 62 +------------------ 6 files changed, 140 insertions(+), 65 deletions(-) diff --git a/components/engine/daemon/volumes.go b/components/engine/daemon/volumes.go index bc96b345ff..2fdcd8311d 100644 --- a/components/engine/daemon/volumes.go +++ b/components/engine/daemon/volumes.go @@ -70,11 +70,21 @@ func parseBindMount(spec string, config *runconfig.Config) (*mountPoint, error) return nil, fmt.Errorf("Invalid volume specification: %s", spec) } - if !filepath.IsAbs(arr[0]) { - return nil, fmt.Errorf("cannot bind mount volume: %s volume paths must be absolute.", spec) + name, source, err := parseVolumeSource(arr[0], config) + if err != nil { + return nil, err } - bind.Source = filepath.Clean(arr[0]) + if len(source) == 0 { + bind.Driver = config.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 } @@ -245,7 +255,8 @@ func (daemon *Daemon) verifyOldVolumesInfo(container *Container) error { if strings.HasPrefix(hostPath, vfsPath) { id := filepath.Base(hostPath) - container.addLocalMountPoint(id, destination, vols.VolumesRW[destination]) + rw := vols.VolumesRW != nil && vols.VolumesRW[destination] + container.addLocalMountPoint(id, destination, rw) } } @@ -254,6 +265,7 @@ func (daemon *Daemon) verifyOldVolumesInfo(container *Container) error { func createVolume(name, driverName string) (volume.Volume, error) { vd, err := getVolumeDriver(driverName) + if err != nil { return nil, err } diff --git a/components/engine/daemon/volumes_experimental.go b/components/engine/daemon/volumes_experimental.go index a4f5a9cbe8..c39b7907b0 100644 --- a/components/engine/daemon/volumes_experimental.go +++ b/components/engine/daemon/volumes_experimental.go @@ -3,6 +3,9 @@ package daemon import ( + "path/filepath" + + "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" "github.com/docker/docker/volume/drivers" ) @@ -13,3 +16,11 @@ func getVolumeDriver(name string) (volume.Driver, error) { } return volumedrivers.Lookup(name) } + +func parseVolumeSource(spec string, config *runconfig.Config) (string, string, error) { + if !filepath.IsAbs(spec) { + return spec, "", nil + } + + return "", spec, nil +} diff --git a/components/engine/daemon/volumes_experimental_unit_test.go b/components/engine/daemon/volumes_experimental_unit_test.go index 89ef5e872d..1201f5154e 100644 --- a/components/engine/daemon/volumes_experimental_unit_test.go +++ b/components/engine/daemon/volumes_experimental_unit_test.go @@ -5,6 +5,7 @@ package daemon import ( "testing" + "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" "github.com/docker/docker/volume/drivers" ) @@ -30,3 +31,56 @@ func TestGetVolumeDriver(t *testing.T) { t.Fatalf("Expected fake driver, got %s\n", d.Name()) } } + +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}, + } + + for _, c := range cases { + conf := &runconfig.Config{VolumeDriver: c.driver} + m, err := parseBindMount(c.bind, conf) + if c.fail { + if err == nil { + t.Fatalf("Expected error, was nil, for spec %s\n", c.bind) + } + continue + } + + if m.Destination != c.expDest { + t.Fatalf("Expected destination %s, was %s, for spec %s\n", c.expDest, m.Destination, c.bind) + } + + if m.Source != c.expSource { + t.Fatalf("Expected source %s, was %s, for spec %s\n", c.expSource, m.Source, c.bind) + } + + if m.Name != c.expName { + t.Fatalf("Expected name %s, was %s for spec %s\n", c.expName, m.Name, c.bind) + } + + if m.Driver != c.expDriver { + t.Fatalf("Expected driver %s, was %s, for spec %s\n", c.expDriver, m.Driver, c.bind) + } + + if m.RW != c.expRW { + t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind) + } + } +} diff --git a/components/engine/daemon/volumes_stubs.go b/components/engine/daemon/volumes_stubs.go index 41f8b67343..1d2d873d8e 100644 --- a/components/engine/daemon/volumes_stubs.go +++ b/components/engine/daemon/volumes_stubs.go @@ -3,6 +3,10 @@ package daemon import ( + "fmt" + "path/filepath" + + "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" "github.com/docker/docker/volume/drivers" ) @@ -10,3 +14,11 @@ import ( func getVolumeDriver(_ string) (volume.Driver, error) { return volumedrivers.Lookup(volume.DefaultDriverName) } + +func parseVolumeSource(spec string, _ *runconfig.Config) (string, string, error) { + if !filepath.IsAbs(spec) { + return "", "", fmt.Errorf("cannot bind mount volume: %s volume paths must be absolute.", spec) + } + + return "", spec, nil +} diff --git a/components/engine/daemon/volumes_stubs_unit_test.go b/components/engine/daemon/volumes_stubs_unit_test.go index 655f5f4b79..a3cafe6550 100644 --- a/components/engine/daemon/volumes_stubs_unit_test.go +++ b/components/engine/daemon/volumes_stubs_unit_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" "github.com/docker/docker/volume/drivers" "github.com/docker/docker/volume/local" @@ -33,3 +34,48 @@ func TestGetVolumeDefaultDriver(t *testing.T) { t.Fatalf("Expected local driver, was %s\n", d.Name) } } + +func TestParseBindMount(t *testing.T) { + cases := []struct { + bind string + expDest string + expSource string + expName 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", "", "", "", false, true}, + {"local/name:/tmp:rw", "", "", "", true, true}, + } + + for _, c := range cases { + conf := &runconfig.Config{} + m, err := parseBindMount(c.bind, conf) + 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.RW != c.expRW { + t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind) + } + } +} diff --git a/components/engine/daemon/volumes_unit_test.go b/components/engine/daemon/volumes_unit_test.go index 6578aa2043..b1e7f72f89 100644 --- a/components/engine/daemon/volumes_unit_test.go +++ b/components/engine/daemon/volumes_unit_test.go @@ -1,66 +1,6 @@ package daemon -import ( - "testing" - - "github.com/docker/docker/runconfig" -) - -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", "", "", "", "", "", false, true}, - {"name:/tmp", "external", "/tmp", "", "name", "external", true, true}, - {"external/name:/tmp:rw", "", "/tmp", "", "name", "external", true, true}, - {"external/name:/tmp:ro", "", "/tmp", "", "name", "external", false, true}, - {"external/name:/tmp:foo", "", "/tmp", "", "name", "external", false, true}, - {"name:/tmp", "local", "", "", "", "", false, true}, - {"local/name:/tmp:rw", "", "", "", "", "", true, true}, - } - - for _, c := range cases { - conf := &runconfig.Config{VolumeDriver: c.driver} - m, err := parseBindMount(c.bind, conf) - 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) - } - } -} +import "testing" func TestParseVolumeFrom(t *testing.T) { cases := []struct { From 801e8947c6ed00a77ab2c4a5e05befb31396519c Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 21 May 2015 14:46:54 -0700 Subject: [PATCH 6/9] Mark volume drivers and plugins as experimental in the documentation. Signed-off-by: David Calavera Upstream-commit: a867c1b70a0ee3db2793ab1a8f56d24beaa41388 Component: engine --- .../reference/api/docker_remote_api.md | 6 ++-- .../reference/api/docker_remote_api_v1.19.md | 4 ++- .../docs/sources/reference/commandline/cli.md | 31 ++++++++++--------- .../docs/sources/userguide/dockervolumes.md | 2 +- .../engine/docs/sources/userguide/index.md | 2 +- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/components/engine/docs/sources/reference/api/docker_remote_api.md b/components/engine/docs/sources/reference/api/docker_remote_api.md index 79983ed580..595eeb325d 100644 --- a/components/engine/docs/sources/reference/api/docker_remote_api.md +++ b/components/engine/docs/sources/reference/api/docker_remote_api.md @@ -73,11 +73,11 @@ are now returned as boolean instead of as an int. In addition, the end point now returns the new boolean fields `CpuCfsPeriod`, `CpuCfsQuota`, and `OomKillDisable`. -**New!** +**New in the Experimental channel!** You can now specify a volume plugin in `/v1.19/containers/create`, for example -`"HostConfig": {"Binds": ["flocker/name:/data"]}` where `flocker` is the name -of the plugin, `name` is the user-facing name of the volume (passed to the +`"VolumeDriver": "flocker", "HostConfig": {"Binds": ["vol-1:/data"]}` where `flocker` is the name +of the plugin, `vol-1` is the user-facing name of the volume (passed to the volume plugin) and `/data` is the mountpoint inside the container. ## v1.18 diff --git a/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md b/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md index c252f6211a..2ea4b42998 100644 --- a/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md +++ b/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md @@ -15,6 +15,8 @@ page_keywords: API, Docker, rcli, REST, documentation `STDIN` and `STDERR`. - When the client API version is newer than the daemon's an HTTP `400 Bad Request` error message is returned. + - Options marked as *Experimental* are only available using Docker + from the Experimental channel. # 2. Endpoints @@ -223,7 +225,7 @@ Json Parameters: container - **ExposedPorts** - An object mapping ports to an empty object in the form of: `"ExposedPorts": { "/: {}" }` -- **VolumeDriver** - A string value containing the volume driver to use, `local` by default. +- **VolumeDriver** - *(Experimental)* A string value containing the volume driver to use, `local` by default. - **HostConfig** - **Binds** – A list of volume bindings for this container. Each volume binding is a string of the form `container_path` (to create a new diff --git a/components/engine/docs/sources/reference/commandline/cli.md b/components/engine/docs/sources/reference/commandline/cli.md index e1c9f56ad3..b5171b1b34 100644 --- a/components/engine/docs/sources/reference/commandline/cli.md +++ b/components/engine/docs/sources/reference/commandline/cli.md @@ -1001,7 +1001,7 @@ Creates a new container. -t, --tty=false Allocate a pseudo-TTY -u, --user="" Username or UID -v, --volume=[] Bind mount a volume, or specify name for volume plugin - --volume-driver= Optional volume driver (plugin name) for the container + --volume-driver= Optional volume driver (plugin name) for the container (Experimental) --volumes-from=[] Mount volumes from the specified container(s) -w, --workdir="" Working directory inside the container @@ -1972,7 +1972,7 @@ To remove an image using its digest: -t, --tty=false Allocate a pseudo-TTY -u, --user="" Username or UID (format: [:]) -v, --volume=[] Bind mount a volume, or specify name for volume plugin - --volume-driver= Optional volume driver (plugin name) for the container + --volume-driver= Optional volume driver (plugin name) for the container (Experimental) --volumes-from=[] Mount volumes from the specified container(s) -w, --workdir="" Working directory inside the container @@ -2068,18 +2068,6 @@ binary (such as that provided by [https://get.docker.com]( https://get.docker.com)), you give the container the full access to create and manipulate the host's Docker daemon. - $ docker run -ti -v volumename:/data --volume-driver=flocker busybox sh - -By specifying a volume name in conjunction with a volume driver, volume plugins -such as [Flocker](https://clusterhq.com/docker-plugin/), once installed, can be -used to manage volumes external to a single host, such as those on EBS. In this -example, "volumename" is passed through to the volume plugin as a user-given -name for the volume which allows the plugin to associate it with an external -volume beyond the lifetime of a single container or container host. This can be -used, for example, to move a stateful container from one server to another. - -The `volumename` must not begin with a `/`. - $ docker run -p 127.0.0.1:80:8080 ubuntu bash This binds port `8080` of the container to port `80` on `127.0.0.1` of @@ -2285,6 +2273,21 @@ application change: `--rm` option means that when the container exits, the container's layer is removed. + +*Experimental*: + + $ docker run -ti -v volumename:/data --volume-driver=flocker busybox sh + +By specifying a volume name in conjunction with a volume driver, volume plugins +such as [Flocker](https://clusterhq.com/docker-plugin/), once installed, can be +used to manage volumes external to a single host, such as those on EBS. In this +example, "volumename" is passed through to the volume plugin as a user-given +name for the volume which allows the plugin to associate it with an external +volume beyond the lifetime of a single container or container host. This can be +used, for example, to move a stateful container from one server to another. + +The `volumename` must not begin with a `/`. + #### Restart policies Use Docker's `--restart` to specify a container's *restart policy*. A restart diff --git a/components/engine/docs/sources/userguide/dockervolumes.md b/components/engine/docs/sources/userguide/dockervolumes.md index c993d61814..567d174716 100644 --- a/components/engine/docs/sources/userguide/dockervolumes.md +++ b/components/engine/docs/sources/userguide/dockervolumes.md @@ -210,7 +210,7 @@ Then un-tar the backup file in the new container's data volume. You can use the techniques above to automate backup, migration and restore testing using your preferred tools. -## Integrating Docker with external storage systems +## (Experimental) Integrating Docker with external storage systems Docker volume plugins such as [Flocker](https://clusterhq.com/docker-plugin/) enable Docker deployments to be integrated with external storage systems, such diff --git a/components/engine/docs/sources/userguide/index.md b/components/engine/docs/sources/userguide/index.md index f82e8c70fc..8395757919 100644 --- a/components/engine/docs/sources/userguide/index.md +++ b/components/engine/docs/sources/userguide/index.md @@ -105,7 +105,7 @@ works with Docker can now transparently scale up to multiple hosts. Go to [Docker Swarm user guide](/swarm/). -## Docker Plugins +## (Experimental) Docker Plugins Docker plugins allow you to extend the capabilities of the Docker Engine. From fd9e9a6b5be5ee422ce6f01bbf3029b004359edb Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 22 May 2015 10:37:00 -0700 Subject: [PATCH 7/9] Propagate unmount events to the external volume drivers. Signed-off-by: David Calavera Upstream-commit: d592778f4a75d36745aaffaf73c0775ecd420545 Component: engine --- components/engine/daemon/container.go | 59 +++++-- components/engine/daemon/create.go | 14 +- components/engine/daemon/delete.go | 1 + .../integration-cli/docker_cli_run_test.go | 8 - ...ocker_cli_start_volume_driver_unix_test.go | 144 +++++++++++++++--- components/engine/volume/drivers/adapter.go | 13 +- components/engine/volume/drivers/extpoint.go | 6 +- components/engine/volume/drivers/proxy.go | 29 ++-- 8 files changed, 217 insertions(+), 57 deletions(-) diff --git a/components/engine/daemon/container.go b/components/engine/daemon/container.go index 468dc23044..1f65386fa9 100644 --- a/components/engine/daemon/container.go +++ b/components/engine/daemon/container.go @@ -346,6 +346,8 @@ func (container *Container) cleanup() { for _, eConfig := range container.execCommands.s { container.daemon.unregisterExecCommand(eConfig) } + + container.UnmountVolumes(true) } func (container *Container) KillSig(sig int) error { @@ -469,6 +471,7 @@ func (container *Container) Stop(seconds int) error { return err } } + return nil } @@ -564,16 +567,10 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { if err := container.Mount(); err != nil { return nil, err } - var paths []string - unmount := func() { - for _, p := range paths { - syscall.Unmount(p, 0) - } - } defer func() { if err != nil { // unmount any volumes - unmount() + container.UnmountVolumes(true) // unmount the container's rootfs container.Unmount() } @@ -587,7 +584,6 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { if err != nil { return nil, err } - paths = append(paths, dest) if err := mount.Mount(m.Source, dest, "bind", "rbind,ro"); err != nil { return nil, err } @@ -618,7 +614,7 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { } return ioutils.NewReadCloserWrapper(archive, func() error { err := archive.Close() - unmount() + container.UnmountVolumes(true) container.Unmount() return err }), @@ -1090,3 +1086,48 @@ func (container *Container) shouldRestart() bool { return container.hostConfig.RestartPolicy.Name == "always" || (container.hostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0) } + +func (container *Container) UnmountVolumes(forceSyscall bool) error { + for _, m := range container.MountPoints { + dest, err := container.GetResourcePath(m.Destination) + if err != nil { + return err + } + + if forceSyscall { + syscall.Unmount(dest, 0) + } + + if m.Volume != nil { + if err := m.Volume.Unmount(); err != nil { + return err + } + } + } + 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 + } + return err + } + + path, err := v.Mount() + if err != nil { + return err + } + + if err := copyExistingContents(rootfs, path); err != nil { + return err + } + + return v.Unmount() +} diff --git a/components/engine/daemon/create.go b/components/engine/daemon/create.go index 0020226dd3..5d67ed55f1 100644 --- a/components/engine/daemon/create.go +++ b/components/engine/daemon/create.go @@ -10,7 +10,6 @@ import ( "github.com/docker/docker/image" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/runconfig" "github.com/docker/libcontainer/label" ) @@ -120,21 +119,20 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos if err != nil { return nil, nil, err } - if stat, err := os.Stat(path); err == nil && !stat.IsDir() { + + stat, err := os.Stat(path) + if err == nil && !stat.IsDir() { return nil, nil, fmt.Errorf("cannot mount volume over existing file, file exists %s", path) } + v, err := createVolume(name, config.VolumeDriver) if err != nil { return nil, nil, err } - rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs) - if err != nil { + + if err := container.copyImagePathContent(v, destination); err != nil { return nil, nil, err } - if path, err = v.Mount(); err != nil { - return nil, nil, err - } - copyExistingContents(rootfs, path) container.addMountPointWithVolume(destination, v, true) } diff --git a/components/engine/daemon/delete.go b/components/engine/daemon/delete.go index 02e6c9814e..b323f1b1db 100644 --- a/components/engine/daemon/delete.go +++ b/components/engine/daemon/delete.go @@ -70,6 +70,7 @@ func (daemon *Daemon) ContainerRm(name string, config *ContainerRmConfig) error } } container.LogEvent("destroy") + if config.RemoveVolume { container.removeMountPoints() } diff --git a/components/engine/integration-cli/docker_cli_run_test.go b/components/engine/integration-cli/docker_cli_run_test.go index 7e60675045..821ea4a995 100644 --- a/components/engine/integration-cli/docker_cli_run_test.go +++ b/components/engine/integration-cli/docker_cli_run_test.go @@ -440,14 +440,6 @@ func (s *DockerSuite) TestRunCreateVolumesInSymlinkDir(c *check.C) { } } -// Regression test for #4830 -func (s *DockerSuite) TestRunWithRelativePath(c *check.C) { - runCmd := exec.Command(dockerBinary, "run", "-v", "tmp:/other-tmp", "busybox", "true") - if _, _, _, err := runCommandWithStdoutStderr(runCmd); err == nil { - c.Fatalf("relative path should result in an error") - } -} - func (s *DockerSuite) TestRunVolumesMountedAsReadonly(c *check.C) { cmd := exec.Command(dockerBinary, "run", "-v", "/test:/test:ro", "busybox", "touch", "/test/somefile") if code, err := runCommand(cmd); err == nil || code == 0 { diff --git a/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go b/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go index 5b4223dd8a..1cc90801d6 100644 --- a/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go +++ b/components/engine/integration-cli/docker_cli_start_volume_driver_unix_test.go @@ -10,7 +10,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "strings" @@ -18,25 +17,40 @@ import ( ) func init() { - check.Suite(&ExternalVolumeSuite{ + check.Suite(&DockerExternalVolumeSuite{ ds: &DockerSuite{}, }) } -type ExternalVolumeSuite struct { +type eventCounter struct { + activations int + creations int + removals int + mounts int + unmounts int + paths int +} + +type DockerExternalVolumeSuite struct { server *httptest.Server ds *DockerSuite + d *Daemon + ec *eventCounter } -func (s *ExternalVolumeSuite) SetUpTest(c *check.C) { +func (s *DockerExternalVolumeSuite) SetUpTest(c *check.C) { + s.d = NewDaemon(c) s.ds.SetUpTest(c) + s.ec = &eventCounter{} + } -func (s *ExternalVolumeSuite) TearDownTest(c *check.C) { +func (s *DockerExternalVolumeSuite) TearDownTest(c *check.C) { + s.d.Stop() s.ds.TearDownTest(c) } -func (s *ExternalVolumeSuite) SetUpSuite(c *check.C) { +func (s *DockerExternalVolumeSuite) SetUpSuite(c *check.C) { mux := http.NewServeMux() s.server = httptest.NewServer(mux) @@ -44,26 +58,30 @@ func (s *ExternalVolumeSuite) SetUpSuite(c *check.C) { name string } - hostVolumePath := func(name string) string { - return fmt.Sprintf("/var/lib/docker/volumes/%s", name) - } - mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + s.ec.activations++ + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") fmt.Fprintln(w, `{"Implements": ["VolumeDriver"]}`) }) mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) { + s.ec.creations++ + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") fmt.Fprintln(w, `{}`) }) mux.HandleFunc("/VolumeDriver.Remove", func(w http.ResponseWriter, r *http.Request) { + s.ec.removals++ + w.Header().Set("Content-Type", "appplication/vnd.docker.plugins.v1+json") fmt.Fprintln(w, `{}`) }) mux.HandleFunc("/VolumeDriver.Path", func(w http.ResponseWriter, r *http.Request) { + s.ec.paths++ + var pr pluginRequest if err := json.NewDecoder(r.Body).Decode(&pr); err != nil { http.Error(w, err.Error(), 500) @@ -76,6 +94,8 @@ func (s *ExternalVolumeSuite) SetUpSuite(c *check.C) { }) mux.HandleFunc("/VolumeDriver.Mount", func(w http.ResponseWriter, r *http.Request) { + s.ec.mounts++ + var pr pluginRequest if err := json.NewDecoder(r.Body).Decode(&pr); err != nil { http.Error(w, err.Error(), 500) @@ -94,7 +114,9 @@ func (s *ExternalVolumeSuite) SetUpSuite(c *check.C) { fmt.Fprintln(w, fmt.Sprintf("{\"Mountpoint\": \"%s\"}", p)) }) - mux.HandleFunc("/VolumeDriver.Umount", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/VolumeDriver.Unmount", func(w http.ResponseWriter, r *http.Request) { + s.ec.unmounts++ + var pr pluginRequest if err := json.NewDecoder(r.Body).Decode(&pr); err != nil { http.Error(w, err.Error(), 500) @@ -118,7 +140,7 @@ func (s *ExternalVolumeSuite) SetUpSuite(c *check.C) { } } -func (s *ExternalVolumeSuite) TearDownSuite(c *check.C) { +func (s *DockerExternalVolumeSuite) TearDownSuite(c *check.C) { s.server.Close() if err := os.RemoveAll("/usr/share/docker/plugins"); err != nil { @@ -126,14 +148,102 @@ func (s *ExternalVolumeSuite) TearDownSuite(c *check.C) { } } -func (s *ExternalVolumeSuite) TestStartExternalVolumeDriver(c *check.C) { - runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", "test-external-volume-driver", "busybox:latest", "cat", "/tmp/external-volume-test/test") - out, stderr, exitCode, err := runCommandWithStdoutStderr(runCmd) - if err != nil && exitCode != 0 { - c.Fatal(out, stderr, err) +func (s *DockerExternalVolumeSuite) TestStartExternalNamedVolumeDriver(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + out, err := s.d.Cmd("run", "--rm", "--name", "test-data", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", "test-external-volume-driver", "busybox:latest", "cat", "/tmp/external-volume-test/test") + if err != nil { + c.Fatal(err) } if !strings.Contains(out, s.server.URL) { c.Fatalf("External volume mount failed. Output: %s\n", out) } + + p := hostVolumePath("external-volume-test") + _, err = os.Lstat(p) + if err == nil { + c.Fatalf("Expected error checking volume path in host: %s\n", p) + } + + if !os.IsNotExist(err) { + c.Fatalf("Expected volume path in host to not exist: %s, %v\n", p, err) + } + + c.Assert(s.ec.activations, check.Equals, 1) + c.Assert(s.ec.creations, check.Equals, 1) + c.Assert(s.ec.removals, check.Equals, 1) + c.Assert(s.ec.mounts, check.Equals, 1) + c.Assert(s.ec.unmounts, check.Equals, 1) +} + +func (s *DockerExternalVolumeSuite) TestStartExternalVolumeUnnamedDriver(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + out, err := s.d.Cmd("run", "--rm", "--name", "test-data", "-v", "/tmp/external-volume-test", "--volume-driver", "test-external-volume-driver", "busybox:latest", "cat", "/tmp/external-volume-test/test") + if err != nil { + c.Fatal(err) + } + + if !strings.Contains(out, s.server.URL) { + c.Fatalf("External volume mount failed. Output: %s\n", out) + } + + c.Assert(s.ec.activations, check.Equals, 1) + c.Assert(s.ec.creations, check.Equals, 1) + c.Assert(s.ec.removals, check.Equals, 1) + c.Assert(s.ec.mounts, check.Equals, 1) + c.Assert(s.ec.unmounts, check.Equals, 1) +} + +func (s DockerExternalVolumeSuite) TestStartExternalVolumeDriverVolumesFrom(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + if _, err := s.d.Cmd("run", "-d", "--name", "vol-test1", "-v", "/foo", "--volume-driver", "test-external-volume-driver", "busybox:latest"); err != nil { + c.Fatal(err) + } + + if _, err := s.d.Cmd("run", "--rm", "--volumes-from", "vol-test1", "--name", "vol-test2", "busybox", "ls", "/tmp"); err != nil { + c.Fatal(err) + } + + if _, err := s.d.Cmd("rm", "-f", "vol-test1"); err != nil { + c.Fatal(err) + } + + c.Assert(s.ec.activations, check.Equals, 1) + c.Assert(s.ec.creations, check.Equals, 2) + c.Assert(s.ec.removals, check.Equals, 1) + c.Assert(s.ec.mounts, check.Equals, 2) + c.Assert(s.ec.unmounts, check.Equals, 2) +} + +func (s DockerExternalVolumeSuite) TestStartExternalVolumeDriverDeleteContainer(c *check.C) { + if err := s.d.StartWithBusybox(); err != nil { + c.Fatal(err) + } + + if _, err := s.d.Cmd("run", "-d", "--name", "vol-test1", "-v", "/foo", "--volume-driver", "test-external-volume-driver", "busybox:latest"); err != nil { + c.Fatal(err) + } + + if _, err := s.d.Cmd("rm", "-fv", "vol-test1"); err != nil { + c.Fatal(err) + } + + c.Assert(s.ec.activations, check.Equals, 1) + c.Assert(s.ec.creations, check.Equals, 1) + c.Assert(s.ec.removals, check.Equals, 1) + c.Assert(s.ec.mounts, check.Equals, 1) + c.Assert(s.ec.unmounts, check.Equals, 1) +} + +func hostVolumePath(name string) string { + return fmt.Sprintf("/var/lib/docker/volumes/%s", name) } diff --git a/components/engine/volume/drivers/adapter.go b/components/engine/volume/drivers/adapter.go index e849fb8083..6846a3a8e8 100644 --- a/components/engine/volume/drivers/adapter.go +++ b/components/engine/volume/drivers/adapter.go @@ -16,7 +16,10 @@ func (a *volumeDriverAdapter) Create(name string) (volume.Volume, error) { if err != nil { return nil, err } - return &volumeAdapter{a.proxy, name, a.name}, nil + return &volumeAdapter{ + proxy: a.proxy, + name: name, + driverName: a.name}, nil } func (a *volumeDriverAdapter) Remove(v volume.Volume) error { @@ -27,6 +30,7 @@ type volumeAdapter struct { proxy *volumeDriverProxy name string driverName string + eMount string // ephemeral host volume path } func (a *volumeAdapter) Name() string { @@ -38,12 +42,17 @@ func (a *volumeAdapter) DriverName() string { } func (a *volumeAdapter) Path() string { + if len(a.eMount) > 0 { + return a.eMount + } m, _ := a.proxy.Path(a.name) return m } func (a *volumeAdapter) Mount() (string, error) { - return a.proxy.Mount(a.name) + var err error + a.eMount, err = a.proxy.Mount(a.name) + return a.eMount, err } func (a *volumeAdapter) Unmount() error { diff --git a/components/engine/volume/drivers/extpoint.go b/components/engine/volume/drivers/extpoint.go index 0e90843517..b002a0ffda 100644 --- a/components/engine/volume/drivers/extpoint.go +++ b/components/engine/volume/drivers/extpoint.go @@ -1,9 +1,9 @@ package volumedrivers import ( + "fmt" "sync" - "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/plugins" "github.com/docker/docker/volume" ) @@ -52,9 +52,9 @@ func Lookup(name string) (volume.Driver, error) { } pl, err := plugins.Get(name, "VolumeDriver") if err != nil { - logrus.Errorf("Error: %v", err) - return nil, err + return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err) } + d := NewVolumeDriver(name, pl.Client) drivers.extensions[name] = d return d, nil diff --git a/components/engine/volume/drivers/proxy.go b/components/engine/volume/drivers/proxy.go index 1bc1586791..545e49007a 100644 --- a/components/engine/volume/drivers/proxy.go +++ b/components/engine/volume/drivers/proxy.go @@ -1,5 +1,7 @@ package volumedrivers +import "fmt" + // currently created by hand. generation tool would generate this like: // $ rpc-gen volume/drivers/api.go VolumeDriver > volume/drivers/proxy.go @@ -21,9 +23,9 @@ func (pp *volumeDriverProxy) Create(name string) error { var ret volumeDriverResponse err := pp.c.Call("VolumeDriver.Create", args, &ret) if err != nil { - return err + return pp.fmtError(name, err) } - return ret.Err + return pp.fmtError(name, ret.Err) } func (pp *volumeDriverProxy) Remove(name string) error { @@ -31,27 +33,27 @@ func (pp *volumeDriverProxy) Remove(name string) error { var ret volumeDriverResponse err := pp.c.Call("VolumeDriver.Remove", args, &ret) if err != nil { - return err + return pp.fmtError(name, err) } - return ret.Err + return pp.fmtError(name, ret.Err) } func (pp *volumeDriverProxy) Path(name string) (string, error) { args := volumeDriverRequest{name} var ret volumeDriverResponse if err := pp.c.Call("VolumeDriver.Path", args, &ret); err != nil { - return "", err + return "", pp.fmtError(name, err) } - return ret.Mountpoint, ret.Err + return ret.Mountpoint, pp.fmtError(name, ret.Err) } func (pp *volumeDriverProxy) Mount(name string) (string, error) { args := volumeDriverRequest{name} var ret volumeDriverResponse if err := pp.c.Call("VolumeDriver.Mount", args, &ret); err != nil { - return "", err + return "", pp.fmtError(name, err) } - return ret.Mountpoint, ret.Err + return ret.Mountpoint, pp.fmtError(name, ret.Err) } func (pp *volumeDriverProxy) Unmount(name string) error { @@ -59,7 +61,14 @@ func (pp *volumeDriverProxy) Unmount(name string) error { var ret volumeDriverResponse err := pp.c.Call("VolumeDriver.Unmount", args, &ret) if err != nil { - return err + return pp.fmtError(name, err) } - return ret.Err + return pp.fmtError(name, ret.Err) +} + +func (pp *volumeDriverProxy) fmtError(name string, err error) error { + if err == nil { + return nil + } + return fmt.Errorf("External volume driver request failed for %s: %v", name, err) } From f23ada12c49e1869b8b18c942e094edfdef71f9d Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 22 May 2015 16:57:05 -0700 Subject: [PATCH 8/9] Move Documentation changes out or this PR. The will come in a follow up PR inside the experimental section. Signed-off-by: David Calavera Upstream-commit: ab8a5bcf04243577516207257c7fe5ec8257a9f4 Component: engine --- components/engine/docs/mkdocs.yml | 2 - .../docs/sources/reference/api/README.md | 1 - .../reference/api/docker_remote_api.md | 7 - .../reference/api/docker_remote_api_v1.19.md | 10 +- .../docs/sources/reference/api/plugin_api.md | 223 ------------------ .../docs/sources/reference/commandline/cli.md | 21 +- .../docs/sources/userguide/dockervolumes.md | 8 - .../engine/docs/sources/userguide/index.md | 6 - .../engine/docs/sources/userguide/plugins.md | 51 ---- 9 files changed, 4 insertions(+), 325 deletions(-) delete mode 100644 components/engine/docs/sources/reference/api/plugin_api.md delete mode 100644 components/engine/docs/sources/userguide/plugins.md diff --git a/components/engine/docs/mkdocs.yml b/components/engine/docs/mkdocs.yml index d357983005..b501c9c398 100644 --- a/components/engine/docs/mkdocs.yml +++ b/components/engine/docs/mkdocs.yml @@ -73,7 +73,6 @@ pages: - ['machine/index.md', 'User Guide', 'Docker Machine' ] - ['swarm/index.md', 'User Guide', 'Docker Swarm' ] - ['kitematic/userguide.md', 'User Guide', 'Kitematic'] -- ['userguide/plugins.md', 'User Guide', 'Docker Plugins'] # Docker Hub docs: - ['docker-hub/index.md', 'Docker Hub', 'Docker Hub' ] @@ -186,7 +185,6 @@ pages: - ['reference/api/docker_remote_api_v1.0.md', '**HIDDEN**'] - ['reference/api/remote_api_client_libraries.md', 'Reference', 'Docker Remote API client libraries'] - ['reference/api/docker_io_accounts_api.md', 'Reference', 'Docker Hub accounts API'] -- ['reference/api/plugin_api.md', 'Reference', 'Docker Plugin API'] - ['kitematic/faq.md', 'Reference', 'Kitematic: FAQ'] - ['kitematic/known-issues.md', 'Reference', 'Kitematic: Known issues'] diff --git a/components/engine/docs/sources/reference/api/README.md b/components/engine/docs/sources/reference/api/README.md index 4f72fbfe33..ec1cbcb2c3 100644 --- a/components/engine/docs/sources/reference/api/README.md +++ b/components/engine/docs/sources/reference/api/README.md @@ -7,4 +7,3 @@ This directory holds the authoritative specifications of APIs defined and implem index for images to download * The docker.io OAuth and accounts API which 3rd party services can use to access account information - * The plugin API for Docker Plugins diff --git a/components/engine/docs/sources/reference/api/docker_remote_api.md b/components/engine/docs/sources/reference/api/docker_remote_api.md index 595eeb325d..4c77cab8cf 100644 --- a/components/engine/docs/sources/reference/api/docker_remote_api.md +++ b/components/engine/docs/sources/reference/api/docker_remote_api.md @@ -73,13 +73,6 @@ are now returned as boolean instead of as an int. In addition, the end point now returns the new boolean fields `CpuCfsPeriod`, `CpuCfsQuota`, and `OomKillDisable`. -**New in the Experimental channel!** - -You can now specify a volume plugin in `/v1.19/containers/create`, for example -`"VolumeDriver": "flocker", "HostConfig": {"Binds": ["vol-1:/data"]}` where `flocker` is the name -of the plugin, `vol-1` is the user-facing name of the volume (passed to the -volume plugin) and `/data` is the mountpoint inside the container. - ## v1.18 ### Full documentation diff --git a/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md b/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md index 2ea4b42998..dde8ee79a1 100644 --- a/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md +++ b/components/engine/docs/sources/reference/api/docker_remote_api_v1.19.md @@ -15,8 +15,6 @@ page_keywords: API, Docker, rcli, REST, documentation `STDIN` and `STDERR`. - When the client API version is newer than the daemon's an HTTP `400 Bad Request` error message is returned. - - Options marked as *Experimental* are only available using Docker - from the Experimental channel. # 2. Endpoints @@ -144,7 +142,6 @@ Create a container "ExposedPorts": { "22/tcp": {} }, - VolumeDriver: "local", "HostConfig": { "Binds": ["/tmp:/tmp"], "Links": ["redis3:redis"], @@ -225,15 +222,12 @@ Json Parameters: container - **ExposedPorts** - An object mapping ports to an empty object in the form of: `"ExposedPorts": { "/: {}" }` -- **VolumeDriver** - *(Experimental)* A string value containing the volume driver to use, `local` by default. - **HostConfig** - **Binds** – A list of volume bindings for this container. Each volume binding is a string of the form `container_path` (to create a new volume for the container), `host_path:container_path` (to bind-mount - a host path into the container), `host_path:container_path:ro` - (to make the bind-mount read-only inside the container), or - `volume_name:container_path` (to provision a volume named `volume_name` - from a [volume plugin](/userguide/plugins)). + a host path into the container), or `host_path:container_path:ro` + (to make the bind-mount read-only inside the container). - **Links** - A list of links for the container. Each link entry should be in the form of `container_name:alias`. - **LxcConf** - LXC specific configurations. These configurations will only diff --git a/components/engine/docs/sources/reference/api/plugin_api.md b/components/engine/docs/sources/reference/api/plugin_api.md deleted file mode 100644 index 770f606b4c..0000000000 --- a/components/engine/docs/sources/reference/api/plugin_api.md +++ /dev/null @@ -1,223 +0,0 @@ -page_title: Plugin API documentation -page_description: Documentation for writing a Docker plugin. -page_keywords: docker, plugins, api, extensions - -# Docker Plugin API - -Docker plugins are out-of-process extensions which add capabilities to the -Docker Engine. - -This page is intended for people who want to develop their own Docker plugin. -If you just want to learn about or use Docker plugins, look -[here](/userguide/plugins). - -## What plugins are - -A plugin is a process running on the same docker host as the docker daemon, -which registers itself by placing a file in `/usr/share/docker/plugins` (the -"plugin directory"). - -Plugins have human-readable names, which are short, lowercase strings. For -example, `flocker` or `weave`. - -Plugins can run inside or outside containers. Currently running them outside -containers is recommended. - -## Plugin discovery - -Docker discovers plugins by looking for them in the plugin directory whenever a -user or container tries to use one by name. - -There are two types of files which can be put in the plugin directory. - -* `.sock` files are UNIX domain sockets. -* `.spec` files are text files containing a URL, such as `unix:///other.sock`. - -The name of the file (excluding the extension) determines the plugin name. - -For example, the `flocker` plugin might create a UNIX socket at -`/usr/share/docker/plugins/flocker.sock`. - -Plugins must be run locally on the same machine as the Docker daemon. UNIX -domain sockets are strongly encouraged for security reasons. - -## Plugin lifecycle - -Plugins should be started before Docker, and stopped after Docker. For -example, when packaging a plugin for a platform which supports `systemd`, you -might use [`systemd` dependencies]( -http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before=) to -manage startup and shutdown order. - -When upgrading a plugin, you should first stop the Docker daemon, upgrade the -plugin, then start Docker again. - -If a plugin is packaged as a container, this may cause issues. Plugins as -containers are currently considered experimental due to these shutdown/startup -ordering issues. These issues are mitigated by plugin retries (see below). - -## Plugin activation - -When a plugin is first referred to -- either by a user referring to it by name -(e.g. `docker run --volume-driver=foo`) or a container already configured to -use a plugin being started -- Docker looks for the named plugin in the plugin -directory and activates it with a handshake. See Handshake API below. - -Plugins are *not* activated automatically at Docker daemon startup. Rather, -they are activated only lazily, or on-demand, when they are needed. - -## API design - -The Plugin API is RPC-style JSON over HTTP, much like webhooks. - -Requests flow *from* the Docker daemon *to* the plugin. So the plugin needs to -implement an HTTP server and bind this to the UNIX socket mentioned in the -"plugin discovery" section. - -All requests are HTTP `POST` requests. - -The API is versioned via an Accept header, which currently is always set to -`application/vnd.docker.plugins.v1+json`. - -## Handshake API - -Plugins are activated via the following "handshake" API call. - -### /Plugin.Activate - -**Request:** empty body - -**Response:** -``` -{ - "Implements": ["VolumeDriver"] -} -``` - -Responds with a list of Docker subsystems which this plugin implements. -After activation, the plugin will then be sent events from this subsystem. - -## Volume API - -If a plugin registers itself as a `VolumeDriver` (see above) then it is -expected to provide writeable paths on the host filesystem for the Docker -daemon to provide to containers to consume. - -The Docker daemon handles bind-mounting the provided paths into user -containers. - -### /VolumeDriver.Create - -**Request**: -``` -{ - "Name": "volume_name" -} -``` - -Instruct the plugin that the user wants to create a volume, given a user -specified volume name. The plugin does not need to actually manifest the -volume on the filesystem yet (until Mount is called). - -**Response**: -``` -{ - "Err": null -} -``` - -Respond with a string error if an error occurred. - -### /VolumeDriver.Remove - -**Request**: -``` -{ - "Name": "volume_name" -} -``` - -Create a volume, given a user specified volume name. - -**Response**: -``` -{ - "Err": null -} -``` - -Respond with a string error if an error occurred. - -### /VolumeDriver.Mount - -**Request**: -``` -{ - "Name": "volume_name" -} -``` - -Docker requires the plugin to provide a volume, given a user specified volume -name. This is called once per container start. - -**Response**: -``` -{ - "Mountpoint": "/path/to/directory/on/host", - "Err": null -} -``` - -Respond with the path on the host filesystem where the volume has been made -available, and/or a string error if an error occurred. - -### /VolumeDriver.Path - -**Request**: -``` -{ - "Name": "volume_name" -} -``` - -Docker needs reminding of the path to the volume on the host. - -**Response**: -``` -{ - "Mountpoint": "/path/to/directory/on/host", - "Err": null -} -``` - -Respond with the path on the host filesystem where the volume has been made -available, and/or a string error if an error occurred. - -### /VolumeDriver.Unmount - -**Request**: -``` -{ - "Name": "volume_name" -} -``` - -Indication that Docker no longer is using the named volume. This is called once -per container stop. Plugin may deduce that it is safe to deprovision it at -this point. - -**Response**: -``` -{ - "Err": null -} -``` - -Respond with a string error if an error occurred. - -## Plugin retries - -Attempts to call a method on a plugin are retried with an exponential backoff -for up to 30 seconds. This may help when packaging plugins as containers, since -it gives plugin containers a chance to start up before failing any user -containers which depend on them. diff --git a/components/engine/docs/sources/reference/commandline/cli.md b/components/engine/docs/sources/reference/commandline/cli.md index b5171b1b34..a4a44fbb54 100644 --- a/components/engine/docs/sources/reference/commandline/cli.md +++ b/components/engine/docs/sources/reference/commandline/cli.md @@ -1000,8 +1000,7 @@ Creates a new container. --security-opt=[] Security options -t, --tty=false Allocate a pseudo-TTY -u, --user="" Username or UID - -v, --volume=[] Bind mount a volume, or specify name for volume plugin - --volume-driver= Optional volume driver (plugin name) for the container (Experimental) + -v, --volume=[] Bind mount a volume --volumes-from=[] Mount volumes from the specified container(s) -w, --workdir="" Working directory inside the container @@ -1971,8 +1970,7 @@ To remove an image using its digest: --sig-proxy=true Proxy received signals to the process -t, --tty=false Allocate a pseudo-TTY -u, --user="" Username or UID (format: [:]) - -v, --volume=[] Bind mount a volume, or specify name for volume plugin - --volume-driver= Optional volume driver (plugin name) for the container (Experimental) + -v, --volume=[] Bind mount a volume --volumes-from=[] Mount volumes from the specified container(s) -w, --workdir="" Working directory inside the container @@ -2273,21 +2271,6 @@ application change: `--rm` option means that when the container exits, the container's layer is removed. - -*Experimental*: - - $ docker run -ti -v volumename:/data --volume-driver=flocker busybox sh - -By specifying a volume name in conjunction with a volume driver, volume plugins -such as [Flocker](https://clusterhq.com/docker-plugin/), once installed, can be -used to manage volumes external to a single host, such as those on EBS. In this -example, "volumename" is passed through to the volume plugin as a user-given -name for the volume which allows the plugin to associate it with an external -volume beyond the lifetime of a single container or container host. This can be -used, for example, to move a stateful container from one server to another. - -The `volumename` must not begin with a `/`. - #### Restart policies Use Docker's `--restart` to specify a container's *restart policy*. A restart diff --git a/components/engine/docs/sources/userguide/dockervolumes.md b/components/engine/docs/sources/userguide/dockervolumes.md index 567d174716..c7126d7c33 100644 --- a/components/engine/docs/sources/userguide/dockervolumes.md +++ b/components/engine/docs/sources/userguide/dockervolumes.md @@ -210,14 +210,6 @@ Then un-tar the backup file in the new container's data volume. You can use the techniques above to automate backup, migration and restore testing using your preferred tools. -## (Experimental) Integrating Docker with external storage systems - -Docker volume plugins such as [Flocker](https://clusterhq.com/docker-plugin/) -enable Docker deployments to be integrated with external storage systems, such -as Amazon EBS, and enable data volumes to persist beyond the lifetime of a -single Docker host. See the [plugin section of the user -guide](/userguide/plugins) for more information. - # Next steps Now we've learned a bit more about how to use Docker we're going to see how to diff --git a/components/engine/docs/sources/userguide/index.md b/components/engine/docs/sources/userguide/index.md index 8395757919..9cc1c6db30 100644 --- a/components/engine/docs/sources/userguide/index.md +++ b/components/engine/docs/sources/userguide/index.md @@ -105,12 +105,6 @@ works with Docker can now transparently scale up to multiple hosts. Go to [Docker Swarm user guide](/swarm/). -## (Experimental) Docker Plugins - -Docker plugins allow you to extend the capabilities of the Docker Engine. - -Go to [Docker Plugins](/userguide/plugins). - ## Getting help * [Docker homepage](http://www.docker.com/) diff --git a/components/engine/docs/sources/userguide/plugins.md b/components/engine/docs/sources/userguide/plugins.md deleted file mode 100644 index 5fc7864010..0000000000 --- a/components/engine/docs/sources/userguide/plugins.md +++ /dev/null @@ -1,51 +0,0 @@ -page_title: Docker Plugins -page_description: Learn what Docker Plugins are and how to use them. -page_keywords: plugins, extensions, extensibility - -# Understanding Docker Plugins - -You can extend the capabilities of the Docker Engine by loading third-party -plugins. - -## Types of plugins - -Plugins extend Docker's functionality. They come in specific types. For -example, a **volume plugin** might enable Docker volumes to persist across -multiple Docker hosts. - -Currently Docker supports **volume plugins**. In the future it will support -additional plugin types. - -## Installing a plugin - -Follow the instructions in the plugin's documentation. - -## Finding a plugin - -The following plugins exist: - -* The [Flocker plugin](https://clusterhq.com/docker-plugin/) is a volume plugin - which provides multi-host portable volumes for Docker, enabling you to run - databases and other stateful containers and move them around across a cluster - of machines. - -## Using a plugin - -Depending on the plugin type, there are additional arguments to `docker` CLI -commands. - -* For example `docker run` has a [`--volume-driver` argument]( - /reference/commandline/cli/#run). - -You can also use plugins via the [Docker Remote API]( -/reference/api/docker_remote_api/). - -## Troubleshooting a plugin - -If you are having problems with Docker after loading a plugin, ask the authors -of the plugin for help. The Docker team may not be able to assist you. - -## Writing a plugin - -If you are interested in writing a plugin for Docker, or seeing how they work -under the hood, see the [docker plugins reference](/reference/api/plugin_api). From cfff66c7bb46b2412840c438c19dda9cef2e9d0a Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Fri, 22 May 2015 16:47:28 -0700 Subject: [PATCH 9/9] Document in the experimental section Signed-off-by: Arnaud Porterie (cherry picked from commit a31633c2707c78c2ffd684a5a3e576f310af0fb5) Upstream-commit: 8d0486832dafb775e93dafd1229fcf7e81ee4a28 Component: engine --- .../docs/sources/experimental/plugin_api.md | 223 ++++++++++++++++++ .../docs/sources/experimental/plugins.md | 46 ++++ .../sources/experimental/plugins_volume.md | 43 ++++ 3 files changed, 312 insertions(+) create mode 100644 components/engine/docs/sources/experimental/plugin_api.md create mode 100644 components/engine/docs/sources/experimental/plugins.md create mode 100644 components/engine/docs/sources/experimental/plugins_volume.md diff --git a/components/engine/docs/sources/experimental/plugin_api.md b/components/engine/docs/sources/experimental/plugin_api.md new file mode 100644 index 0000000000..770f606b4c --- /dev/null +++ b/components/engine/docs/sources/experimental/plugin_api.md @@ -0,0 +1,223 @@ +page_title: Plugin API documentation +page_description: Documentation for writing a Docker plugin. +page_keywords: docker, plugins, api, extensions + +# Docker Plugin API + +Docker plugins are out-of-process extensions which add capabilities to the +Docker Engine. + +This page is intended for people who want to develop their own Docker plugin. +If you just want to learn about or use Docker plugins, look +[here](/userguide/plugins). + +## What plugins are + +A plugin is a process running on the same docker host as the docker daemon, +which registers itself by placing a file in `/usr/share/docker/plugins` (the +"plugin directory"). + +Plugins have human-readable names, which are short, lowercase strings. For +example, `flocker` or `weave`. + +Plugins can run inside or outside containers. Currently running them outside +containers is recommended. + +## Plugin discovery + +Docker discovers plugins by looking for them in the plugin directory whenever a +user or container tries to use one by name. + +There are two types of files which can be put in the plugin directory. + +* `.sock` files are UNIX domain sockets. +* `.spec` files are text files containing a URL, such as `unix:///other.sock`. + +The name of the file (excluding the extension) determines the plugin name. + +For example, the `flocker` plugin might create a UNIX socket at +`/usr/share/docker/plugins/flocker.sock`. + +Plugins must be run locally on the same machine as the Docker daemon. UNIX +domain sockets are strongly encouraged for security reasons. + +## Plugin lifecycle + +Plugins should be started before Docker, and stopped after Docker. For +example, when packaging a plugin for a platform which supports `systemd`, you +might use [`systemd` dependencies]( +http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before=) to +manage startup and shutdown order. + +When upgrading a plugin, you should first stop the Docker daemon, upgrade the +plugin, then start Docker again. + +If a plugin is packaged as a container, this may cause issues. Plugins as +containers are currently considered experimental due to these shutdown/startup +ordering issues. These issues are mitigated by plugin retries (see below). + +## Plugin activation + +When a plugin is first referred to -- either by a user referring to it by name +(e.g. `docker run --volume-driver=foo`) or a container already configured to +use a plugin being started -- Docker looks for the named plugin in the plugin +directory and activates it with a handshake. See Handshake API below. + +Plugins are *not* activated automatically at Docker daemon startup. Rather, +they are activated only lazily, or on-demand, when they are needed. + +## API design + +The Plugin API is RPC-style JSON over HTTP, much like webhooks. + +Requests flow *from* the Docker daemon *to* the plugin. So the plugin needs to +implement an HTTP server and bind this to the UNIX socket mentioned in the +"plugin discovery" section. + +All requests are HTTP `POST` requests. + +The API is versioned via an Accept header, which currently is always set to +`application/vnd.docker.plugins.v1+json`. + +## Handshake API + +Plugins are activated via the following "handshake" API call. + +### /Plugin.Activate + +**Request:** empty body + +**Response:** +``` +{ + "Implements": ["VolumeDriver"] +} +``` + +Responds with a list of Docker subsystems which this plugin implements. +After activation, the plugin will then be sent events from this subsystem. + +## Volume API + +If a plugin registers itself as a `VolumeDriver` (see above) then it is +expected to provide writeable paths on the host filesystem for the Docker +daemon to provide to containers to consume. + +The Docker daemon handles bind-mounting the provided paths into user +containers. + +### /VolumeDriver.Create + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Instruct the plugin that the user wants to create a volume, given a user +specified volume name. The plugin does not need to actually manifest the +volume on the filesystem yet (until Mount is called). + +**Response**: +``` +{ + "Err": null +} +``` + +Respond with a string error if an error occurred. + +### /VolumeDriver.Remove + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Create a volume, given a user specified volume name. + +**Response**: +``` +{ + "Err": null +} +``` + +Respond with a string error if an error occurred. + +### /VolumeDriver.Mount + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Docker requires the plugin to provide a volume, given a user specified volume +name. This is called once per container start. + +**Response**: +``` +{ + "Mountpoint": "/path/to/directory/on/host", + "Err": null +} +``` + +Respond with the path on the host filesystem where the volume has been made +available, and/or a string error if an error occurred. + +### /VolumeDriver.Path + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Docker needs reminding of the path to the volume on the host. + +**Response**: +``` +{ + "Mountpoint": "/path/to/directory/on/host", + "Err": null +} +``` + +Respond with the path on the host filesystem where the volume has been made +available, and/or a string error if an error occurred. + +### /VolumeDriver.Unmount + +**Request**: +``` +{ + "Name": "volume_name" +} +``` + +Indication that Docker no longer is using the named volume. This is called once +per container stop. Plugin may deduce that it is safe to deprovision it at +this point. + +**Response**: +``` +{ + "Err": null +} +``` + +Respond with a string error if an error occurred. + +## Plugin retries + +Attempts to call a method on a plugin are retried with an exponential backoff +for up to 30 seconds. This may help when packaging plugins as containers, since +it gives plugin containers a chance to start up before failing any user +containers which depend on them. diff --git a/components/engine/docs/sources/experimental/plugins.md b/components/engine/docs/sources/experimental/plugins.md new file mode 100644 index 0000000000..d113e50a53 --- /dev/null +++ b/components/engine/docs/sources/experimental/plugins.md @@ -0,0 +1,46 @@ +page_title: Experimental feature - Plugins +page_keywords: experimental, Docker, plugins + +# Overview + +You can extend the capabilities of the Docker Engine by loading third-party +plugins. + +## Types of plugins + +Plugins extend Docker's functionality. They come in specific types. For +example, a [volume plugin](/experimental/plugins_volume) might enable Docker +volumes to persist across multiple Docker hosts. + +Currently Docker supports volume plugins. In the future it will support +additional plugin types. + +## Installing a plugin + +Follow the instructions in the plugin's documentation. + +## Finding a plugin + +The following plugins exist: + +* The [Flocker plugin](https://clusterhq.com/docker-plugin/) is a volume plugin +which provides multi-host portable volumes for Docker, enabling you to run + databases and other stateful containers and move them around across a cluster + of machines. + +## Troubleshooting a plugin + +If you are having problems with Docker after loading a plugin, ask the authors +of the plugin for help. The Docker team may not be able to assist you. + +## Writing a plugin + +If you are interested in writing a plugin for Docker, or seeing how they work +under the hood, see the [docker plugins reference](/experimental/plugin_api). + +# Related GitHub PRs and issues + +- [#13222](https://github.com/docker/docker/pull/13222) Plugins plumbing + +Send us feedback and comments on [#13419](https://github.com/docker/docker/issues/13419), +or on the usual Google Groups (docker-user, docker-dev) and IRC channels. diff --git a/components/engine/docs/sources/experimental/plugins_volume.md b/components/engine/docs/sources/experimental/plugins_volume.md new file mode 100644 index 0000000000..81112e18ae --- /dev/null +++ b/components/engine/docs/sources/experimental/plugins_volume.md @@ -0,0 +1,43 @@ +page_title: Experimental feature - Volume plugins +page_keywords: experimental, Docker, plugins, volume + +# Overview + +Docker volume plugins enable Docker deployments to be integrated with external +storage systems, such as Amazon EBS, and enable data volumes to persist beyond +the lifetime of a single Docker host. See the [plugin documentation](/experimental/plugins) +for more information. + +# Command-line changes + +This experimental features introduces two changes to the `docker run` command: + +- The `--volume-driver` flag is introduced. +- The `-v` syntax is changed to accept a volume name a first component. + +Example: + + $ docker run -ti -v volumename:/data --volume-driver=flocker busybox sh + +By specifying a volume name in conjunction with a volume driver, volume plugins +such as [Flocker](https://clusterhq.com/docker-plugin/), once installed, can be +used to manage volumes external to a single host, such as those on EBS. In this +example, "volumename" is passed through to the volume plugin as a user-given +name for the volume which allows the plugin to associate it with an external +volume beyond the lifetime of a single container or container host. This can be +used, for example, to move a stateful container from one server to another. + +The `volumename` must not begin with a `/`. + +# API changes + +The container creation endpoint (`/containers/create`) accepts a `VolumeDriver` +field of type `string` allowing to specify the name of the driver. It's default +value of `"local"` (the default driver for local volumes). + +# Related GitHub PRs and issues + +- [#13161](https://github.com/docker/docker/pull/13161) Volume refactor and external volume plugins + +Send us feedback and comments on [#13420](https://github.com/docker/docker/issues/13420), +or on the usual Google Groups (docker-user, docker-dev) and IRC channels.