Add volume API/CLI

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff 2015-06-12 09:25:32 -04:00
parent 7ef08f39ed
commit b3b7eb2723
49 changed files with 1457 additions and 396 deletions

231
api/client/volume.go Normal file
View file

@ -0,0 +1,231 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/url"
"text/tabwriter"
"text/template"
"github.com/docker/docker/api/types"
Cli "github.com/docker/docker/cli"
"github.com/docker/docker/opts"
flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/parsers/filters"
)
// CmdVolume is the parent subcommand for all volume commands
//
// Usage: docker volume <COMMAND> <OPTS>
func (cli *DockerCli) CmdVolume(args ...string) error {
description := "Manage Docker volumes\n\nCommands:\n"
commands := [][]string{
{"create", "Create a volume"},
{"inspect", "Return low-level information on a volume"},
{"ls", "List volumes"},
{"rm", "Remove a volume"},
}
for _, cmd := range commands {
description += fmt.Sprintf(" %-25.25s%s\n", cmd[0], cmd[1])
}
description += "\nRun 'docker volume COMMAND --help' for more information on a command."
cmd := Cli.Subcmd("volume", []string{"[COMMAND]"}, description, true)
cmd.ParseFlags(args, true)
return cli.CmdVolumeLs(args...)
}
// CmdVolumeLs outputs a list of Docker volumes.
//
// Usage: docker volume ls [OPTIONS]
func (cli *DockerCli) CmdVolumeLs(args ...string) error {
cmd := Cli.Subcmd("volume ls", nil, "List volumes", true)
quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display volume names")
flFilter := opts.NewListOpts(nil)
cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')")
cmd.Require(flag.Exact, 0)
cmd.ParseFlags(args, true)
volFilterArgs := filters.Args{}
for _, f := range flFilter.GetAll() {
var err error
volFilterArgs, err = filters.ParseFlag(f, volFilterArgs)
if err != nil {
return err
}
}
v := url.Values{}
if len(volFilterArgs) > 0 {
filterJSON, err := filters.ToParam(volFilterArgs)
if err != nil {
return err
}
v.Set("filters", filterJSON)
}
resp, err := cli.call("GET", "/volumes?"+v.Encode(), nil, nil)
if err != nil {
return err
}
var volumes types.VolumesListResponse
if err := json.NewDecoder(resp.body).Decode(&volumes); err != nil {
return err
}
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
if !*quiet {
fmt.Fprintf(w, "DRIVER \tVOLUME NAME")
fmt.Fprintf(w, "\n")
}
for _, vol := range volumes.Volumes {
if *quiet {
fmt.Fprintln(w, vol.Name)
continue
}
fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name)
}
w.Flush()
return nil
}
// CmdVolumeInspect displays low-level information on one or more volumes.
//
// Usage: docker volume inspect [OPTIONS] VOLUME [VOLUME...]
func (cli *DockerCli) CmdVolumeInspect(args ...string) error {
cmd := Cli.Subcmd("volume inspect", []string{"[VOLUME NAME]"}, "Return low-level information on a volume", true)
tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template.")
if err := cmd.Parse(args); err != nil {
return nil
}
cmd.Require(flag.Min, 1)
cmd.ParseFlags(args, true)
var tmpl *template.Template
if *tmplStr != "" {
var err error
tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr)
if err != nil {
return err
}
}
var status = 0
var volumes []*types.Volume
for _, name := range cmd.Args() {
resp, err := cli.call("GET", "/volumes/"+name, nil, nil)
if err != nil {
return err
}
var volume types.Volume
if err := json.NewDecoder(resp.body).Decode(&volume); err != nil {
fmt.Fprintf(cli.err, "%s\n", err)
status = 1
continue
}
if tmpl == nil {
volumes = append(volumes, &volume)
continue
}
if err := tmpl.Execute(cli.out, &volume); err != nil {
if err := tmpl.Execute(cli.out, &volume); err != nil {
fmt.Fprintf(cli.err, "%s\n", err)
status = 1
continue
}
}
io.WriteString(cli.out, "\n")
}
if tmpl != nil {
return nil
}
b, err := json.MarshalIndent(volumes, "", " ")
if err != nil {
return err
}
_, err = io.Copy(cli.out, bytes.NewReader(b))
if err != nil {
return err
}
io.WriteString(cli.out, "\n")
if status != 0 {
return Cli.StatusError{StatusCode: status}
}
return nil
}
// CmdVolumeCreate creates a new container from a given image.
//
// Usage: docker volume create [OPTIONS]
func (cli *DockerCli) CmdVolumeCreate(args ...string) error {
cmd := Cli.Subcmd("volume create", nil, "Create a volume", true)
flDriver := cmd.String([]string{"d", "-driver"}, "local", "Specify volume driver name")
flName := cmd.String([]string{"-name"}, "", "Specify volume name")
flDriverOpts := opts.NewMapOpts(nil, nil)
cmd.Var(flDriverOpts, []string{"o", "-opt"}, "Set driver specific options")
cmd.Require(flag.Exact, 0)
cmd.ParseFlags(args, true)
volReq := &types.VolumeCreateRequest{
Driver: *flDriver,
DriverOpts: flDriverOpts.GetAll(),
}
if *flName != "" {
volReq.Name = *flName
}
resp, err := cli.call("POST", "/volumes", volReq, nil)
if err != nil {
return err
}
var vol types.Volume
if err := json.NewDecoder(resp.body).Decode(&vol); err != nil {
return err
}
fmt.Fprintf(cli.out, "%s\n", vol.Name)
return nil
}
// CmdVolumeRm removes one or more containers.
//
// Usage: docker volume rm VOLUME [VOLUME...]
func (cli *DockerCli) CmdVolumeRm(args ...string) error {
cmd := Cli.Subcmd("volume rm", []string{"[NAME]"}, "Remove a volume", true)
cmd.Require(flag.Min, 1)
cmd.ParseFlags(args, true)
var status = 0
for _, name := range cmd.Args() {
_, err := cli.call("DELETE", "/volumes/"+name, nil, nil)
if err != nil {
fmt.Fprintf(cli.err, "%s\n", err)
status = 1
continue
}
fmt.Fprintf(cli.out, "%s\n", name)
}
if status != 0 {
return Cli.StatusError{StatusCode: status}
}
return nil
}

View file

