From 81fa9feb0cdc0773eff99d7393c16271e84aac08 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 19 May 2015 13:05:25 -0700 Subject: [PATCH] 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 --- api/client/cli.go | 17 +- builder/internals.go | 2 +- daemon/container.go | 175 ++++++-- daemon/container_linux.go | 9 +- daemon/container_windows.go | 6 - daemon/create.go | 51 ++- daemon/daemon.go | 93 ++-- daemon/delete.go | 16 +- daemon/inspect.go | 16 +- daemon/volumes.go | 413 ++++++++---------- daemon/volumes_linux.go | 65 +-- daemon/volumes_unit_test.go | 146 +++++++ daemon/volumes_windows.go | 10 +- 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 + docs/sources/userguide/index.md | 6 + docs/sources/userguide/plugins.md | 51 +++ integration-cli/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 - pkg/plugins/client.go | 20 +- pkg/plugins/client_test.go | 44 +- runconfig/config.go | 1 + runconfig/parse.go | 2 + utils/tcp.go | 22 + volume/drivers/adapter.go | 51 +++ volume/drivers/api.go | 20 + volume/drivers/extpoint.go | 61 +++ volume/drivers/proxy.go | 65 +++ volume/local/local.go | 126 ++++++ volume/volume.go | 26 ++ volumes/repository.go | 193 -------- volumes/repository_test.go | 164 ------- volumes/volume.go | 152 ------- volumes/volume_test.go | 55 --- 43 files changed, 1538 insertions(+), 1191 deletions(-) create mode 100644 daemon/volumes_unit_test.go create mode 100644 docs/sources/reference/api/plugin_api.md create mode 100644 docs/sources/userguide/plugins.md create mode 100644 integration-cli/docker_cli_start_volume_driver_unix_test.go create mode 100644 utils/tcp.go create mode 100644 volume/drivers/adapter.go create mode 100644 volume/drivers/api.go create mode 100644 volume/drivers/extpoint.go create mode 100644 volume/drivers/proxy.go create mode 100644 volume/local/local.go create mode 100644 volume/volume.go delete mode 100644 volumes/repository.go delete mode 100644 volumes/repository_test.go delete mode 100644 volumes/volume.go delete mode 100644 volumes/volume_test.go diff --git a/api/client/cli.go b/api/client/cli.go index b649537c97..f78827dfa1 100644 --- a/api/client/cli.go +++ b/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/builder/internals.go b/builder/internals.go index 5764c19e29..b264918c3a 100644 --- a/builder/internals.go +++ b/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/daemon/container.go b/daemon/container.go index 7a59bec5d7..23a63a52db 100644 --- a/daemon/container.go +++ b/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/daemon/container_linux.go b/daemon/container_linux.go index 72e8409275..422da05867 100644 --- a/daemon/container_linux.go +++ b/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/daemon/container_windows.go b/daemon/container_windows.go index 5f980b5288..141c6c9ef3 100644 --- a/daemon/container_windows.go +++ b/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/daemon/create.go b/daemon/create.go index d8addd3a99..bf55febe02 100644 --- a/daemon/create.go +++ b/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/daemon/daemon.go b/daemon/daemon.go index 96c4e9c736..269e689b99 100644 --- a/daemon/daemon.go +++ b/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/daemon/delete.go b/daemon/delete.go index f82f612ae3..20451bd9de 100644 --- a/daemon/delete.go +++ b/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/daemon/inspect.go b/daemon/inspect.go index 17f8ec42ed..146bd77e9e 100644 --- a/daemon/inspect.go +++ b/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/daemon/volumes.go b/daemon/volumes.go index 794775dcbb..8ca985500b 100644 --- a/daemon/volumes.go +++ b/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/daemon/volumes_linux.go b/daemon/volumes_linux.go index 8bb7b8d841..8eea5e067f 100644 --- a/daemon/volumes_linux.go +++ b/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()...) - - container.command.Mounts = mounts - return nil + mounts = sortMounts(mounts) + return append(mounts, container.networkMounts()...), 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/daemon/volumes_unit_test.go b/daemon/volumes_unit_test.go new file mode 100644 index 0000000000..ac17b292fd --- /dev/null +++ b/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/daemon/volumes_windows.go b/daemon/volumes_windows.go index c910302daf..762f404883 100644 --- a/daemon/volumes_windows.go +++ b/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/docs/mkdocs.yml b/docs/mkdocs.yml index b501c9c398..d357983005 100644 --- a/docs/mkdocs.yml +++ b/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/docs/sources/reference/api/README.md b/docs/sources/reference/api/README.md index ec1cbcb2c3..4f72fbfe33 100644 --- a/docs/sources/reference/api/README.md +++ b/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/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 4c77cab8cf..79983ed580 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/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/docs/sources/reference/api/docker_remote_api_v1.19.md b/docs/sources/reference/api/docker_remote_api_v1.19.md index dde8ee79a1..5175ed9782 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.19.md +++ b/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/docs/sources/reference/api/plugin_api.md b/docs/sources/reference/api/plugin_api.md new file mode 100644 index 0000000000..770f606b4c --- /dev/null +++ b/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/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index a4a44fbb54..e1c9f56ad3 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/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/docs/sources/userguide/dockervolumes.md b/docs/sources/userguide/dockervolumes.md index c7126d7c33..c993d61814 100644 --- a/docs/sources/userguide/dockervolumes.md +++ b/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/docs/sources/userguide/index.md b/docs/sources/userguide/index.md index 9cc1c6db30..f82e8c70fc 100644 --- a/docs/sources/userguide/index.md +++ b/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/docs/sources/userguide/plugins.md b/docs/sources/userguide/plugins.md new file mode 100644 index 0000000000..5fc7864010 --- /dev/null +++ b/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/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index ec68f550c3..363d279120 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/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/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index ef22c993bb..e6c1255d4c 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/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/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 0051f280e1..7e60675045 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/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/integration-cli/docker_cli_start_test.go b/integration-cli/docker_cli_start_test.go index caf8428738..0475826738 100644 --- a/integration-cli/docker_cli_start_test.go +++ b/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/integration-cli/docker_cli_start_volume_driver_unix_test.go b/integration-cli/docker_cli_start_volume_driver_unix_test.go new file mode 100644 index 0000000000..4d6d44a537 --- /dev/null +++ b/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/integration-cli/docker_test_vars.go b/integration-cli/docker_test_vars.go index 9cb28b274e..ed394d26dd 100644 --- a/integration-cli/docker_test_vars.go +++ b/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/pkg/plugins/client.go b/pkg/plugins/client.go index 00ca105cd2..d531fa46fb 100644 --- a/pkg/plugins/client.go +++ b/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/pkg/plugins/client_test.go b/pkg/plugins/client_test.go index b414ecb5d9..0f7cd34dfa 100644 --- a/pkg/plugins/client_test.go +++ b/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/runconfig/config.go b/runconfig/config.go index 8778d26125..13d7189569 100644 --- a/runconfig/config.go +++ b/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/runconfig/parse.go b/runconfig/parse.go index f6967ab444..e9f0b51577 100644 --- a/runconfig/parse.go +++ b/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/utils/tcp.go b/utils/tcp.go new file mode 100644 index 0000000000..75980ff69a --- /dev/null +++ b/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/volume/drivers/adapter.go b/volume/drivers/adapter.go new file mode 100644 index 0000000000..e849fb8083 --- /dev/null +++ b/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/volume/drivers/api.go b/volume/drivers/api.go new file mode 100644 index 0000000000..1b98fa7fc5 --- /dev/null +++ b/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/volume/drivers/extpoint.go b/volume/drivers/extpoint.go new file mode 100644 index 0000000000..228b337be3 --- /dev/null +++ b/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/volume/drivers/proxy.go b/volume/drivers/proxy.go new file mode 100644 index 0000000000..1bc1586791 --- /dev/null +++ b/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/volume/local/local.go b/volume/local/local.go new file mode 100644 index 0000000000..3082e72bd0 --- /dev/null +++ b/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/volume/volume.go b/volume/volume.go new file mode 100644 index 0000000000..6edcae3c21 --- /dev/null +++ b/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/volumes/repository.go b/volumes/repository.go deleted file mode 100644 index 71d6c0ad60..0000000000 --- a/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/volumes/repository_test.go b/volumes/repository_test.go deleted file mode 100644 index 801c225f75..0000000000 --- a/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/volumes/volume.go b/volumes/volume.go deleted file mode 100644 index 5b3b646018..0000000000 --- a/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/volumes/volume_test.go b/volumes/volume_test.go deleted file mode 100644 index b30549d379..0000000000 --- a/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") - } -}