@ -328,6 +328,8 @@ func createRouter(s *Server) *mux.Router {
"/containers/{name:.*}/attach/ws": s.wsContainersAttach,
"/exec/{id:.*}/json": s.getExecByID,
"/containers/{name:.*}/archive": s.getContainersArchive,
"/volumes": s.getVolumesList,
"/volumes/{name:.*}": s.getVolumeByName,
},
"POST": {
"/auth": s.postAuth,
@ -352,6 +354,7 @@ func createRouter(s *Server) *mux.Router {
"/exec/{name:.*}/start": s.postContainerExecStart,
"/exec/{name:.*}/resize": s.postContainerExecResize,
"/containers/{name:.*}/rename": s.postContainerRename,
"/volumes": s.postVolumesCreate,
},
"PUT": {
"/containers/{name:.*}/archive": s.putContainersArchive,
@ -359,6 +362,7 @@ func createRouter(s *Server) *mux.Router {
"DELETE": {
"/containers/{name:.*}": s.deleteContainers,
"/images/{name:.*}": s.deleteImages,
"/volumes/{name:.*}": s.deleteVolumes,
},
"OPTIONS": {
"": s.optionsHandler,

65
api/server/volume.go Normal file
View file

@ -0,0 +1,65 @@
package server
import (
"encoding/json"
"net/http"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/version"
)
func (s *Server) getVolumesList(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := parseForm(r); err != nil {
return err
}
volumes, err := s.daemon.Volumes(r.Form.Get("filters"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, &types.VolumesListResponse{Volumes: volumes})
}
func (s *Server) getVolumeByName(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := parseForm(r); err != nil {
return err
}
v, err := s.daemon.VolumeInspect(vars["name"])
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, v)
}
func (s *Server) postVolumesCreate(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := parseForm(r); err != nil {
return err
}
if err := checkForJSON(r); err != nil {
return err
}
var req types.VolumeCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return err
}
volume, err := s.daemon.VolumeCreate(req.Name, req.Driver, req.DriverOpts)
if err != nil {
return err
}
return writeJSON(w, http.StatusCreated, volume)
}
func (s *Server) deleteVolumes(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := parseForm(r); err != nil {
return err
}
if err := s.daemon.VolumeRm(vars["name"]); err != nil {
return err
}
w.WriteHeader(http.StatusNoContent)
return nil
}

View file

@ -301,3 +301,24 @@ type MountPoint struct {
Mode string
RW bool
}
// Volume represents the configuration of a volume for the remote API
type Volume struct {
Name string // Name is the name of the volume
Driver string // Driver is the Driver name used to create the volume
Mountpoint string // Mountpoint is the location on disk of the volume
}
// VolumesListResponse contains the response for the remote API:
// GET "/volumes"
type VolumesListResponse struct {
Volumes []*Volume // Volumes is the list of volumes being returned
}
// VolumeCreateRequest contains the response for the remote API:
// POST "/volumes"
type VolumeCreateRequest struct {
Name string // Name is the requested name of the volume
Driver string // Driver is the name of the driver that should be used to create the volume
DriverOpts map[string]string // DriverOpts holds the driver specific options to use for when creating the volume.
}

View file

@ -1197,7 +1197,7 @@ func (container *Container) isDestinationMounted(destination string) bool {
func (container *Container) prepareMountPoints() error {
for _, config := range container.MountPoints {
if len(config.Driver) > 0 {
v, err := createVolume(config.Name, config.Driver)
v, err := container.daemon.createVolume(config.Name, config.Driver, nil)
if err != nil {
return err
}
@ -1207,14 +1207,23 @@ func (container *Container) prepareMountPoints() error {
return nil
}
func (container *Container) removeMountPoints() error {
func (container *Container) removeMountPoints(rm bool) error {
var rmErrors []string
for _, m := range container.MountPoints {
if m.Volume != nil {
if err := removeVolume(m.Volume); err != nil {
return err
if m.Volume == nil {
continue
}
container.daemon.volumes.Decrement(m.Volume)
if rm {
if err := container.daemon.volumes.Remove(m.Volume); err != nil {
rmErrors = append(rmErrors, fmt.Sprintf("%v\n", err))
continue
}
}
}
if len(rmErrors) > 0 {
return fmt.Errorf("Error removing volumes:\n%v", rmErrors)
}
return nil
}

View file

@ -169,7 +169,7 @@ func (container *Container) prepareMountPoints() error {
}
// removeMountPoints is a no-op on Windows.
func (container *Container) removeMountPoints() error {
func (container *Container) removeMountPoints(_ bool) error {
return nil
}

View file

@ -4,9 +4,11 @@ import (
"fmt"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/graph/tags"
"github.com/docker/docker/image"
"github.com/docker/docker/pkg/parsers"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/runconfig"
"github.com/opencontainers/runc/libcontainer/label"
)
@ -124,3 +126,17 @@ func (daemon *Daemon) GenerateSecurityOpt(ipcMode runconfig.IpcMode, pidMode run
}
return nil, nil
}
// VolumeCreate creates a volume with the specified name, driver, and opts
// This is called directly from the remote API
func (daemon *Daemon) VolumeCreate(name, driverName string, opts map[string]string) (*types.Volume, error) {
if name == "" {
name = stringid.GenerateNonCryptoID()
}
v, err := daemon.volumes.Create(name, driverName, opts)
if err != nil {
return nil, err
}
return volumeToAPIType(v), nil
}

View file

@ -54,10 +54,11 @@ func createContainerPlatformSpecificSettings(container *Container, config *runco
}
}
v, err := createVolume(name, volumeDriver)
v, err := container.daemon.createVolume(name, volumeDriver, nil)
if err != nil {
return err
}
if err := label.Relabel(v.Path(), container.MountLabel, "z"); err != nil {
return err
}

View file

@ -103,6 +103,7 @@ type Daemon struct {
RegistryService *registry.Service
EventsService *events.Events
netController libnetwork.NetworkController
volumes *volumeStore
root string
shutdown bool
}
@ -653,7 +654,8 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
}
// Configure the volumes driver
if err := configureVolumes(config); err != nil {
volStore, err := configureVolumes(config)
if err != nil {
return nil, err
}
@ -740,6 +742,7 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
d.defaultLogConfig = config.LogConfig
d.RegistryService = registryService
d.EventsService = eventsService
d.volumes = volStore
d.root = config.Root
go d.execCommandGC()

View file

@ -13,7 +13,7 @@ import (
"github.com/docker/docker/pkg/truncindex"
"github.com/docker/docker/runconfig"
"github.com/docker/docker/volume"
"github.com/docker/docker/volume/drivers"
volumedrivers "github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/local"
)
@ -486,12 +486,12 @@ func TestRemoveLocalVolumesFollowingSymlinks(t *testing.T) {
}
m := c.MountPoints["/vol1"]
v, err := createVolume(m.Name, m.Driver)
_, err = daemon.VolumeCreate(m.Name, m.Driver, nil)
if err != nil {
t.Fatal(err)
}
if err := removeVolume(v); err != nil {
if err := daemon.VolumeRm(m.Name); err != nil {
t.Fatal(err)
}
@ -505,6 +505,7 @@ func initDaemonForVolumesTest(tmp string) (*Daemon, error) {
daemon := &Daemon{
repository: tmp,
root: tmp,
volumes: newVolumeStore([]volume.Volume{}),
}
volumesDriver, err := local.New(tmp)

View file

@ -256,13 +256,13 @@ func migrateIfDownlevel(driver graphdriver.Driver, root string) error {
return migrateIfAufs(driver, root)
}
func configureVolumes(config *Config) error {
func configureVolumes(config *Config) (*volumeStore, error) {
volumesDriver, err := local.New(config.Root)
if err != nil {
return err
return nil, err
}
volumedrivers.Register(volumesDriver, volumesDriver.Name())
return nil
return newVolumeStore(volumesDriver.List()), nil
}
func configureSysInit(config *Config) (string, error) {

View file

@ -74,9 +74,9 @@ func migrateIfDownlevel(driver graphdriver.Driver, root string) error {
return nil
}
func configureVolumes(config *Config) error {
func configureVolumes(config *Config) (*volumeStore, error) {
// Windows does not support volumes at this time
return nil
return &volumeStore{}, nil
}
func configureSysInit(config *Config) (string, error) {

View file

@ -50,9 +50,7 @@ func (daemon *Daemon) ContainerRm(name string, config *ContainerRmConfig) error
return fmt.Errorf("Cannot destroy container %s: %v", name, err)
}
if config.RemoveVolume {
container.removeMountPoints()
}
container.removeMountPoints(config.RemoveVolume)
return nil
}
@ -137,6 +135,19 @@ func (daemon *Daemon) rm(container *Container, forceRemove bool) (err error) {
return nil
}
func (daemon *Daemon) DeleteVolumes(c *Container) error {
return c.removeMountPoints()
// VolumeRm removes the volume with the given name.
// If the volume is referenced by a container it is not removed
// This is called directly from the remote API
func (daemon *Daemon) VolumeRm(name string) error {
v, err := daemon.volumes.Get(name)
if err != nil {
return err
}
if err := daemon.volumes.Remove(v); err != nil {
if err == ErrVolumeInUse {
return fmt.Errorf("Conflict: %v", err)
}
return err
}
return nil
}

View file

@ -97,3 +97,11 @@ func (daemon *Daemon) ContainerExecInspect(id string) (*execConfig, error) {
}
return eConfig, nil
}
func (daemon *Daemon) VolumeInspect(name string) (*types.Volume, error) {
v, err := daemon.volumes.Get(name)
if err != nil {
return nil, err
}
return volumeToAPIType(v), nil
}

View file

@ -214,3 +214,32 @@ func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container,
}
return containers, nil
}
func (daemon *Daemon) Volumes(filter string) ([]*types.Volume, error) {
var volumesOut []*types.Volume
volFilters, err := filters.FromParam(filter)
if err != nil {
return nil, err
}
filterUsed := false
if i, ok := volFilters["dangling"]; ok {
if len(i) > 1 {
return nil, fmt.Errorf("Conflict: cannot use more than 1 value for `dangling` filter")
}
filterValue := i[0]
if strings.ToLower(filterValue) == "true" || filterValue == "1" {
filterUsed = true
}
}
volumes := daemon.volumes.List()
for _, v := range volumes {
if filterUsed && daemon.volumes.Count(v) == 0 {
continue
}
volumesOut = append(volumesOut, volumeToAPIType(v))
}
return volumesOut, nil
}

View file

@ -7,15 +7,24 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/chrootarchive"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/volume"
"github.com/docker/docker/volume/drivers"
)
// ErrVolumeReadonly is used to signal an error when trying to copy data into
// a volume mount that is not writable.
var ErrVolumeReadonly = errors.New("mounted volume is marked read-only")
var (
// ErrVolumeReadonly is used to signal an error when trying to copy data into
// a volume mount that is not writable.
ErrVolumeReadonly = errors.New("mounted volume is marked read-only")
// ErrVolumeInUse is a typed error returned when trying to remove a volume that is currently in use by a container
ErrVolumeInUse = errors.New("volume is in use")
// ErrNoSuchVolume is a typed error returned if the requested volume doesn't exist in the volume store
ErrNoSuchVolume = errors.New("no such volume")
)
// mountPoint is the intersection point between a volume and a container. It
// specifies which volume is to be used and where inside a container it should
@ -92,3 +101,144 @@ func copyExistingContents(source, destination string) error {
}
return copyOwnership(source, destination)
}
func newVolumeStore(vols []volume.Volume) *volumeStore {
store := &volumeStore{
vols: make(map[string]*volumeCounter),
}
for _, v := range vols {
store.vols[v.Name()] = &volumeCounter{v, 0}
}
return store
}
// volumeStore is a struct that stores the list of volumes available and keeps track of their usage counts
type volumeStore struct {
vols map[string]*volumeCounter
mu sync.Mutex
}
type volumeCounter struct {
volume.Volume
count int
}
func getVolumeDriver(name string) (volume.Driver, error) {
if name == "" {
name = volume.DefaultDriverName
}
return volumedrivers.Lookup(name)
}
// Create tries to find an existing volume with the given name or create a new one from the passed in driver
func (s *volumeStore) Create(name, driverName string, opts map[string]string) (volume.Volume, error) {
s.mu.Lock()
defer s.mu.Unlock()
if vc, exists := s.vols[name]; exists {
return vc.Volume, nil
}
vd, err := getVolumeDriver(driverName)
if err != nil {
return nil, err
}
v, err := vd.Create(name, opts)
if err != nil {
return nil, err
}
s.vols[v.Name()] = &volumeCounter{v, 0}
return v, nil
}
// Get looks if a volume with the given name exists and returns it if so
func (s *volumeStore) Get(name string) (volume.Volume, error) {
s.mu.Lock()
defer s.mu.Unlock()
vc, exists := s.vols[name]
if !exists {
return nil, ErrNoSuchVolume
}
return vc.Volume, nil
}
// Remove removes the requested volume. A volume is not removed if the usage count is > 0
func (s *volumeStore) Remove(v volume.Volume) error {
s.mu.Lock()
defer s.mu.Unlock()
name := v.Name()
vc, exists := s.vols[name]
if !exists {
return ErrNoSuchVolume
}
if vc.count != 0 {
return ErrVolumeInUse
}
vd, err := getVolumeDriver(vc.DriverName())
if err != nil {
return err
}
if err := vd.Remove(vc.Volume); err != nil {
return err
}
delete(s.vols, name)
return nil
}
// Increment increments the usage count of the passed in volume by 1
func (s *volumeStore) Increment(v volume.Volume) {
s.mu.Lock()
defer s.mu.Unlock()
vc, exists := s.vols[v.Name()]
if !exists {
s.vols[v.Name()] = &volumeCounter{v, 1}
return
}
vc.count++
return
}
// Decrement decrements the usage count of the passed in volume by 1
func (s *volumeStore) Decrement(v volume.Volume) {
s.mu.Lock()
defer s.mu.Unlock()
vc, exists := s.vols[v.Name()]
if !exists {
return
}
vc.count--
return
}
// Count returns the usage count of the passed in volume
func (s *volumeStore) Count(v volume.Volume) int {
vc, exists := s.vols[v.Name()]
if !exists {
return 0
}
return vc.count
}
// List returns all the available volumes
func (s *volumeStore) List() []volume.Volume {
var ls []volume.Volume
for _, vc := range s.vols {
ls = append(ls, vc.Volume)
}
return ls
}
// volumeToAPIType converts a volume.Volume to the type used by the remote API
func volumeToAPIType(v volume.Volume) *types.Volume {
return &types.Volume{
Name: v.Name(),
Driver: v.DriverName(),
Mountpoint: v.Path(),
}
}

View file

@ -12,9 +12,9 @@ import (
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 (fakeDriver) Name() string { return "fake" }
func (fakeDriver) Create(name string, opts map[string]string) (volume.Volume, error) { return nil, nil }
func (fakeDriver) Remove(v volume.Volume) error { return nil }
func TestGetVolumeDriver(t *testing.T) {
_, err := getVolumeDriver("missing")

View file

@ -15,7 +15,7 @@ import (
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/runconfig"
"github.com/docker/docker/volume"
"github.com/docker/docker/volume/drivers"
volumedrivers "github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/local"
"github.com/opencontainers/runc/libcontainer/label"
)
@ -138,7 +138,7 @@ func (m mounts) parts(i int) int {
// It preserves the volume json configuration generated pre Docker 1.7 to be able to
// downgrade from Docker 1.7 to Docker 1.6 without losing volume compatibility.
func migrateVolume(id, vfs string) error {
l, err := getVolumeDriver(volume.DefaultDriverName)
l, err := volumedrivers.Lookup(volume.DefaultDriverName)
if err != nil {
return err
}
@ -209,7 +209,7 @@ func (daemon *Daemon) verifyVolumesInfo(container *Container) error {
// Volumes created with a Docker version >= 1.7. We verify integrity in case of data created
// with Docker 1.7 RC versions that put the information in
// DOCKER_ROOT/volumes/VOLUME_ID rather than DOCKER_ROOT/volumes/VOLUME_ID/_container_data.
l, err := getVolumeDriver(volume.DefaultDriverName)
l, err := volumedrivers.Lookup(volume.DefaultDriverName)
if err != nil {
return err
}
@ -311,7 +311,7 @@ func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runc
}
if len(cp.Source) == 0 {
v, err := createVolume(cp.Name, cp.Driver)
v, err := daemon.createVolume(cp.Name, cp.Driver, nil)
if err != nil {
return err
}
@ -336,7 +336,7 @@ func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runc
if len(bind.Name) > 0 && len(bind.Driver) > 0 {
// create the volume
v, err := createVolume(bind.Name, bind.Driver)
v, err := daemon.createVolume(bind.Name, bind.Driver, nil)
if err != nil {
return err
}
@ -362,6 +362,11 @@ func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runc
if m.BackwardsCompatible() {
bcVolumes[m.Destination] = m.Path()
bcVolumesRW[m.Destination] = m.RW
// This mountpoint is replacing an existing one, so the count needs to be decremented
if mp, exists := container.MountPoints[m.Destination]; exists && mp.Volume != nil {
daemon.volumes.Decrement(mp.Volume)
}
}
}
@ -375,29 +380,13 @@ func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runc
}
// createVolume creates a volume.
func createVolume(name, driverName string) (volume.Volume, error) {
vd, err := getVolumeDriver(driverName)
func (daemon *Daemon) createVolume(name, driverName string, opts map[string]string) (volume.Volume, error) {
v, err := daemon.volumes.Create(name, driverName, opts)
if err != nil {
return nil, err
}
return vd.Create(name)
}
// removeVolume removes a volume.
func removeVolume(v volume.Volume) error {
vd, err := getVolumeDriver(v.DriverName())
if err != nil {
return nil
}
return vd.Remove(v)
}
// getVolumeDriver returns the volume driver for the supplied name.
func getVolumeDriver(name string) (volume.Driver, error) {
if name == "" {
name = volume.DefaultDriverName
}
return volumedrivers.Lookup(name)
daemon.volumes.Increment(v)
return v, nil
}
// parseVolumeSource parses the origin sources that's mounted into the container.

View file

@ -59,5 +59,6 @@ var dockerCommands = []command{
{"top", "Display the running processes of a container"},
{"unpause", "Unpause all processes within a container"},
{"version", "Show the Docker version information"},
{"volume", "Manage Docker volumes"},
{"wait", "Block until a container stops, then print its exit code"},
}

View file

@ -52,13 +52,15 @@ containers.
**Request**:
```
{
"Name": "volume_name"
"Name": "volume_name",
"Opts": {}
}
```
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).
Opts is a map of driver specific options passed through from the user request.
**Response**:
```

View file

@ -10,39 +10,48 @@ parent = "smn_remoteapi"
# Docker Remote API
- By default the Docker daemon listens on `unix:///var/run/docker.sock`
and the client must have `root` access to interact with the daemon.
- If you are using `docker-machine`, the Docker daemon is on a virtual host that uses an encrypted TCP socket. In this situation, you need to add extra
parameters to `curl` or `wget` when making test API requests:
`curl --insecure --cert ~/.docker/cert.pem --key ~/.docker/key.pem https://YOUR_VM_IP:2376/images/json`
or
`wget --no-check-certificate --certificate=$DOCKER_CERT_PATH/cert.pem --private-key=$DOCKER_CERT_PATH/key.pem https://your_vm_ip:2376/images/json -O - -q`
- If a group named `docker` exists on your system, docker will apply
ownership of the socket to the group.
- The API tends to be REST, but for some complex commands, like attach
or pull, the HTTP connection is hijacked to transport STDOUT, STDIN,
and STDERR.
- Since API version 1.2, the auth configuration is now handled client
side, so the client has to send the `authConfig` as a `POST` in `/images/(name)/push`.
- authConfig, set as the `X-Registry-Auth` header, is currently a Base64
encoded (JSON) string with the following structure:
`{"username": "string", "password": "string", "email": "string",
"serveraddress" : "string", "auth": ""}`. Notice that `auth` is to be left
empty, `serveraddress` is a domain/ip without protocol, and that double
quotes (instead of single ones) are required.
- The Remote API uses an open schema model. In this model, unknown
properties in incoming messages will be ignored.
Client applications need to take this into account to ensure
they will not break when talking to newer Docker daemons.
Docker's Remote API uses an open schema model. In this model, unknown
properties in incoming messages are ignored. Client applications need to take
this behavior into account to ensure they do not break when talking to newer
Docker daemons.
The current version of the API is v1.21
The API tends to be REST, but for some complex commands, like attach or pull,
the HTTP connection is hijacked to transport STDOUT, STDIN, and STDERR.
By default the Docker daemon listens on `unix:///var/run/docker.sock` and the
client must have `root` access to interact with the daemon. If a group named
`docker` exists on your system, `docker` applies ownership of the socket to the
group.
Calling `/info` is the same as calling
`/v1.21/info`.
You can still call an old version of the API using
The current version of the API is v1.21 which means calling `/info` is the same
as calling `/v1.21/info`. To call an older version of the API use
`/v1.20/info`.
## Authentication
Since API version 1.2, the auth configuration is now handled client side, so the
client has to send the `authConfig` as a `POST` in `/images/(name)/push`. The
`authConfig`, set as the `X-Registry-Auth` header, is currently a Base64 encoded
(JSON) string with the following structure:
```
{"username": "string", "password": "string", "email": "string",
"serveraddress" : "string", "auth": ""}
```
Callers should leave the `auth` empty. The `serveraddress` is a domain/ip
without protocol. Throughout this structure, double quotes are required.
## Using Docker Machine with the API
If you are using `docker-machine`, the Docker daemon is on a virtual host that uses an encrypted TCP socket. This means, for Docker Machine users, you need to add extra parameters to `curl` or `wget` when making test API requests, for example:
```
curl --insecure --cert ~/.docker/cert.pem --key ~/.docker/key.pem https://YOUR_VM_IP:2376/images/json
wget --no-check-certificate --certificate=$DOCKER_CERT_PATH/cert.pem --private-key=$DOCKER_CERT_PATH/key.pem https://your_vm_ip:2376/images/json -O - -q
```
## Docker Events
The following diagram depicts the container states accessible through the API.
@ -59,248 +68,109 @@ Running `docker rmi` emits an **untag** event when removing an image name. The
> **Acknowledgement**: This diagram and the accompanying text were used with the permission of Matt Good and Gilder Labs. See Matt's original blog post [Docker Events Explained](http://gliderlabs.com/blog/2015/04/14/docker-events-explained/).
## v1.21
## Version history
### Full documentation
This section lists each version from latest to oldest. Each listing includes a link to the full documentation set and the changes relevant in that release.
[*Docker Remote API v1.21*](/reference/api/docker_remote_api_v1.21/)
### v1.21 API changes
## v1.20
[Docker Remote API v1.21](/reference/api/docker_remote_api_v1.21/) documentation
### Full documentation
* `GET /volumes` lists volumes from all volume drivers.
* `POST /volumes` to create a volume.
* `GET /volumes/(name)` get low-level information about a volume.
* `DELETE /volumes/(name)`remove a volume with the specified name.
[*Docker Remote API v1.20*](/reference/api/docker_remote_api_v1.20/)
### What's new
### v1.20 API changes
`GET /containers/(id)/archive`
[Docker Remote API v1.20](/reference/api/docker_remote_api_v1.20/) documentation
**New!**
Get an archive of filesystem content from a container.
* `GET /containers/(id)/archive` get an archive of filesystem content from a container.
* `PUT /containers/(id)/archive` upload an archive of content to be extracted to
an existing directory inside a container's filesystem.
* `POST /containers/(id)/copy` is deprecated in favor of the above `archive`
endpoint which can be used to download files and directories from a container.
* The `hostConfig` option now accepts the field `GroupAdd`, which specifies a
list of additional groups that the container process will run as.
`PUT /containers/(id)/archive`
### v1.19 API changes
**New!**
Upload an archive of content to be extracted to an
existing directory inside a container's filesystem.
[Docker Remote API v1.19](/reference/api/docker_remote_api_v1.19/) documentation
`POST /containers/(id)/copy`
**Deprecated!**
This copy endpoint has been deprecated in favor of the above `archive` endpoint
which can be used to download files and directories from a container.
**New!**
The `hostConfig` option now accepts the field `GroupAdd`, which specifies a list of additional
groups that the container process will run as.
## v1.19
### Full documentation
[*Docker Remote API v1.19*](/reference/api/docker_remote_api_v1.19/)
### What's new
**New!**
When the daemon detects a version mismatch with the client, usually when
* When the daemon detects a version mismatch with the client, usually when
the client is newer than the daemon, an HTTP 400 is now returned instead
of a 404.
* `GET /containers/(id)/stats` now accepts `stream` bool to get only one set of stats and disconnect.
* `GET /containers/(id)/logs` now accepts a `since` timestamp parameter.
* `GET /info` The fields `Debug`, `IPv4Forwarding`, `MemoryLimit`, and
`SwapLimit` 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`.
`GET /containers/(id)/stats`
### v1.18 API changes
**New!**
You can now supply a `stream` bool to get only one set of stats and
disconnect
[Docker Remote API v1.18](/reference/api/docker_remote_api_v1.18/) documentation
`GET /containers/(id)/logs`
* `GET /version` now returns `Os`, `Arch` and `KernelVersion`.
* `POST /containers/create` and `POST /containers/(id)/start`allow you to set ulimit settings for use in the container.
* `GET /info` now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`.
* `GET /images/json` added a `RepoDigests` field to include image digest information.
* `POST /build` can now set resource constraints for all containers created for the build.
* `CgroupParent` can be passed in the host config to setup container cgroups under a specific cgroup.
* `POST /build` closing the HTTP request cancels the build
* `POST /containers/(id)/exec` includes `Warnings` field to response.
**New!**
### v1.17 API changes
This endpoint now accepts a `since` timestamp parameter.
[Docker Remote API v1.17](/reference/api/docker_remote_api_v1.17/) documentation
`GET /info`
**New!**
The fields `Debug`, `IPv4Forwarding`, `MemoryLimit`, and `SwapLimit`
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`.
## v1.18
### Full documentation
[*Docker Remote API v1.18*](/reference/api/docker_remote_api_v1.18/)
### What's new
`GET /version`
**New!**
This endpoint now returns `Os`, `Arch` and `KernelVersion`.
`POST /containers/create`
`POST /containers/(id)/start`
**New!**
You can set ulimit settings to be used within the container.
`GET /info`
**New!**
This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`.
`GET /images/json`
**New!**
Added a `RepoDigests` field to include image digest information.
`POST /build`
**New!**
Builds can now set resource constraints for all containers created for the build.
**New!**
(`CgroupParent`) can be passed in the host config to setup container cgroups under a specific cgroup.
`POST /build`
**New!**
Closing the HTTP request will now cause the build to be canceled.
`POST /containers/(id)/exec`
**New!**
Add `Warnings` field to response.
## v1.17
### Full documentation
[*Docker Remote API v1.17*](/reference/api/docker_remote_api_v1.17/)
### What's new
The build supports `LABEL` command. Use this to add metadata
to an image. For example you could add data describing the content of an image.
`LABEL "com.example.vendor"="ACME Incorporated"`
**New!**
`POST /containers/(id)/attach` and `POST /exec/(id)/start`
**New!**
Docker client now hints potential proxies about connection hijacking using HTTP Upgrade headers.
`POST /containers/create`
**New!**
You can set labels on container create describing the container.
`GET /containers/json`
**New!**
The endpoint returns the labels associated with the containers (`Labels`).
`GET /containers/(id)/json`
**New!**
This endpoint now returns the list current execs associated with the container (`ExecIDs`).
This endpoint now returns the container labels (`Config.Labels`).
`POST /containers/(id)/rename`
**New!**
New endpoint to rename a container `id` to a new name.
`POST /containers/create`
`POST /containers/(id)/start`
**New!**
(`ReadonlyRootfs`) can be passed in the host config to mount the container's
root filesystem as read only.
`GET /containers/(id)/stats`
**New!**
This endpoint returns a live stream of a container's resource usage statistics.
`GET /images/json`
**New!**
This endpoint now returns the labels associated with each image (`Labels`).
* The build supports `LABEL` command. Use this to add metadata to an image. For
example you could add data describing the content of an image. `LABEL
"com.example.vendor"="ACME Incorporated"`
* `POST /containers/(id)/attach` and `POST /exec/(id)/start`
* The Docker client now hints potential proxies about connection hijacking using HTTP Upgrade headers.
* `POST /containers/create` sets labels on container create describing the container.
* `GET /containers/json` returns the labels associated with the containers (`Labels`).
* `GET /containers/(id)/json` returns the list current execs associated with the
container (`ExecIDs`). This endpoint now returns the container labels
(`Config.Labels`).
* `POST /containers/(id)/rename` renames a container `id` to a new name.*
* `POST /containers/create` and `POST /containers/(id)/start` callers can pass
`ReadonlyRootfs` in the host config to mount the container's root filesystem as
read only.
* `GET /containers/(id)/stats` returns a live stream of a container's resource usage statistics.
* `GET /images/json` returns the labels associated with each image (`Labels`).
## v1.16
### v1.16 API changes
### Full documentation
[Docker Remote API v1.16](/reference/api/docker_remote_api_v1.16/)
[*Docker Remote API v1.16*](/reference/api/docker_remote_api_v1.16/)
### What's new
`GET /info`
**New!**
`info` now returns the number of CPUs available on the machine (`NCPU`),
* `GET /info` returns the number of CPUs available on the machine (`NCPU`),
total memory available (`MemTotal`), a user-friendly name describing the running Docker daemon (`Name`), a unique ID identifying the daemon (`ID`), and
a list of daemon labels (`Labels`).
* `POST /containers/create` callers can set the new container's MAC address explicitly.
* Volumes are now initialized when the container is created.
* `POST /containers/(id)/copy` copies data which is contained in a volume.
`POST /containers/create`
### v1.15 API changes
**New!**
You can set the new container's MAC address explicitly.
[Docker Remote API v1.15](/reference/api/docker_remote_api_v1.15/) documentation
**New!**
Volumes are now initialized when the container is created.
`POST /containers/create` you can set a container's `HostConfig` when creating a
container. Previously this was only available when starting a container.
`POST /containers/(id)/copy`
### v1.14 API changes
**New!**
You can now copy data which is contained in a volume.
[Docker Remote API v1.14](/reference/api/docker_remote_api_v1.14/) documentation
## v1.15
### Full documentation
[*Docker Remote API v1.15*](/reference/api/docker_remote_api_v1.15/)
### What's new
`POST /containers/create`
**New!**
It is now possible to set a container's HostConfig when creating a container.
Previously this was only available when starting a container.
## v1.14
### Full documentation
[*Docker Remote API v1.14*](/reference/api/docker_remote_api_v1.14/)
### What's new
`DELETE /containers/(id)`
**New!**
When using `force`, the container will be immediately killed with SIGKILL.
`POST /containers/(id)/start`
**New!**
The `hostConfig` option now accepts the field `CapAdd`, which specifies a list of capabilities
* `DELETE /containers/(id)` when using `force`, the container will be immediately killed with SIGKILL.
* `POST /containers/(id)/start` the `hostConfig` option accepts the field `CapAdd`, which specifies a list of capabilities
to add, and the field `CapDrop`, which specifies a list of capabilities to drop.
`POST /images/create`
**New!**
The `fromImage` and `repo` parameters now supports the `repo:tag` format.
Consequently, the `tag` parameter is now obsolete. Using the new format and
the `tag` parameter at the same time will return an error.
* `POST /images/create` th `fromImage` and `repo` parameters supportthe
`repo:tag` format. Consequently, the `tag` parameter is now obsolete. Using the
new format and the `tag` parameter at the same time will return an error.

View file

@ -2245,6 +2245,126 @@ Status Codes:
- **404** no such exec instance
- **500** - server error
## 2.4 Volumes
### List volumes
`GET /volumes`
**Example request**:
GET /volumes HTTP/1.1
**Example response**:
HTTP/1.1 200 OK
Content-Type: application/json
{
"Volumes": [
{
"Name": "tardis",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/tardis"
}
]
}
Query Parameters:
- **filter** - JSON encoded value of the filters (a `map[string][]string`) to process on the volumes list. There is one available filter: `dangling=true`
Status Codes:
- **200** - no error
- **500** - server error
### Create a volume
`POST /volumes`
Create a volume
**Example request**:
POST /volumes HTTP/1.1
Content-Type: application/json
{
"Name": "tardis"
}
**Example response**:
HTTP/1.1 201 Created
Content-Type: application/json
{
"Name": "tardis"
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/tardis"
}
Status Codes:
- **201** - no error
- **500** - server error
JSON Parameters:
- **Name** - The new volume's name. If not specified, Docker generates a name.
- **Driver** - Name of the volume driver to use. Defaults to `local` for the name.
- **DriverOpts** - A mapping of driver options and values. These options are
passed directly to the driver and are driver specific.
### Inspect a volume
`GET /volumes/(name)`
Return low-level information on the volume `name`
**Example request**:
GET /volumes/tardis
**Example response**:
HTTP/1.1 200 OK
Content-Type: application/json
{
"Name": "tardis",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/tardis"
}
Status Codes:
- **200** - no error
- **404** - no such volume
- **500** - server error
### Remove a volume
`DELETE /volumes/(name)`
Instruct the driver to remove the volume (`name`).
**Example request**:
DELETE /volumes/local/tardis HTTP/1.1
**Example response**:
HTTP/1.1 204 No Content
Status Codes
- **204** - no error
- **404** - no such volume or volume driver
- **409** - volume is in use and cannot be removed
- **500** - server error
# 3. Going further
## 3.1 Inside `docker run`

View file

@ -40,7 +40,7 @@ Running `docker ps --no-trunc` showing 2 linked containers.
## Filtering
The filtering flag (`-f` or `--filter)` format is a `key=value` pair. If there is more
The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there is more
than one filter, then pass multiple flags (e.g. `--filter "foo=bar" --filter "bif=baz"`)
The currently supported filters are:

View file

@ -0,0 +1,42 @@
<!--[metadata]>
+++
title = "volume create"
description = "The volume create command description and usage"
keywords = ["volume, create"]
[menu.main]
parent = "smn_cli"
+++
<![end-metadata]-->
# volume create
Usage: docker volume create [OPTIONS]
Create a volume
-d, --driver=local Specify volume driver name
--help=false Print usage
--name= Specify volume name
-o, --opt=map[] Set driver specific options
Creates a new volume that containers can can consume and store data in. If a name is not specified, Docker generates a random name. You create a volume and then configure the container to use it, for example:
$ docker volume create --name hello
hello
$ docker run -d -v hello:/world busybox ls /world
The mount is created inside the container's `/src` directory. Docker does not support relative paths for mount points inside the container.
Multiple containers can use the same volume in the same time period. This is useful if two containers need access to shared data. For example, if one container writes and the other reads the data.
## Driver specific options
Some volume drivers may take options to customize the volume creation. Use the `-o` or `--opt` flags to pass driver options:
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
These options are passed directly to the volume driver. Options for
different volume drivers may do different things (or nothing at all).
*Note*: The built-in `local` volume driver does not currently accept any options.

View file

@ -0,0 +1,39 @@
<!--[metadata]>
+++
title = "volume inspect"
description = "The volume inspect command description and usage"
keywords = ["volume, inspect"]
[menu.main]
parent = "smn_cli"
+++
<![end-metadata]-->
# volume inspect
Usage: docker volume inspect [OPTIONS] [VOLUME NAME]
Inspect a volume
-f, --format= Format the output using the given go template.
Returns information about a volume. By default, this command renders all results
in a JSON array. You can specify an alternate format to execute a given template
is executed for each result. Go's
[text/template](http://golang.org/pkg/text/template/) package describes all the
details of the format.
Example output:
$ docker volume create
85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d
$ docker volume inspect 85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d
[
{
"Name": "85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d/_data"
}
]
$ docker volume inspect --format '{{ .Mountpoint }}' 85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d
"/var/lib/docker/volumes/85bffb0677236974f93955d8ecc4df55ef5070117b0e53333cc1b443777be24d/_data"

View file

@ -0,0 +1,34 @@
<!--[metadata]>
+++
title = "volume ls"
description = "The volume ls command description and usage"
keywords = ["volume, list"]
[menu.main]
parent = "smn_cli"
+++
<![end-metadata]-->
# volume ls
Usage: docker volume ls [OPTIONS]
List volumes
-f, --filter=[] Provide filter values (i.e. 'dangling=true')
--help=false Print usage
-q, --quiet=false Only display volume names
Lists all the volumes Docker knows about. You can filter using the `-f` or `--filter` flag. The filtering format is a `key=value` pair. To specify more than one filter, pass multiple flags (for example, `--filter "foo=bar" --filter "bif=baz"`)
There is a single supported filter `dangling=value` which takes a boolean of `true` or `false`.
Example output:
$ docker volume create --name rose
rose
$docker volume create --name tyler
tyler
$ docker volume ls
DRIVER VOLUME NAME
local rose
local tyler

View file

@ -0,0 +1,22 @@
<!--[metadata]>
+++
title = "ps"
description = "the volume rm command description and usage"
keywords = ["volume, rm"]
[menu.main]
parent = "smn_cli"
+++
<![end-metadata]-->
# volume rm
Usage: docker volume rm [OPTIONS] [VOLUME NAME]
Remove a volume
--help=false Print usage
Removes a volume. You cannot remove a volume that is in use by a container.
$ docker volume rm hello
hello

View file

@ -20,6 +20,7 @@ type DockerSuite struct {
func (s *DockerSuite) TearDownTest(c *check.C) {
deleteAllContainers()
deleteAllImages()
deleteAllVolumes()
}
func init() {

View file

@ -0,0 +1,85 @@
package main
import (
"encoding/json"
"net/http"
"path"
"github.com/docker/docker/api/types"
"github.com/go-check/check"
)
func (s *DockerSuite) TestVolumesApiList(c *check.C) {
dockerCmd(c, "run", "-d", "-v", "/foo", "busybox")
status, b, err := sockRequest("GET", "/volumes", nil)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusOK)
var volumes types.VolumesListResponse
c.Assert(json.Unmarshal(b, &volumes), check.IsNil)
c.Assert(len(volumes.Volumes), check.Equals, 1, check.Commentf("\n%v", volumes.Volumes))
}
func (s *DockerSuite) TestVolumesApiCreate(c *check.C) {
config := types.VolumeCreateRequest{
Name: "test",
}
status, b, err := sockRequest("POST", "/volumes", config)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusCreated, check.Commentf(string(b)))
var vol types.Volume
err = json.Unmarshal(b, &vol)
c.Assert(err, check.IsNil)
c.Assert(path.Base(path.Dir(vol.Mountpoint)), check.Equals, config.Name)
}
func (s *DockerSuite) TestVolumesApiRemove(c *check.C) {
dockerCmd(c, "run", "-d", "-v", "/foo", "--name=test", "busybox")
status, b, err := sockRequest("GET", "/volumes", nil)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusOK)
var volumes types.VolumesListResponse
c.Assert(json.Unmarshal(b, &volumes), check.IsNil)
c.Assert(len(volumes.Volumes), check.Equals, 1, check.Commentf("\n%v", volumes.Volumes))
v := volumes.Volumes[0]
status, _, err = sockRequest("DELETE", "/volumes/"+v.Name, nil)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusConflict, check.Commentf("Should not be able to remove a volume that is in use"))
dockerCmd(c, "rm", "-f", "test")
status, data, err := sockRequest("DELETE", "/volumes/"+v.Name, nil)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusNoContent, check.Commentf(string(data)))
}
func (s *DockerSuite) TestVolumesApiInspect(c *check.C) {
config := types.VolumeCreateRequest{
Name: "test",
}
status, b, err := sockRequest("POST", "/volumes", config)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusCreated, check.Commentf(string(b)))
status, b, err = sockRequest("GET", "/volumes", nil)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusOK, check.Commentf(string(b)))
var volumes types.VolumesListResponse
c.Assert(json.Unmarshal(b, &volumes), check.IsNil)
c.Assert(len(volumes.Volumes), check.Equals, 1, check.Commentf("\n%v", volumes.Volumes))
var vol types.Volume
status, b, err = sockRequest("GET", "/volumes/"+config.Name, nil)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusOK, check.Commentf(string(b)))
c.Assert(json.Unmarshal(b, &vol), check.IsNil)
c.Assert(vol.Name, check.Equals, config.Name)
}

View file

@ -71,6 +71,7 @@ func (s *DockerDaemonSuite) TestDaemonRestartWithVolumesRefs(c *check.C) {
if err := s.d.Restart(); err != nil {
c.Fatal(err)
}
if _, err := s.d.Cmd("run", "-d", "--volumes-from", "volrestarttest1", "--name", "volrestarttest2", "busybox", "top"); err != nil {
c.Fatal(err)
}
@ -1660,5 +1661,28 @@ func (s *DockerDaemonSuite) TestDaemonRestartWithPausedContainer(c *check.C) {
c.Fatal(err)
}
}
}
func (s *DockerDaemonSuite) TestDaemonRestartRmVolumeInUse(c *check.C) {
c.Assert(s.d.StartWithBusybox(), check.IsNil)
out, err := s.d.Cmd("create", "-v", "test:/foo", "busybox")
c.Assert(err, check.IsNil, check.Commentf(out))
c.Assert(s.d.Restart(), check.IsNil)
out, err = s.d.Cmd("volume", "rm", "test")
c.Assert(err, check.Not(check.IsNil), check.Commentf("should not be able to remove in use volume after daemon restart"))
c.Assert(strings.Contains(out, "in use"), check.Equals, true)
}
func (s *DockerDaemonSuite) TestDaemonRestartLocalVolumes(c *check.C) {
c.Assert(s.d.Start(), check.IsNil)
_, err := s.d.Cmd("volume", "create", "--name", "test")
c.Assert(err, check.IsNil)
c.Assert(s.d.Restart(), check.IsNil)
_, err = s.d.Cmd("volume", "inspect", "test")
c.Assert(err, check.IsNil)
}

View file

@ -238,7 +238,7 @@ func (s *DockerSuite) TestHelpTextVerify(c *check.C) {
}
expected := 39
expected := 40
if isLocalDaemon {
expected++ // for the daemon command
}

View file

@ -2781,3 +2781,13 @@ func (s *DockerSuite) TestRunCreateContainerFailedCleanUp(c *check.C) {
containerID, err := inspectField(name, "Id")
c.Assert(containerID, check.Equals, "", check.Commentf("Expected not to have this container: %s!", containerID))
}
func (s *DockerSuite) TestRunNamedVolume(c *check.C) {
dockerCmd(c, "run", "--name=test", "-v", "testing:/foo", "busybox", "sh", "-c", "echo hello > /foo/bar")
out, _ := dockerCmd(c, "run", "--volumes-from", "test", "busybox", "sh", "-c", "cat /foo/bar")
c.Assert(strings.TrimSpace(out), check.Equals, "hello")
out, _ = dockerCmd(c, "run", "-v", "testing:/foo", "busybox", "sh", "-c", "cat /foo/bar")
c.Assert(strings.TrimSpace(out), check.Equals, "hello")
}

View file

@ -119,11 +119,6 @@ func (s *DockerExternalVolumeSuite) SetUpSuite(c *check.C) {
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", "application/vnd.docker.plugins.v1+json")
fmt.Fprintln(w, `{}`)
})
@ -152,7 +147,7 @@ func (s *DockerExternalVolumeSuite) TestStartExternalNamedVolumeDriver(c *check.
out, err := s.d.Cmd("run", "--rm", "--name", "test-data", "-v", "external-volume-test:/tmp/external-volume-test", "--volume-driver", "test-external-volume-driver", "busybox:latest", "cat", "/tmp/external-volume-test/test")
if err != nil {
c.Fatal(err)
c.Fatal(out, err)
}
if !strings.Contains(out, s.server.URL) {
@ -202,20 +197,17 @@ func (s DockerExternalVolumeSuite) TestStartExternalVolumeDriverVolumesFrom(c *c
c.Fatal(err)
}
if _, err := s.d.Cmd("run", "-d", "--name", "vol-test1", "-v", "/foo", "--volume-driver", "test-external-volume-driver", "busybox:latest"); err != nil {
c.Fatal(err)
}
out, err := s.d.Cmd("run", "-d", "--name", "vol-test1", "-v", "/foo", "--volume-driver", "test-external-volume-driver", "busybox:latest")
c.Assert(err, check.IsNil, check.Commentf(out))
if _, err := s.d.Cmd("run", "--rm", "--volumes-from", "vol-test1", "--name", "vol-test2", "busybox", "ls", "/tmp"); err != nil {
c.Fatal(err)
}
out, err = s.d.Cmd("run", "--rm", "--volumes-from", "vol-test1", "--name", "vol-test2", "busybox", "ls", "/tmp")
c.Assert(err, check.IsNil, check.Commentf(out))
if _, err := s.d.Cmd("rm", "-f", "vol-test1"); err != nil {
c.Fatal(err)
}
out, err = s.d.Cmd("rm", "-fv", "vol-test1")
c.Assert(err, check.IsNil, check.Commentf(out))
c.Assert(s.ec.activations, check.Equals, 1)
c.Assert(s.ec.creations, check.Equals, 2)
c.Assert(s.ec.creations, check.Equals, 1)
c.Assert(s.ec.removals, check.Equals, 1)
c.Assert(s.ec.mounts, check.Equals, 2)
c.Assert(s.ec.unmounts, check.Equals, 2)
@ -226,12 +218,12 @@ func (s DockerExternalVolumeSuite) TestStartExternalVolumeDriverDeleteContainer(
c.Fatal(err)
}
if _, err := s.d.Cmd("run", "-d", "--name", "vol-test1", "-v", "/foo", "--volume-driver", "test-external-volume-driver", "busybox:latest"); err != nil {
c.Fatal(err)
if out, err := s.d.Cmd("run", "-d", "--name", "vol-test1", "-v", "/foo", "--volume-driver", "test-external-volume-driver", "busybox:latest"); err != nil {
c.Fatal(out, err)
}
if _, err := s.d.Cmd("rm", "-fv", "vol-test1"); err != nil {
c.Fatal(err)
if out, err := s.d.Cmd("rm", "-fv", "vol-test1"); err != nil {
c.Fatal(out, err)
}
c.Assert(s.ec.activations, check.Equals, 1)

View file

@ -0,0 +1,90 @@
package main
import (
"os/exec"
"strings"
"github.com/go-check/check"
)
func (s *DockerSuite) TestVolumeCliCreate(c *check.C) {
dockerCmd(c, "volume", "create")
_, err := runCommand(exec.Command(dockerBinary, "volume", "create", "-d", "nosuchdriver"))
c.Assert(err, check.Not(check.IsNil))
out, _ := dockerCmd(c, "volume", "create", "--name=test")
name := strings.TrimSpace(out)
c.Assert(name, check.Equals, "test")
}
func (s *DockerSuite) TestVolumeCliInspect(c *check.C) {
c.Assert(
exec.Command(dockerBinary, "volume", "inspect", "doesntexist").Run(),
check.Not(check.IsNil),
check.Commentf("volume inspect should error on non-existant volume"),
)
out, _ := dockerCmd(c, "volume", "create")
name := strings.TrimSpace(out)
out, _ = dockerCmd(c, "volume", "inspect", "--format='{{ .Name }}'", name)
c.Assert(strings.TrimSpace(out), check.Equals, name)
dockerCmd(c, "volume", "create", "--name", "test")
out, _ = dockerCmd(c, "volume", "inspect", "--format='{{ .Name }}'", "test")
c.Assert(strings.TrimSpace(out), check.Equals, "test")
}
func (s *DockerSuite) TestVolumeCliLs(c *check.C) {
out, _ := dockerCmd(c, "volume", "create")
id := strings.TrimSpace(out)
dockerCmd(c, "volume", "create", "--name", "test")
dockerCmd(c, "run", "-v", "/foo", "busybox", "ls", "/")
out, _ = dockerCmd(c, "volume", "ls")
outArr := strings.Split(strings.TrimSpace(out), "\n")
c.Assert(len(outArr), check.Equals, 4, check.Commentf("\n%s", out))
// Since there is no guarentee of ordering of volumes, we just make sure the names are in the output
c.Assert(strings.Contains(out, id+"\n"), check.Equals, true)
c.Assert(strings.Contains(out, "test\n"), check.Equals, true)
}
func (s *DockerSuite) TestVolumeCliRm(c *check.C) {
out, _ := dockerCmd(c, "volume", "create")
id := strings.TrimSpace(out)
dockerCmd(c, "volume", "create", "--name", "test")
dockerCmd(c, "volume", "rm", id)
dockerCmd(c, "volume", "rm", "test")
out, _ = dockerCmd(c, "volume", "ls")
outArr := strings.Split(strings.TrimSpace(out), "\n")
c.Assert(len(outArr), check.Equals, 1, check.Commentf("%s\n", out))
volumeID := "testing"
dockerCmd(c, "run", "-v", volumeID+":/foo", "--name=test", "busybox", "sh", "-c", "echo hello > /foo/bar")
out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "volume", "rm", "testing"))
c.Assert(
err,
check.Not(check.IsNil),
check.Commentf("Should not be able to remove volume that is in use by a container\n%s", out))
out, _ = dockerCmd(c, "run", "--volumes-from=test", "--name=test2", "busybox", "sh", "-c", "cat /foo/bar")
c.Assert(strings.TrimSpace(out), check.Equals, "hello")
dockerCmd(c, "rm", "-fv", "test2")
dockerCmd(c, "volume", "inspect", volumeID)
dockerCmd(c, "rm", "-f", "test")
out, _ = dockerCmd(c, "run", "--name=test2", "-v", volumeID+":/foo", "busybox", "sh", "-c", "cat /foo/bar")
c.Assert(strings.TrimSpace(out), check.Equals, "hello", check.Commentf("volume data was removed"))
dockerCmd(c, "rm", "test2")
dockerCmd(c, "volume", "rm", volumeID)
c.Assert(
exec.Command("volume", "rm", "doesntexist").Run(),
check.Not(check.IsNil),
check.Commentf("volume rm should fail with non-existant volume"),
)
}

View file

@ -445,6 +445,40 @@ func deleteAllContainers() error {
return nil
}
func deleteAllVolumes() error {
volumes, err := getAllVolumes()
if err != nil {
return err
}
var errors []string
for _, v := range volumes {
status, b, err := sockRequest("DELETE", "/volumes/"+v.Name, nil)
if err != nil {
errors = append(errors, err.Error())
continue
}
if status != http.StatusNoContent {
errors = append(errors, fmt.Sprintf("error deleting volume %s: %s", v.Name, string(b)))
}
}
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, "\n"))
}
return nil
}
func getAllVolumes() ([]*types.Volume, error) {
var volumes types.VolumesListResponse
_, b, err := sockRequest("GET", "/volumes", nil)
if err != nil {
return nil, err
}
if err := json.Unmarshal(b, &volumes); err != nil {
return nil, err
}
return volumes.Volumes, nil
}
var protectedImages = map[string]struct{}{}
func init() {

View file

@ -0,0 +1,51 @@
% DOCKER(1) Docker User Manuals
% Docker Community
% JULY 2015
# NAME
docker-volume-create - Create a new volume
# SYNOPSIS
**docker volume create**
[**-d**|**--driver**[=*local*]]
[**--name**[=**]]
[**-o**|**--opt**[=**]]
[OPTIONS]
# DESCRIPTION
Creates a new volume that containers can consume and store data in. If a name is not specified, Docker generates a random name. You create a volume and then configure the container to use it, for example:
```
$ docker volume create --name hello
hello
$ docker run -d -v hello:/world busybox ls /world
```
The mount is created inside the container's `/src` directory. Docker doesn't not support relative paths for mount points inside the container.
Multiple containers can use the same volume in the same time period. This is useful if two containers need access to shared data. For example, if one container writes and the other reads the data.
## Driver specific options
Some volume drivers may take options to customize the volume creation. Use the `-o` or `--opt` flags to pass driver options:
```
$ docker volume create --driver fake --opt tardis=blue --opt timey=wimey
```
These options are passed directly to the volume driver. Options for
different volume drivers may do different things (or nothing at all).
*Note*: The built-in `local` volume driver does not currently accept any options.
# OPTIONS
**-d**, **--driver**=[]
Specify volume driver name
**--name**=""
Specify volume name
**-o**, **--opt**=map[]
Set driver specific options
# HISTORY
July 2015, created by Brian Goff <cpuguy83@gmail.com>

View file

@ -0,0 +1,26 @@
% DOCKER(1) Docker User Manuals
% Docker Community
% JULY 2015
# NAME
docker-volume-inspect - Get low-level information about a volume
# SYNOPSIS
**docker volume inspect**
[**-f**|**--format**[=**]]
[OPTIONS] [VOLUME NAME]
# DESCRIPTION
Returns information about a volume. By default, this command renders all results
in a JSON array. You can specify an alternate format to execute a given template
is executed for each result. Go's
http://golang.org/pkg/text/template/ package describes all the details of the
format.
# OPTIONS
**-f**, **--format**=""
Format the output using the given go template.
# HISTORY
July 2015, created by Brian Goff <cpuguy83@gmail.com>

27
man/docker-volume-ls.1.md Normal file
View file

@ -0,0 +1,27 @@
% DOCKER(1) Docker User Manuals
% Docker Community
% JULY 2015
# NAME
docker-volume-ls - List all volumes
# SYNOPSIS
**docker volume ls**
[**-f**|**--filter**[=**]]
[**-q**|**--quiet**[=**]]
[OPTIONS]
# DESCRIPTION
Lists all the volumes Docker knows about. You can filter using the `-f` or `--filter` flag. The filtering format is a `key=value` pair. To specify more than one filter, pass multiple flags (for example, `--filter "foo=bar" --filter "bif=baz"`)
There is a single supported filter `dangling=value` which takes a boolean of `true` or `false`.
# OPTIONS
**-f**, **--filter**=""
Provide filter values (i.e. 'dangling=true')
**-q**, **--quiet**=false
Only display volume names
# HISTORY
July 2015, created by Brian Goff <cpuguy83@gmail.com>

24
man/docker-volume-rm.1.md Normal file
View file

@ -0,0 +1,24 @@
% DOCKER(1) Docker User Manuals
% Docker Community
% JULY 2015
# NAME
docker-volume-rm - Remove a volume
# SYNOPSIS
**docker volume rm**
[OPTIONS] [VOLUME NAME]
# DESCRIPTION
Removes a volume. You cannot remove a volume that is in use by a container.
```
$ docker volume rm hello
hello
```
# OPTIONS
# HISTORY
July 2015, created by Brian Goff <cpuguy83@gmail.com>

View file

@ -131,6 +131,10 @@ func (opts *MapOpts) Set(value string) error {
return nil
}
func (opts *MapOpts) GetAll() map[string]string {
return opts.values
}
func (opts *MapOpts) String() string {
return fmt.Sprintf("%v", map[string]string((opts.values)))
}

View file

@ -15,10 +15,19 @@ import (
)
const (
versionMimetype = "application/vnd.docker.plugins.v1+json"
versionMimetype = "application/vnd.docker.plugins.v1.1+json"
defaultTimeOut = 30
)
type remoteError struct {
method string
err string
}
func (e *remoteError) Error() string {
return fmt.Sprintf("Plugin Error: %s, %s", e.err, e.method)
}
// NewClient creates a new plugin client (http).
func NewClient(addr string, tlsConfig tlsconfig.Options) (*Client, error) {
tr := &http.Transport{}
@ -84,9 +93,9 @@ func (c *Client) callWithRetry(serviceMethod string, args interface{}, ret inter
if resp.StatusCode != http.StatusOK {
remoteErr, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("Plugin Error: %s", err)
return &remoteError{err.Error(), serviceMethod}
}
return fmt.Errorf("Plugin Error: %s", remoteErr)
return &remoteError{string(remoteErr), serviceMethod}
}
return json.NewDecoder(resp.Body).Decode(&ret)

View file

@ -7,7 +7,6 @@ import (
"go/parser"
"go/token"
"reflect"
"strings"
)
var ErrBadReturn = errors.New("found return arg with no name: all args must be named")
@ -39,7 +38,7 @@ type arg struct {
}
func (a *arg) String() string {
return strings.ToLower(a.Name) + " " + strings.ToLower(a.ArgType)
return a.Name + " " + a.ArgType
}
// Parses the given file for an interface definition with the given name

View file

@ -11,8 +11,8 @@ func (a *volumeDriverAdapter) Name() string {
return a.name
}
func (a *volumeDriverAdapter) Create(name string) (volume.Volume, error) {
err := a.proxy.Create(name)
func (a *volumeDriverAdapter) Create(name string, opts map[string]string) (volume.Volume, error) {
err := a.proxy.Create(name, opts)
if err != nil {
return nil, err
}
@ -33,6 +33,11 @@ type volumeAdapter struct {
eMount string // ephemeral host volume path
}
type proxyVolume struct {
Name string
Mountpoint string
}
func (a *volumeAdapter) Name() string {
return a.name
}

View file

@ -1,25 +0,0 @@
//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type VolumeDriver -name VolumeDriver
package volumedrivers
import "github.com/docker/docker/volume"
// NewVolumeDriver returns a driver has the given name mapped on the given client.
func NewVolumeDriver(name string, c client) volume.Driver {
proxy := &volumeDriverProxy{c}
return &volumeDriverAdapter{name, proxy}
}
// VolumeDriver defines the available functions that volume plugins must implement.
type VolumeDriver interface {
// Create a volume with the given name
Create(name string) (err error)
// Remove the volume with the given name
Remove(name string) (err error)
// Get the mountpoint of the given volume
Path(name string) (mountpoint string, err error)
// Mount the given volume and return the mountpoint
Mount(name string) (mountpoint string, err error)
// Unmount the given volume
Unmount(name string) (err error)
}

View file

@ -1,3 +1,5 @@
//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type VolumeDriver -name VolumeDriver
package volumedrivers
import (
@ -13,6 +15,28 @@ import (
var drivers = &driverExtpoint{extensions: make(map[string]volume.Driver)}
// NewVolumeDriver returns a driver has the given name mapped on the given client.
func NewVolumeDriver(name string, c client) volume.Driver {
proxy := &volumeDriverProxy{c}
return &volumeDriverAdapter{name, proxy}
}
type opts map[string]string
// VolumeDriver defines the available functions that volume plugins must implement.
type VolumeDriver interface {
// Create a volume with the given name
Create(name string, opts opts) (err error)
// Remove the volume with the given name
Remove(name string) (err error)
// Get the mountpoint of the given volume
Path(name string) (mountpoint string, err error)
// Mount the given volume and return the mountpoint
Mount(name string) (mountpoint string, err error)
// Unmount the given volume
Unmount(name string) (err error)
}
type driverExtpoint struct {
extensions map[string]volume.Driver
sync.Mutex

View file

@ -14,19 +14,21 @@ type volumeDriverProxy struct {
type volumeDriverProxyCreateRequest struct {
Name string
Opts opts
}
type volumeDriverProxyCreateResponse struct {
Err string
}
func (pp *volumeDriverProxy) Create(name string) (err error) {
func (pp *volumeDriverProxy) Create(name string, opts opts) (err error) {
var (
req volumeDriverProxyCreateRequest
ret volumeDriverProxyCreateResponse
)
req.Name = name
req.Opts = opts
if err = pp.Call("VolumeDriver.Create", req, &ret); err != nil {
return
}

View file

@ -50,7 +50,7 @@ func TestVolumeRequestError(t *testing.T) {
driver := volumeDriverProxy{client}
if err = driver.Create("volume"); err == nil {
if err = driver.Create("volume", nil); err == nil {
t.Fatal("Expected error, was nil")
}

View file

@ -23,7 +23,11 @@ const (
volumesPathName = "volumes"
)
var oldVfsDir = filepath.Join("vfs", "dir")
var (
// ErrNotFound is the typed error returned when the requested volume name can't be found
ErrNotFound = errors.New("volume not found")
oldVfsDir = filepath.Join("vfs", "dir")
)
// New instantiates a new Root instance with the provided scope. Scope
// is the base path that the Root instance uses to store its
@ -54,6 +58,7 @@ func New(scope string) (*Root, error) {
path: r.DataPath(name),
}
}
return r, nil
}
@ -67,6 +72,15 @@ type Root struct {
volumes map[string]*localVolume
}
// List lists all the volumes
func (r *Root) List() []volume.Volume {
var ls []volume.Volume
for _, v := range r.volumes {
ls = append(ls, v)
}
return ls
}
// DataPath returns the constructed path of this volume.
func (r *Root) DataPath(volumeName string) string {
return filepath.Join(r.path, volumeName, VolumeDataPathName)
@ -80,27 +94,28 @@ func (r *Root) Name() string {
// Create creates a new volume.Volume with the provided name, creating
// the underlying directory tree required for this volume in the
// process.
func (r *Root) Create(name string) (volume.Volume, error) {
func (r *Root) Create(name string, _ map[string]string) (volume.Volume, error) {
r.m.Lock()
defer r.m.Unlock()
v, exists := r.volumes[name]
if !exists {
path := r.DataPath(name)
if err := os.MkdirAll(path, 0755); err != nil {
if os.IsExist(err) {
return nil, fmt.Errorf("volume already exists under %s", filepath.Dir(path))
}
return nil, err
}
v = &localVolume{
driverName: r.Name(),
name: name,
path: path,
}
r.volumes[name] = v
if exists {
return v, nil
}
v.use()
path := r.DataPath(name)
if err := os.MkdirAll(path, 0755); err != nil {
if os.IsExist(err) {
return nil, fmt.Errorf("volume already exists under %s", filepath.Dir(path))
}
return nil, err
}
v = &localVolume{
driverName: r.Name(),
name: name,
path: path,
}
r.volumes[name] = v
return v, nil
}
@ -115,24 +130,32 @@ func (r *Root) Remove(v volume.Volume) error {
if !ok {
return errors.New("unknown volume type")
}
lv.release()
if lv.usedCount == 0 {
realPath, err := filepath.EvalSymlinks(lv.path)
if err != nil {
return err
}
if !r.scopedPath(realPath) {
return fmt.Errorf("Unable to remove a directory of out the Docker root: %s", realPath)
}
if err := os.RemoveAll(realPath); err != nil {
return err
}
delete(r.volumes, lv.name)
return os.RemoveAll(filepath.Dir(lv.path))
realPath, err := filepath.EvalSymlinks(lv.path)
if err != nil {
return err
}
return nil
if !r.scopedPath(realPath) {
return fmt.Errorf("Unable to remove a directory of out the Docker root: %s", realPath)
}
if err := os.RemoveAll(realPath); err != nil {
return err
}
delete(r.volumes, lv.name)
return os.RemoveAll(filepath.Dir(lv.path))
}
// Get looks up the volume for the given name and returns it if found
func (r *Root) Get(name string) (volume.Volume, error) {
r.m.Lock()
v, exists := r.volumes[name]
r.m.Unlock()
if !exists {
return nil, ErrNotFound
}
return v, nil
}
// scopedPath verifies that the path where the volume is located
@ -188,15 +211,3 @@ func (v *localVolume) Mount() (string, error) {
func (v *localVolume) Unmount() error {
return nil
}
func (v *localVolume) use() {
v.m.Lock()
v.usedCount++
v.m.Unlock()
}
func (v *localVolume) release() {
v.m.Lock()
v.usedCount--
v.m.Unlock()
}

View file

@ -9,7 +9,7 @@ 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)
Create(name string, opts map[string]string) (Volume, error)
// Remove deletes the volume.
Remove(Volume) error
}