summaryrefslogtreecommitdiff
path: root/volume
diff options
context:
space:
mode:
authorhwajeong.son <hwajeong.son@samsung.com>2018-08-20 13:30:55 +0900
committerhwajeong.son <hwajeong.son@samsung.com>2018-08-20 13:30:55 +0900
commit0b51891e5977b87f986f4db2cbbe09295cfdbedc (patch)
treec35ac732cb1dffccee5a32131431f753481077c2 /volume
parenteea0e89806b2cf59af3dccabc67014bd19b91b82 (diff)
downloaddocker-engine-master.tar.gz
docker-engine-master.tar.bz2
docker-engine-master.zip
Signed-off-by: hwajeong.son <hwajeong.son@samsung.com>
Diffstat (limited to 'volume')
-rw-r--r--volume/drivers/adapter.go184
-rw-r--r--volume/drivers/extpoint.go217
-rw-r--r--volume/drivers/extpoint_test.go23
-rw-r--r--volume/drivers/proxy.go242
-rw-r--r--volume/drivers/proxy_test.go132
-rw-r--r--volume/local/local.go387
-rw-r--r--volume/local/local_test.go345
-rw-r--r--volume/local/local_unix.go99
-rw-r--r--volume/local/local_windows.go46
-rw-r--r--volume/store/db.go88
-rw-r--r--volume/store/errors.go76
-rw-r--r--volume/store/restore.go83
-rw-r--r--volume/store/store.go669
-rw-r--r--volume/store/store_test.go234
-rw-r--r--volume/store/store_unix.go9
-rw-r--r--volume/store/store_windows.go12
-rw-r--r--volume/testutils/testutils.go123
-rw-r--r--volume/validate.go140
-rw-r--r--volume/validate_test.go43
-rw-r--r--volume/validate_test_unix.go8
-rw-r--r--volume/validate_test_windows.go6
-rw-r--r--volume/volume.go374
-rw-r--r--volume/volume_copy.go23
-rw-r--r--volume/volume_copy_unix.go8
-rw-r--r--volume/volume_copy_windows.go6
-rw-r--r--volume/volume_linux.go56
-rw-r--r--volume/volume_linux_test.go51
-rw-r--r--volume/volume_propagation_linux.go47
-rw-r--r--volume/volume_propagation_linux_test.go65
-rw-r--r--volume/volume_propagation_unsupported.go24
-rw-r--r--volume/volume_test.go269
-rw-r--r--volume/volume_unix.go148
-rw-r--r--volume/volume_unsupported.go16
-rw-r--r--volume/volume_windows.go201
34 files changed, 4454 insertions, 0 deletions
diff --git a/volume/drivers/adapter.go b/volume/drivers/adapter.go
new file mode 100644
index 0000000..0ec68da
--- /dev/null
+++ b/volume/drivers/adapter.go
@@ -0,0 +1,184 @@
+package volumedrivers
+
+import (
+ "errors"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/docker/docker/volume"
+)
+
+var (
+ errNoSuchVolume = errors.New("no such volume")
+)
+
+type volumeDriverAdapter struct {
+ name string
+ baseHostPath string
+ capabilities *volume.Capability
+ proxy *volumeDriverProxy
+}
+
+func (a *volumeDriverAdapter) Name() string {
+ return a.name
+}
+
+func (a *volumeDriverAdapter) Create(name string, opts map[string]string) (volume.Volume, error) {
+ if err := a.proxy.Create(name, opts); err != nil {
+ return nil, err
+ }
+ return &volumeAdapter{
+ proxy: a.proxy,
+ name: name,
+ driverName: a.name,
+ baseHostPath: a.baseHostPath,
+ }, nil
+}
+
+func (a *volumeDriverAdapter) Remove(v volume.Volume) error {
+ return a.proxy.Remove(v.Name())
+}
+
+func hostPath(baseHostPath, path string) string {
+ if baseHostPath != "" {
+ path = filepath.Join(baseHostPath, path)
+ }
+ return path
+}
+
+func (a *volumeDriverAdapter) List() ([]volume.Volume, error) {
+ ls, err := a.proxy.List()
+ if err != nil {
+ return nil, err
+ }
+
+ var out []volume.Volume
+ for _, vp := range ls {
+ out = append(out, &volumeAdapter{
+ proxy: a.proxy,
+ name: vp.Name,
+ baseHostPath: a.baseHostPath,
+ driverName: a.name,
+ eMount: hostPath(a.baseHostPath, vp.Mountpoint),
+ })
+ }
+ return out, nil
+}
+
+func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) {
+ v, err := a.proxy.Get(name)
+ if err != nil {
+ return nil, err
+ }
+
+ // plugin may have returned no volume and no error
+ if v == nil {
+ return nil, errNoSuchVolume
+ }
+
+ return &volumeAdapter{
+ proxy: a.proxy,
+ name: v.Name,
+ driverName: a.Name(),
+ eMount: v.Mountpoint,
+ createdAt: v.CreatedAt,
+ status: v.Status,
+ baseHostPath: a.baseHostPath,
+ }, nil
+}
+
+func (a *volumeDriverAdapter) Scope() string {
+ cap := a.getCapabilities()
+ return cap.Scope
+}
+
+func (a *volumeDriverAdapter) getCapabilities() volume.Capability {
+ if a.capabilities != nil {
+ return *a.capabilities
+ }
+ cap, err := a.proxy.Capabilities()
+ if err != nil {
+ // `GetCapabilities` is a not a required endpoint.
+ // On error assume it's a local-only driver
+ logrus.Warnf("Volume driver %s returned an error while trying to query its capabilities, using default capabilties: %v", a.name, err)
+ return volume.Capability{Scope: volume.LocalScope}
+ }
+
+ // don't spam the warn log below just because the plugin didn't provide a scope
+ if len(cap.Scope) == 0 {
+ cap.Scope = volume.LocalScope
+ }
+
+ cap.Scope = strings.ToLower(cap.Scope)
+ if cap.Scope != volume.LocalScope && cap.Scope != volume.GlobalScope {
+ logrus.Warnf("Volume driver %q returned an invalid scope: %q", a.Name(), cap.Scope)
+ cap.Scope = volume.LocalScope
+ }
+
+ a.capabilities = &cap
+ return cap
+}
+
+type volumeAdapter struct {
+ proxy *volumeDriverProxy
+ name string
+ baseHostPath string
+ driverName string
+ eMount string // ephemeral host volume path
+ createdAt time.Time // time the directory was created
+ status map[string]interface{}
+}
+
+type proxyVolume struct {
+ Name string
+ Mountpoint string
+ CreatedAt time.Time
+ Status map[string]interface{}
+}
+
+func (a *volumeAdapter) Name() string {
+ return a.name
+}
+
+func (a *volumeAdapter) DriverName() string {
+ return a.driverName
+}
+
+func (a *volumeAdapter) Path() string {
+ if len(a.eMount) == 0 {
+ mountpoint, _ := a.proxy.Path(a.name)
+ a.eMount = hostPath(a.baseHostPath, mountpoint)
+ }
+ return a.eMount
+}
+
+func (a *volumeAdapter) CachedPath() string {
+ return a.eMount
+}
+
+func (a *volumeAdapter) Mount(id string) (string, error) {
+ mountpoint, err := a.proxy.Mount(a.name, id)
+ a.eMount = hostPath(a.baseHostPath, mountpoint)
+ return a.eMount, err
+}
+
+func (a *volumeAdapter) Unmount(id string) error {
+ err := a.proxy.Unmount(a.name, id)
+ if err == nil {
+ a.eMount = ""
+ }
+ return err
+}
+
+func (a *volumeAdapter) CreatedAt() (time.Time, error) {
+ return a.createdAt, nil
+}
+func (a *volumeAdapter) Status() map[string]interface{} {
+ out := make(map[string]interface{}, len(a.status))
+ for k, v := range a.status {
+ out[k] = v
+ }
+ return out
+}
diff --git a/volume/drivers/extpoint.go b/volume/drivers/extpoint.go
new file mode 100644
index 0000000..da230dc
--- /dev/null
+++ b/volume/drivers/extpoint.go
@@ -0,0 +1,217 @@
+//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type volumeDriver -name VolumeDriver
+
+package volumedrivers
+
+import (
+ "fmt"
+ "sort"
+ "sync"
+
+ "github.com/docker/docker/pkg/locker"
+ getter "github.com/docker/docker/pkg/plugingetter"
+ "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),
+ driverLock: &locker.Locker{},
+}
+
+const extName = "VolumeDriver"
+
+// NewVolumeDriver returns a driver has the given name mapped on the given client.
+func NewVolumeDriver(name string, baseHostPath string, c client) volume.Driver {
+ proxy := &volumeDriverProxy{c}
+ return &volumeDriverAdapter{name: name, baseHostPath: baseHostPath, proxy: proxy}
+}
+
+// volumeDriver defines the available functions that volume plugins must implement.
+// This interface is only defined to generate the proxy objects.
+// It's not intended to be public or reused.
+type volumeDriver interface {
+ // Create a volume with the given name
+ Create(name string, opts map[string]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, id string) (mountpoint string, err error)
+ // Unmount the given volume
+ Unmount(name, id string) (err error)
+ // List lists all the volumes known to the driver
+ List() (volumes []*proxyVolume, err error)
+ // Get retrieves the volume with the requested name
+ Get(name string) (volume *proxyVolume, err error)
+ // Capabilities gets the list of capabilities of the driver
+ Capabilities() (capabilities volume.Capability, err error)
+}
+
+type driverExtpoint struct {
+ extensions map[string]volume.Driver
+ sync.Mutex
+ driverLock *locker.Locker
+ plugingetter getter.PluginGetter
+}
+
+// RegisterPluginGetter sets the plugingetter
+func RegisterPluginGetter(plugingetter getter.PluginGetter) {
+ drivers.plugingetter = plugingetter
+}
+
+// Register associates the given driver to the given name, checking if
+// the name is already associated
+func Register(extension volume.Driver, name string) bool {
+ if name == "" {
+ return false
+ }
+
+ drivers.Lock()
+ defer drivers.Unlock()
+
+ _, exists := drivers.extensions[name]
+ if exists {
+ return false
+ }
+
+ if err := validateDriver(extension); err != nil {
+ return false
+ }
+
+ drivers.extensions[name] = extension
+
+ return true
+}
+
+// Unregister dissociates the name from its driver, if the association exists.
+func Unregister(name string) bool {
+ drivers.Lock()
+ defer drivers.Unlock()
+
+ _, exists := drivers.extensions[name]
+ if !exists {
+ return false
+ }
+ delete(drivers.extensions, name)
+ return true
+}
+
+// lookup returns the driver associated with the given name. If a
+// driver with the given name has not been registered it checks if
+// there is a VolumeDriver plugin available with the given name.
+func lookup(name string, mode int) (volume.Driver, error) {
+ drivers.driverLock.Lock(name)
+ defer drivers.driverLock.Unlock(name)
+
+ drivers.Lock()
+ ext, ok := drivers.extensions[name]
+ drivers.Unlock()
+ if ok {
+ return ext, nil
+ }
+ if drivers.plugingetter != nil {
+ p, err := drivers.plugingetter.Get(name, extName, mode)
+ if err != nil {
+ return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err)
+ }
+
+ d := NewVolumeDriver(p.Name(), p.BasePath(), p.Client())
+ if err := validateDriver(d); err != nil {
+ return nil, err
+ }
+
+ if p.IsV1() {
+ drivers.Lock()
+ drivers.extensions[name] = d
+ drivers.Unlock()
+ }
+ return d, nil
+ }
+ return nil, fmt.Errorf("Error looking up volume plugin %s", name)
+}
+
+func validateDriver(vd volume.Driver) error {
+ scope := vd.Scope()
+ if scope != volume.LocalScope && scope != volume.GlobalScope {
+ return fmt.Errorf("Driver %q provided an invalid capability scope: %s", vd.Name(), scope)
+ }
+ return nil
+}
+
+// GetDriver returns a volume driver by its name.
+// If the driver is empty, it looks for the local driver.
+func GetDriver(name string) (volume.Driver, error) {
+ if name == "" {
+ name = volume.DefaultDriverName
+ }
+ return lookup(name, getter.Lookup)
+}
+
+// CreateDriver returns a volume driver by its name and increments RefCount.
+// If the driver is empty, it looks for the local driver.
+func CreateDriver(name string) (volume.Driver, error) {
+ if name == "" {
+ name = volume.DefaultDriverName
+ }
+ return lookup(name, getter.Acquire)
+}
+
+// RemoveDriver returns a volume driver by its name and decrements RefCount..
+// If the driver is empty, it looks for the local driver.
+func RemoveDriver(name string) (volume.Driver, error) {
+ if name == "" {
+ name = volume.DefaultDriverName
+ }
+ return lookup(name, getter.Release)
+}
+
+// GetDriverList returns list of volume drivers registered.
+// If no driver is registered, empty string list will be returned.
+func GetDriverList() []string {
+ var driverList []string
+ drivers.Lock()
+ for driverName := range drivers.extensions {
+ driverList = append(driverList, driverName)
+ }
+ drivers.Unlock()
+ sort.Strings(driverList)
+ return driverList
+}
+
+// GetAllDrivers lists all the registered drivers
+func GetAllDrivers() ([]volume.Driver, error) {
+ var plugins []getter.CompatPlugin
+ if drivers.plugingetter != nil {
+ var err error
+ plugins, err = drivers.plugingetter.GetAllByCap(extName)
+ if err != nil {
+ return nil, fmt.Errorf("error listing plugins: %v", err)
+ }
+ }
+ var ds []volume.Driver
+
+ drivers.Lock()
+ defer drivers.Unlock()
+
+ for _, d := range drivers.extensions {
+ ds = append(ds, d)
+ }
+
+ for _, p := range plugins {
+ name := p.Name()
+
+ if _, ok := drivers.extensions[name]; ok {
+ continue
+ }
+
+ ext := NewVolumeDriver(name, p.BasePath(), p.Client())
+ if p.IsV1() {
+ drivers.extensions[name] = ext
+ }
+ ds = append(ds, ext)
+ }
+ return ds, nil
+}
diff --git a/volume/drivers/extpoint_test.go b/volume/drivers/extpoint_test.go
new file mode 100644
index 0000000..428b075
--- /dev/null
+++ b/volume/drivers/extpoint_test.go
@@ -0,0 +1,23 @@
+package volumedrivers
+
+import (
+ "testing"
+
+ volumetestutils "github.com/docker/docker/volume/testutils"
+)
+
+func TestGetDriver(t *testing.T) {
+ _, err := GetDriver("missing")
+ if err == nil {
+ t.Fatal("Expected error, was nil")
+ }
+ Register(volumetestutils.NewFakeDriver("fake"), "fake")
+
+ d, err := GetDriver("fake")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if d.Name() != "fake" {
+ t.Fatalf("Expected fake driver, got %s\n", d.Name())
+ }
+}
diff --git a/volume/drivers/proxy.go b/volume/drivers/proxy.go
new file mode 100644
index 0000000..b23db62
--- /dev/null
+++ b/volume/drivers/proxy.go
@@ -0,0 +1,242 @@
+// generated code - DO NOT EDIT
+
+package volumedrivers
+
+import (
+ "errors"
+
+ "github.com/docker/docker/volume"
+)
+
+type client interface {
+ Call(string, interface{}, interface{}) error
+}
+
+type volumeDriverProxy struct {
+ client
+}
+
+type volumeDriverProxyCreateRequest struct {
+ Name string
+ Opts map[string]string
+}
+
+type volumeDriverProxyCreateResponse struct {
+ Err string
+}
+
+func (pp *volumeDriverProxy) Create(name string, opts map[string]string) (err error) {
+ var (
+ req volumeDriverProxyCreateRequest
+ ret volumeDriverProxyCreateResponse
+ )
+
+ req.Name = name
+ req.Opts = opts
+ if err = pp.Call("VolumeDriver.Create", req, &ret); err != nil {
+ return
+ }
+
+ if ret.Err != "" {
+ err = errors.New(ret.Err)
+ }
+
+ return
+}
+
+type volumeDriverProxyRemoveRequest struct {
+ Name string
+}
+
+type volumeDriverProxyRemoveResponse struct {
+ Err string
+}
+
+func (pp *volumeDriverProxy) Remove(name string) (err error) {
+ var (
+ req volumeDriverProxyRemoveRequest
+ ret volumeDriverProxyRemoveResponse
+ )
+
+ req.Name = name
+ if err = pp.Call("VolumeDriver.Remove", req, &ret); err != nil {
+ return
+ }
+
+ if ret.Err != "" {
+ err = errors.New(ret.Err)
+ }
+
+ return
+}
+
+type volumeDriverProxyPathRequest struct {
+ Name string
+}
+
+type volumeDriverProxyPathResponse struct {
+ Mountpoint string
+ Err string
+}
+
+func (pp *volumeDriverProxy) Path(name string) (mountpoint string, err error) {
+ var (
+ req volumeDriverProxyPathRequest
+ ret volumeDriverProxyPathResponse
+ )
+
+ req.Name = name
+ if err = pp.Call("VolumeDriver.Path", req, &ret); err != nil {
+ return
+ }
+
+ mountpoint = ret.Mountpoint
+
+ if ret.Err != "" {
+ err = errors.New(ret.Err)
+ }
+
+ return
+}
+
+type volumeDriverProxyMountRequest struct {
+ Name string
+ ID string
+}
+
+type volumeDriverProxyMountResponse struct {
+ Mountpoint string
+ Err string
+}
+
+func (pp *volumeDriverProxy) Mount(name string, id string) (mountpoint string, err error) {
+ var (
+ req volumeDriverProxyMountRequest
+ ret volumeDriverProxyMountResponse
+ )
+
+ req.Name = name
+ req.ID = id
+ if err = pp.Call("VolumeDriver.Mount", req, &ret); err != nil {
+ return
+ }
+
+ mountpoint = ret.Mountpoint
+
+ if ret.Err != "" {
+ err = errors.New(ret.Err)
+ }
+
+ return
+}
+
+type volumeDriverProxyUnmountRequest struct {
+ Name string
+ ID string
+}
+
+type volumeDriverProxyUnmountResponse struct {
+ Err string
+}
+
+func (pp *volumeDriverProxy) Unmount(name string, id string) (err error) {
+ var (
+ req volumeDriverProxyUnmountRequest
+ ret volumeDriverProxyUnmountResponse
+ )
+
+ req.Name = name
+ req.ID = id
+ if err = pp.Call("VolumeDriver.Unmount", req, &ret); err != nil {
+ return
+ }
+
+ if ret.Err != "" {
+ err = errors.New(ret.Err)
+ }
+
+ return
+}
+
+type volumeDriverProxyListRequest struct {
+}
+
+type volumeDriverProxyListResponse struct {
+ Volumes []*proxyVolume
+ Err string
+}
+
+func (pp *volumeDriverProxy) List() (volumes []*proxyVolume, err error) {
+ var (
+ req volumeDriverProxyListRequest
+ ret volumeDriverProxyListResponse
+ )
+
+ if err = pp.Call("VolumeDriver.List", req, &ret); err != nil {
+ return
+ }
+
+ volumes = ret.Volumes
+
+ if ret.Err != "" {
+ err = errors.New(ret.Err)
+ }
+
+ return
+}
+
+type volumeDriverProxyGetRequest struct {
+ Name string
+}
+
+type volumeDriverProxyGetResponse struct {
+ Volume *proxyVolume
+ Err string
+}
+
+func (pp *volumeDriverProxy) Get(name string) (volume *proxyVolume, err error) {
+ var (
+ req volumeDriverProxyGetRequest
+ ret volumeDriverProxyGetResponse
+ )
+
+ req.Name = name
+ if err = pp.Call("VolumeDriver.Get", req, &ret); err != nil {
+ return
+ }
+
+ volume = ret.Volume
+
+ if ret.Err != "" {
+ err = errors.New(ret.Err)
+ }
+
+ return
+}
+
+type volumeDriverProxyCapabilitiesRequest struct {
+}
+
+type volumeDriverProxyCapabilitiesResponse struct {
+ Capabilities volume.Capability
+ Err string
+}
+
+func (pp *volumeDriverProxy) Capabilities() (capabilities volume.Capability, err error) {
+ var (
+ req volumeDriverProxyCapabilitiesRequest
+ ret volumeDriverProxyCapabilitiesResponse
+ )
+
+ if err = pp.Call("VolumeDriver.Capabilities", req, &ret); err != nil {
+ return
+ }
+
+ capabilities = ret.Capabilities
+
+ if ret.Err != "" {
+ err = errors.New(ret.Err)
+ }
+
+ return
+}
diff --git a/volume/drivers/proxy_test.go b/volume/drivers/proxy_test.go
new file mode 100644
index 0000000..b78c46a
--- /dev/null
+++ b/volume/drivers/proxy_test.go
@@ -0,0 +1,132 @@
+package volumedrivers
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/pkg/plugins"
+ "github.com/docker/go-connections/tlsconfig"
+)
+
+func TestVolumeRequestError(t *testing.T) {
+ mux := http.NewServeMux()
+ server := httptest.NewServer(mux)
+ defer server.Close()
+
+ mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+ fmt.Fprintln(w, `{"Err": "Cannot create volume"}`)
+ })
+
+ mux.HandleFunc("/VolumeDriver.Remove", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+ fmt.Fprintln(w, `{"Err": "Cannot remove volume"}`)
+ })
+
+ mux.HandleFunc("/VolumeDriver.Mount", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+ fmt.Fprintln(w, `{"Err": "Cannot mount volume"}`)
+ })
+
+ mux.HandleFunc("/VolumeDriver.Unmount", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+ fmt.Fprintln(w, `{"Err": "Cannot unmount volume"}`)
+ })
+
+ mux.HandleFunc("/VolumeDriver.Path", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+ fmt.Fprintln(w, `{"Err": "Unknown volume"}`)
+ })
+
+ mux.HandleFunc("/VolumeDriver.List", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+ fmt.Fprintln(w, `{"Err": "Cannot list volumes"}`)
+ })
+
+ mux.HandleFunc("/VolumeDriver.Get", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+ fmt.Fprintln(w, `{"Err": "Cannot get volume"}`)
+ })
+
+ mux.HandleFunc("/VolumeDriver.Capabilities", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
+ http.Error(w, "error", 500)
+ })
+
+ u, _ := url.Parse(server.URL)
+ client, err := plugins.NewClient("tcp://"+u.Host, &tlsconfig.Options{InsecureSkipVerify: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ driver := volumeDriverProxy{client}
+
+ if err = driver.Create("volume", nil); err == nil {
+ t.Fatal("Expected error, was nil")
+ }
+
+ if !strings.Contains(err.Error(), "Cannot create volume") {
+ t.Fatalf("Unexpected error: %v\n", err)
+ }
+
+ _, err = driver.Mount("volume", "123")
+ if err == nil {
+ t.Fatal("Expected error, was nil")
+ }
+
+ if !strings.Contains(err.Error(), "Cannot mount volume") {
+ t.Fatalf("Unexpected error: %v\n", err)
+ }
+
+ err = driver.Unmount("volume", "123")
+ if err == nil {
+ t.Fatal("Expected error, was nil")
+ }
+
+ if !strings.Contains(err.Error(), "Cannot unmount volume") {
+ t.Fatalf("Unexpected error: %v\n", err)
+ }
+
+ err = driver.Remove("volume")
+ if err == nil {
+ t.Fatal("Expected error, was nil")
+ }
+
+ if !strings.Contains(err.Error(), "Cannot remove volume") {
+ t.Fatalf("Unexpected error: %v\n", err)
+ }
+
+ _, err = driver.Path("volume")
+ if err == nil {
+ t.Fatal("Expected error, was nil")
+ }
+
+ if !strings.Contains(err.Error(), "Unknown volume") {
+ t.Fatalf("Unexpected error: %v\n", err)
+ }
+
+ _, err = driver.List()
+ if err == nil {
+ t.Fatal("Expected error, was nil")
+ }
+ if !strings.Contains(err.Error(), "Cannot list volumes") {
+ t.Fatalf("Unexpected error: %v\n", err)
+ }
+
+ _, err = driver.Get("volume")
+ if err == nil {
+ t.Fatal("Expected error, was nil")
+ }
+ if !strings.Contains(err.Error(), "Cannot get volume") {
+ t.Fatalf("Unexpected error: %v\n", err)
+ }
+
+ _, err = driver.Capabilities()
+ if err == nil {
+ t.Fatal(err)
+ }
+}
diff --git a/volume/local/local.go b/volume/local/local.go
new file mode 100644
index 0000000..43ba1e1
--- /dev/null
+++ b/volume/local/local.go
@@ -0,0 +1,387 @@
+// Package local provides the default implementation for volumes. It
+// is used to mount data volume containers and directories local to
+// the host server.
+package local
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "sync"
+
+ "github.com/pkg/errors"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/docker/docker/api"
+ "github.com/docker/docker/pkg/idtools"
+ "github.com/docker/docker/pkg/mount"
+ "github.com/docker/docker/volume"
+)
+
+// VolumeDataPathName is the name of the directory where the volume data is stored.
+// It uses a very distinctive name to avoid collisions migrating data between
+// Docker versions.
+const (
+ VolumeDataPathName = "_data"
+ volumesPathName = "volumes"
+)
+
+var (
+ // ErrNotFound is the typed error returned when the requested volume name can't be found
+ ErrNotFound = fmt.Errorf("volume not found")
+ // volumeNameRegex ensures the name assigned for the volume is valid.
+ // This name is used to create the bind directory, so we need to avoid characters that
+ // would make the path to escape the root directory.
+ volumeNameRegex = api.RestrictedNamePattern
+)
+
+type validationError struct {
+ error
+}
+
+func (validationError) IsValidationError() bool {
+ return true
+}
+
+type activeMount struct {
+ count uint64
+ mounted bool
+}
+
+// New instantiates a new Root instance with the provided scope. Scope
+// is the base path that the Root instance uses to store its
+// volumes. The base path is created here if it does not exist.
+func New(scope string, rootIDs idtools.IDPair) (*Root, error) {
+ rootDirectory := filepath.Join(scope, volumesPathName)
+
+ if err := idtools.MkdirAllAndChown(rootDirectory, 0700, rootIDs); err != nil {
+ return nil, err
+ }
+
+ r := &Root{
+ scope: scope,
+ path: rootDirectory,
+ volumes: make(map[string]*localVolume),
+ rootIDs: rootIDs,
+ }
+
+ dirs, err := ioutil.ReadDir(rootDirectory)
+ if err != nil {
+ return nil, err
+ }
+
+ mountInfos, err := mount.GetMounts()
+ if err != nil {
+ logrus.Debugf("error looking up mounts for local volume cleanup: %v", err)
+ }
+
+ for _, d := range dirs {
+ if !d.IsDir() {
+ continue
+ }
+
+ name := filepath.Base(d.Name())
+ v := &localVolume{
+ driverName: r.Name(),
+ name: name,
+ path: r.DataPath(name),
+ }
+ r.volumes[name] = v
+ optsFilePath := filepath.Join(rootDirectory, name, "opts.json")
+ if b, err := ioutil.ReadFile(optsFilePath); err == nil {
+ opts := optsConfig{}
+ if err := json.Unmarshal(b, &opts); err != nil {
+ return nil, errors.Wrapf(err, "error while unmarshaling volume options for volume: %s", name)
+ }
+ // Make sure this isn't an empty optsConfig.
+ // This could be empty due to buggy behavior in older versions of Docker.
+ if !reflect.DeepEqual(opts, optsConfig{}) {
+ v.opts = &opts
+ }
+
+ // unmount anything that may still be mounted (for example, from an unclean shutdown)
+ for _, info := range mountInfos {
+ if info.Mountpoint == v.path {
+ mount.Unmount(v.path)
+ break
+ }
+ }
+ }
+ }
+
+ return r, nil
+}
+
+// Root implements the Driver interface for the volume package and
+// manages the creation/removal of volumes. It uses only standard vfs
+// commands to create/remove dirs within its provided scope.
+type Root struct {
+ m sync.Mutex
+ scope string
+ path string
+ volumes map[string]*localVolume
+ rootIDs idtools.IDPair
+}
+
+// List lists all the volumes
+func (r *Root) List() ([]volume.Volume, error) {
+ var ls []volume.Volume
+ r.m.Lock()
+ for _, v := range r.volumes {
+ ls = append(ls, v)
+ }
+ r.m.Unlock()
+ return ls, nil
+}
+
+// DataPath returns the constructed path of this volume.
+func (r *Root) DataPath(volumeName string) string {
+ return filepath.Join(r.path, volumeName, VolumeDataPathName)
+}
+
+// Name returns the name of Root, defined in the volume package in the DefaultDriverName constant.
+func (r *Root) Name() string {
+ return volume.DefaultDriverName
+}
+
+// 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, opts map[string]string) (volume.Volume, error) {
+ if err := r.validateName(name); err != nil {
+ return nil, err
+ }
+
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ v, exists := r.volumes[name]
+ if exists {
+ return v, nil
+ }
+
+ path := r.DataPath(name)
+ if err := idtools.MkdirAllAndChown(path, 0755, r.rootIDs); err != nil {
+ if os.IsExist(err) {
+ return nil, fmt.Errorf("volume already exists under %s", filepath.Dir(path))
+ }
+ return nil, errors.Wrapf(err, "error while creating volume path '%s'", path)
+ }
+
+ var err error
+ defer func() {
+ if err != nil {
+ os.RemoveAll(filepath.Dir(path))
+ }
+ }()
+
+ v = &localVolume{
+ driverName: r.Name(),
+ name: name,
+ path: path,
+ }
+
+ if len(opts) != 0 {
+ if err = setOpts(v, opts); err != nil {
+ return nil, err
+ }
+ var b []byte
+ b, err = json.Marshal(v.opts)
+ if err != nil {
+ return nil, err
+ }
+ if err = ioutil.WriteFile(filepath.Join(filepath.Dir(path), "opts.json"), b, 600); err != nil {
+ return nil, errors.Wrap(err, "error while persisting volume options")
+ }
+ }
+
+ r.volumes[name] = v
+ return v, nil
+}
+
+// Remove removes the specified volume and all underlying data. If the
+// given volume does not belong to this driver and an error is
+// returned. The volume is reference counted, if all references are
+// not released then the volume is not removed.
+func (r *Root) Remove(v volume.Volume) error {
+ r.m.Lock()
+ defer r.m.Unlock()
+
+ lv, ok := v.(*localVolume)
+ if !ok {
+ return fmt.Errorf("unknown volume type %T", v)
+ }
+
+ if lv.active.count > 0 {
+ return fmt.Errorf("volume has active mounts")
+ }
+
+ if err := lv.unmount(); err != nil {
+ return err
+ }
+
+ realPath, err := filepath.EvalSymlinks(lv.path)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return err
+ }
+ realPath = filepath.Dir(lv.path)
+ }
+
+ if !r.scopedPath(realPath) {
+ return fmt.Errorf("Unable to remove a directory of out the Docker root %s: %s", r.scope, realPath)
+ }
+
+ if err := removePath(realPath); err != nil {
+ return err
+ }
+
+ delete(r.volumes, lv.name)
+ return removePath(filepath.Dir(lv.path))
+}
+
+func removePath(path string) error {
+ if err := os.RemoveAll(path); err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return errors.Wrapf(err, "error removing volume path '%s'", path)
+ }
+ return nil
+}
+
+// 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
+}
+
+// Scope returns the local volume scope
+func (r *Root) Scope() string {
+ return volume.LocalScope
+}
+
+func (r *Root) validateName(name string) error {
+ if len(name) == 1 {
+ return validationError{fmt.Errorf("volume name is too short, names should be at least two alphanumeric characters")}
+ }
+ if !volumeNameRegex.MatchString(name) {
+ return validationError{fmt.Errorf("%q includes invalid characters for a local volume name, only %q are allowed. If you intended to pass a host directory, use absolute path", name, api.RestrictedNameChars)}
+ }
+ return nil
+}
+
+// localVolume implements the Volume interface from the volume package and
+// represents the volumes created by Root.
+type localVolume struct {
+ m sync.Mutex
+ // 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
+ // opts is the parsed list of options used to create the volume
+ opts *optsConfig
+ // active refcounts the active mounts
+ active activeMount
+}
+
+// Name returns the name of the given Volume.
+func (v *localVolume) Name() string {
+ return v.name
+}
+
+// DriverName returns the driver that created the given Volume.
+func (v *localVolume) DriverName() string {
+ return v.driverName
+}
+
+// Path returns the data location.
+func (v *localVolume) Path() string {
+ return v.path
+}
+
+// Mount implements the localVolume interface, returning the data location.
+// If there are any provided mount options, the resources will be mounted at this point
+func (v *localVolume) Mount(id string) (string, error) {
+ v.m.Lock()
+ defer v.m.Unlock()
+ if v.opts != nil {
+ if !v.active.mounted {
+ if err := v.mount(); err != nil {
+ return "", err
+ }
+ v.active.mounted = true
+ }
+ v.active.count++
+ }
+ return v.path, nil
+}
+
+// Unmount dereferences the id, and if it is the last reference will unmount any resources
+// that were previously mounted.
+func (v *localVolume) Unmount(id string) error {
+ v.m.Lock()
+ defer v.m.Unlock()
+
+ // Always decrement the count, even if the unmount fails
+ // Essentially docker doesn't care if this fails, it will send an error, but
+ // ultimately there's nothing that can be done. If we don't decrement the count
+ // this volume can never be removed until a daemon restart occurs.
+ if v.opts != nil {
+ v.active.count--
+ }
+
+ if v.active.count > 0 {
+ return nil
+ }
+
+ return v.unmount()
+}
+
+func (v *localVolume) unmount() error {
+ if v.opts != nil {
+ if err := mount.Unmount(v.path); err != nil {
+ if mounted, mErr := mount.Mounted(v.path); mounted || mErr != nil {
+ return errors.Wrapf(err, "error while unmounting volume path '%s'", v.path)
+ }
+ }
+ v.active.mounted = false
+ }
+ return nil
+}
+
+func validateOpts(opts map[string]string) error {
+ for opt := range opts {
+ if !validOpts[opt] {
+ return validationError{fmt.Errorf("invalid option key: %q", opt)}
+ }
+ }
+ return nil
+}
+
+func (v *localVolume) Status() map[string]interface{} {
+ return nil
+}
+
+// getAddress finds out address/hostname from options
+func getAddress(opts string) string {
+ optsList := strings.Split(opts, ",")
+ for i := 0; i < len(optsList); i++ {
+ if strings.HasPrefix(optsList[i], "addr=") {
+ addr := (strings.SplitN(optsList[i], "=", 2)[1])
+ return addr
+ }
+ }
+ return ""
+}
diff --git a/volume/local/local_test.go b/volume/local/local_test.go
new file mode 100644
index 0000000..2353391
--- /dev/null
+++ b/volume/local/local_test.go
@@ -0,0 +1,345 @@
+package local
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/pkg/idtools"
+ "github.com/docker/docker/pkg/mount"
+)
+
+func TestGetAddress(t *testing.T) {
+ cases := map[string]string{
+ "addr=11.11.11.1": "11.11.11.1",
+ " ": "",
+ "addr=": "",
+ "addr=2001:db8::68": "2001:db8::68",
+ }
+ for name, success := range cases {
+ v := getAddress(name)
+ if v != success {
+ t.Errorf("Test case failed for %s actual: %s expected : %s", name, v, success)
+ }
+ }
+
+}
+
+func TestRemove(t *testing.T) {
+ // TODO Windows: Investigate why this test fails on Windows under CI
+ // but passes locally.
+ if runtime.GOOS == "windows" {
+ t.Skip("Test failing on Windows CI")
+ }
+ rootDir, err := ioutil.TempDir("", "local-volume-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(rootDir)
+
+ r, err := New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ vol, err := r.Create("testing", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := r.Remove(vol); err != nil {
+ t.Fatal(err)
+ }
+
+ vol, err = r.Create("testing2", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.RemoveAll(vol.Path()); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := r.Remove(vol); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := os.Stat(vol.Path()); err != nil && !os.IsNotExist(err) {
+ t.Fatal("volume dir not removed")
+ }
+
+ if l, _ := r.List(); len(l) != 0 {
+ t.Fatal("expected there to be no volumes")
+ }
+}
+
+func TestInitializeWithVolumes(t *testing.T) {
+ rootDir, err := ioutil.TempDir("", "local-volume-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(rootDir)
+
+ r, err := New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ vol, err := r.Create("testing", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r, err = New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ v, err := r.Get(vol.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if v.Path() != vol.Path() {
+ t.Fatal("expected to re-initialize root with existing volumes")
+ }
+}
+
+func TestCreate(t *testing.T) {
+ rootDir, err := ioutil.TempDir("", "local-volume-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(rootDir)
+
+ r, err := New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cases := map[string]bool{
+ "name": true,
+ "name-with-dash": true,
+ "name_with_underscore": true,
+ "name/with/slash": false,
+ "name/with/../../slash": false,
+ "./name": false,
+ "../name": false,
+ "./": false,
+ "../": false,
+ "~": false,
+ ".": false,
+ "..": false,
+ "...": false,
+ }
+
+ for name, success := range cases {
+ v, err := r.Create(name, nil)
+ if success {
+ if err != nil {
+ t.Fatal(err)
+ }
+ if v.Name() != name {
+ t.Fatalf("Expected volume with name %s, got %s", name, v.Name())
+ }
+ } else {
+ if err == nil {
+ t.Fatalf("Expected error creating volume with name %s, got nil", name)
+ }
+ }
+ }
+
+ r, err = New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestValidateName(t *testing.T) {
+ r := &Root{}
+ names := map[string]bool{
+ "x": false,
+ "/testvol": false,
+ "thing.d": true,
+ "hello-world": true,
+ "./hello": false,
+ ".hello": false,
+ }
+
+ for vol, expected := range names {
+ err := r.validateName(vol)
+ if expected && err != nil {
+ t.Fatalf("expected %s to be valid got %v", vol, err)
+ }
+ if !expected && err == nil {
+ t.Fatalf("expected %s to be invalid", vol)
+ }
+ }
+}
+
+func TestCreateWithOpts(t *testing.T) {
+ if runtime.GOOS == "windows" || runtime.GOOS == "solaris" {
+ t.Skip()
+ }
+ rootDir, err := ioutil.TempDir("", "local-volume-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(rootDir)
+
+ r, err := New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := r.Create("test", map[string]string{"invalidopt": "notsupported"}); err == nil {
+ t.Fatal("expected invalid opt to cause error")
+ }
+
+ vol, err := r.Create("test", map[string]string{"device": "tmpfs", "type": "tmpfs", "o": "size=1m,uid=1000"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ v := vol.(*localVolume)
+
+ dir, err := v.Mount("1234")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ if err := v.Unmount("1234"); err != nil {
+ t.Fatal(err)
+ }
+ }()
+
+ mountInfos, err := mount.GetMounts()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var found bool
+ for _, info := range mountInfos {
+ if info.Mountpoint == dir {
+ found = true
+ if info.Fstype != "tmpfs" {
+ t.Fatalf("expected tmpfs mount, got %q", info.Fstype)
+ }
+ if info.Source != "tmpfs" {
+ t.Fatalf("expected tmpfs mount, got %q", info.Source)
+ }
+ if !strings.Contains(info.VfsOpts, "uid=1000") {
+ t.Fatalf("expected mount info to have uid=1000: %q", info.VfsOpts)
+ }
+ if !strings.Contains(info.VfsOpts, "size=1024k") {
+ t.Fatalf("expected mount info to have size=1024k: %q", info.VfsOpts)
+ }
+ break
+ }
+ }
+
+ if !found {
+ t.Fatal("mount not found")
+ }
+
+ if v.active.count != 1 {
+ t.Fatalf("Expected active mount count to be 1, got %d", v.active.count)
+ }
+
+ // test double mount
+ if _, err := v.Mount("1234"); err != nil {
+ t.Fatal(err)
+ }
+ if v.active.count != 2 {
+ t.Fatalf("Expected active mount count to be 2, got %d", v.active.count)
+ }
+
+ if err := v.Unmount("1234"); err != nil {
+ t.Fatal(err)
+ }
+ if v.active.count != 1 {
+ t.Fatalf("Expected active mount count to be 1, got %d", v.active.count)
+ }
+
+ mounted, err := mount.Mounted(v.path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !mounted {
+ t.Fatal("expected mount to still be active")
+ }
+
+ r, err = New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ v2, exists := r.volumes["test"]
+ if !exists {
+ t.Fatal("missing volume on restart")
+ }
+
+ if !reflect.DeepEqual(v.opts, v2.opts) {
+ t.Fatal("missing volume options on restart")
+ }
+}
+
+func TestRealodNoOpts(t *testing.T) {
+ rootDir, err := ioutil.TempDir("", "volume-test-reload-no-opts")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(rootDir)
+
+ r, err := New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := r.Create("test1", nil); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := r.Create("test2", nil); err != nil {
+ t.Fatal(err)
+ }
+ // make sure a file with `null` (.e.g. empty opts map from older daemon) is ok
+ if err := ioutil.WriteFile(filepath.Join(rootDir, "test2"), []byte("null"), 600); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := r.Create("test3", nil); err != nil {
+ t.Fatal(err)
+ }
+ // make sure an empty opts file doesn't break us too
+ if err := ioutil.WriteFile(filepath.Join(rootDir, "test3"), nil, 600); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := r.Create("test4", map[string]string{}); err != nil {
+ t.Fatal(err)
+ }
+
+ r, err = New(rootDir, idtools.IDPair{UID: 0, GID: 0})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, name := range []string{"test1", "test2", "test3", "test4"} {
+ v, err := r.Get(name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ lv, ok := v.(*localVolume)
+ if !ok {
+ t.Fatalf("expected *localVolume got: %v", reflect.TypeOf(v))
+ }
+ if lv.opts != nil {
+ t.Fatalf("expected opts to be nil, got: %v", lv.opts)
+ }
+ if _, err := lv.Mount("1234"); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/volume/local/local_unix.go b/volume/local/local_unix.go
new file mode 100644
index 0000000..5bba5b7
--- /dev/null
+++ b/volume/local/local_unix.go
@@ -0,0 +1,99 @@
+// +build linux freebsd solaris
+
+// Package local provides the default implementation for volumes. It
+// is used to mount data volume containers and directories local to
+// the host server.
+package local
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/docker/docker/pkg/mount"
+)
+
+var (
+ oldVfsDir = filepath.Join("vfs", "dir")
+
+ validOpts = map[string]bool{
+ "type": true, // specify the filesystem type for mount, e.g. nfs
+ "o": true, // generic mount options
+ "device": true, // device to mount from
+ }
+)
+
+type optsConfig struct {
+ MountType string
+ MountOpts string
+ MountDevice string
+}
+
+func (o *optsConfig) String() string {
+ return fmt.Sprintf("type='%s' device='%s' o='%s'", o.MountType, o.MountDevice, o.MountOpts)
+}
+
+// scopedPath verifies that the path where the volume is located
+// is under Docker's root and the valid local paths.
+func (r *Root) scopedPath(realPath string) bool {
+ // Volumes path for Docker version >= 1.7
+ if strings.HasPrefix(realPath, filepath.Join(r.scope, volumesPathName)) && realPath != filepath.Join(r.scope, volumesPathName) {
+ return true
+ }
+
+ // Volumes path for Docker version < 1.7
+ if strings.HasPrefix(realPath, filepath.Join(r.scope, oldVfsDir)) {
+ return true
+ }
+
+ return false
+}
+
+func setOpts(v *localVolume, opts map[string]string) error {
+ if len(opts) == 0 {
+ return nil
+ }
+ if err := validateOpts(opts); err != nil {
+ return err
+ }
+
+ v.opts = &optsConfig{
+ MountType: opts["type"],
+ MountOpts: opts["o"],
+ MountDevice: opts["device"],
+ }
+ return nil
+}
+
+func (v *localVolume) mount() error {
+ if v.opts.MountDevice == "" {
+ return fmt.Errorf("missing device in volume options")
+ }
+ mountOpts := v.opts.MountOpts
+ if v.opts.MountType == "nfs" {
+ if addrValue := getAddress(v.opts.MountOpts); addrValue != "" && net.ParseIP(addrValue).To4() == nil {
+ ipAddr, err := net.ResolveIPAddr("ip", addrValue)
+ if err != nil {
+ return errors.Wrapf(err, "error resolving passed in nfs address")
+ }
+ mountOpts = strings.Replace(mountOpts, "addr="+addrValue, "addr="+ipAddr.String(), 1)
+ }
+ }
+ err := mount.Mount(v.opts.MountDevice, v.path, v.opts.MountType, mountOpts)
+ return errors.Wrapf(err, "error while mounting volume with options: %s", v.opts)
+}
+
+func (v *localVolume) CreatedAt() (time.Time, error) {
+ fileInfo, err := os.Stat(v.path)
+ if err != nil {
+ return time.Time{}, err
+ }
+ sec, nsec := fileInfo.Sys().(*syscall.Stat_t).Ctim.Unix()
+ return time.Unix(sec, nsec), nil
+}
diff --git a/volume/local/local_windows.go b/volume/local/local_windows.go
new file mode 100644
index 0000000..6f5d222
--- /dev/null
+++ b/volume/local/local_windows.go
@@ -0,0 +1,46 @@
+// Package local provides the default implementation for volumes. It
+// is used to mount data volume containers and directories local to
+// the host server.
+package local
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "time"
+)
+
+type optsConfig struct{}
+
+var validOpts map[string]bool
+
+// scopedPath verifies that the path where the volume is located
+// is under Docker's root and the valid local paths.
+func (r *Root) scopedPath(realPath string) bool {
+ if strings.HasPrefix(realPath, filepath.Join(r.scope, volumesPathName)) && realPath != filepath.Join(r.scope, volumesPathName) {
+ return true
+ }
+ return false
+}
+
+func setOpts(v *localVolume, opts map[string]string) error {
+ if len(opts) > 0 {
+ return fmt.Errorf("options are not supported on this platform")
+ }
+ return nil
+}
+
+func (v *localVolume) mount() error {
+ return nil
+}
+
+func (v *localVolume) CreatedAt() (time.Time, error) {
+ fileInfo, err := os.Stat(v.path)
+ if err != nil {
+ return time.Time{}, err
+ }
+ ft := fileInfo.Sys().(*syscall.Win32FileAttributeData).CreationTime
+ return time.Unix(0, ft.Nanoseconds()), nil
+}
diff --git a/volume/store/db.go b/volume/store/db.go
new file mode 100644
index 0000000..c5fd164
--- /dev/null
+++ b/volume/store/db.go
@@ -0,0 +1,88 @@
+package store
+
+import (
+ "encoding/json"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/boltdb/bolt"
+ "github.com/pkg/errors"
+)
+
+var volumeBucketName = []byte("volumes")
+
+type volumeMetadata struct {
+ Name string
+ Driver string
+ Labels map[string]string
+ Options map[string]string
+}
+
+func (s *VolumeStore) setMeta(name string, meta volumeMetadata) error {
+ return s.db.Update(func(tx *bolt.Tx) error {
+ return setMeta(tx, name, meta)
+ })
+}
+
+func setMeta(tx *bolt.Tx, name string, meta volumeMetadata) error {
+ metaJSON, err := json.Marshal(meta)
+ if err != nil {
+ return err
+ }
+ b := tx.Bucket(volumeBucketName)
+ return errors.Wrap(b.Put([]byte(name), metaJSON), "error setting volume metadata")
+}
+
+func (s *VolumeStore) getMeta(name string) (volumeMetadata, error) {
+ var meta volumeMetadata
+ err := s.db.View(func(tx *bolt.Tx) error {
+ return getMeta(tx, name, &meta)
+ })
+ return meta, err
+}
+
+func getMeta(tx *bolt.Tx, name string, meta *volumeMetadata) error {
+ b := tx.Bucket(volumeBucketName)
+ val := b.Get([]byte(name))
+ if string(val) == "" {
+ return nil
+ }
+ if err := json.Unmarshal(val, meta); err != nil {
+ return errors.Wrap(err, "error unmarshaling volume metadata")
+ }
+ return nil
+}
+
+func (s *VolumeStore) removeMeta(name string) error {
+ return s.db.Update(func(tx *bolt.Tx) error {
+ return removeMeta(tx, name)
+ })
+}
+
+func removeMeta(tx *bolt.Tx, name string) error {
+ b := tx.Bucket(volumeBucketName)
+ return errors.Wrap(b.Delete([]byte(name)), "error removing volume metadata")
+}
+
+// listMeta is used during restore to get the list of volume metadata
+// from the on-disk database.
+// Any errors that occur are only logged.
+func listMeta(tx *bolt.Tx) []volumeMetadata {
+ var ls []volumeMetadata
+ b := tx.Bucket(volumeBucketName)
+ b.ForEach(func(k, v []byte) error {
+ if len(v) == 0 {
+ // don't try to unmarshal an empty value
+ return nil
+ }
+
+ var m volumeMetadata
+ if err := json.Unmarshal(v, &m); err != nil {
+ // Just log the error
+ logrus.Errorf("Error while reading volume metadata for volume %q: %v", string(k), err)
+ return nil
+ }
+ ls = append(ls, m)
+ return nil
+ })
+ return ls
+}
diff --git a/volume/store/errors.go b/volume/store/errors.go
new file mode 100644
index 0000000..980175f
--- /dev/null
+++ b/volume/store/errors.go
@@ -0,0 +1,76 @@
+package store
+
+import (
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+var (
+ // 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")
+ // errInvalidName is a typed error returned when creating a volume with a name that is not valid on the platform
+ errInvalidName = errors.New("volume name is not valid on this platform")
+ // errNameConflict is a typed error returned on create when a volume exists with the given name, but for a different driver
+ errNameConflict = errors.New("volume name must be unique")
+)
+
+// OpErr is the error type returned by functions in the store package. It describes
+// the operation, volume name, and error.
+type OpErr struct {
+ // Err is the error that occurred during the operation.
+ Err error
+ // Op is the operation which caused the error, such as "create", or "list".
+ Op string
+ // Name is the name of the resource being requested for this op, typically the volume name or the driver name.
+ Name string
+ // Refs is the list of references associated with the resource.
+ Refs []string
+}
+
+// Error satisfies the built-in error interface type.
+func (e *OpErr) Error() string {
+ if e == nil {
+ return "<nil>"
+ }
+ s := e.Op
+ if e.Name != "" {
+ s = s + " " + e.Name
+ }
+
+ s = s + ": " + e.Err.Error()
+ if len(e.Refs) > 0 {
+ s = s + " - " + "[" + strings.Join(e.Refs, ", ") + "]"
+ }
+ return s
+}
+
+// IsInUse returns a boolean indicating whether the error indicates that a
+// volume is in use
+func IsInUse(err error) bool {
+ return isErr(err, errVolumeInUse)
+}
+
+// IsNotExist returns a boolean indicating whether the error indicates that the volume does not exist
+func IsNotExist(err error) bool {
+ return isErr(err, errNoSuchVolume)
+}
+
+// IsNameConflict returns a boolean indicating whether the error indicates that a
+// volume name is already taken
+func IsNameConflict(err error) bool {
+ return isErr(err, errNameConflict)
+}
+
+func isErr(err error, expected error) bool {
+ err = errors.Cause(err)
+ switch pe := err.(type) {
+ case nil:
+ return false
+ case *OpErr:
+ err = errors.Cause(pe.Err)
+ }
+ return err == expected
+}
diff --git a/volume/store/restore.go b/volume/store/restore.go
new file mode 100644
index 0000000..c0c5b51
--- /dev/null
+++ b/volume/store/restore.go
@@ -0,0 +1,83 @@
+package store
+
+import (
+ "sync"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/boltdb/bolt"
+ "github.com/docker/docker/volume"
+ "github.com/docker/docker/volume/drivers"
+)
+
+// restore is called when a new volume store is created.
+// It's primary purpose is to ensure that all drivers' refcounts are set based
+// on known volumes after a restart.
+// This only attempts to track volumes that are actually stored in the on-disk db.
+// It does not probe the available drivers to find anything that may have been added
+// out of band.
+func (s *VolumeStore) restore() {
+ var ls []volumeMetadata
+ s.db.View(func(tx *bolt.Tx) error {
+ ls = listMeta(tx)
+ return nil
+ })
+
+ chRemove := make(chan *volumeMetadata, len(ls))
+ var wg sync.WaitGroup
+ for _, meta := range ls {
+ wg.Add(1)
+ // this is potentially a very slow operation, so do it in a goroutine
+ go func(meta volumeMetadata) {
+ defer wg.Done()
+
+ var v volume.Volume
+ var err error
+ if meta.Driver != "" {
+ v, err = lookupVolume(meta.Driver, meta.Name)
+ if err != nil && err != errNoSuchVolume {
+ logrus.WithError(err).WithField("driver", meta.Driver).WithField("volume", meta.Name).Warn("Error restoring volume")
+ return
+ }
+ if v == nil {
+ // doesn't exist in the driver, remove it from the db
+ chRemove <- &meta
+ return
+ }
+ } else {
+ v, err = s.getVolume(meta.Name)
+ if err != nil {
+ if err == errNoSuchVolume {
+ chRemove <- &meta
+ }
+ return
+ }
+
+ meta.Driver = v.DriverName()
+ if err := s.setMeta(v.Name(), meta); err != nil {
+ logrus.WithError(err).WithField("driver", meta.Driver).WithField("volume", v.Name()).Warn("Error updating volume metadata on restore")
+ }
+ }
+
+ // increment driver refcount
+ volumedrivers.CreateDriver(meta.Driver)
+
+ // cache the volume
+ s.globalLock.Lock()
+ s.options[v.Name()] = meta.Options
+ s.labels[v.Name()] = meta.Labels
+ s.names[v.Name()] = v
+ s.globalLock.Unlock()
+ }(meta)
+ }
+
+ wg.Wait()
+ close(chRemove)
+ s.db.Update(func(tx *bolt.Tx) error {
+ for meta := range chRemove {
+ if err := removeMeta(tx, meta.Name); err != nil {
+ logrus.WithField("volume", meta.Name).Warnf("Error removing stale entry from volume db: %v", err)
+ }
+ }
+ return nil
+ })
+}
diff --git a/volume/store/store.go b/volume/store/store.go
new file mode 100644
index 0000000..cded883
--- /dev/null
+++ b/volume/store/store.go
@@ -0,0 +1,669 @@
+package store
+
+import (
+ "net"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/boltdb/bolt"
+ "github.com/docker/docker/pkg/locker"
+ "github.com/docker/docker/volume"
+ "github.com/docker/docker/volume/drivers"
+)
+
+const (
+ volumeDataDir = "volumes"
+)
+
+type volumeWrapper struct {
+ volume.Volume
+ labels map[string]string
+ scope string
+ options map[string]string
+}
+
+func (v volumeWrapper) Options() map[string]string {
+ options := map[string]string{}
+ for key, value := range v.options {
+ options[key] = value
+ }
+ return options
+}
+
+func (v volumeWrapper) Labels() map[string]string {
+ return v.labels
+}
+
+func (v volumeWrapper) Scope() string {
+ return v.scope
+}
+
+func (v volumeWrapper) CachedPath() string {
+ if vv, ok := v.Volume.(interface {
+ CachedPath() string
+ }); ok {
+ return vv.CachedPath()
+ }
+ return v.Volume.Path()
+}
+
+// New initializes a VolumeStore to keep
+// reference counting of volumes in the system.
+func New(rootPath string) (*VolumeStore, error) {
+ vs := &VolumeStore{
+ locks: &locker.Locker{},
+ names: make(map[string]volume.Volume),
+ refs: make(map[string]map[string]struct{}),
+ labels: make(map[string]map[string]string),
+ options: make(map[string]map[string]string),
+ }
+
+ if rootPath != "" {
+ // initialize metadata store
+ volPath := filepath.Join(rootPath, volumeDataDir)
+ if err := os.MkdirAll(volPath, 750); err != nil {
+ return nil, err
+ }
+
+ dbPath := filepath.Join(volPath, "metadata.db")
+
+ var err error
+ vs.db, err = bolt.Open(dbPath, 0600, &bolt.Options{Timeout: 1 * time.Second})
+ if err != nil {
+ return nil, errors.Wrap(err, "error while opening volume store metadata database")
+ }
+
+ // initialize volumes bucket
+ if err := vs.db.Update(func(tx *bolt.Tx) error {
+ if _, err := tx.CreateBucketIfNotExists(volumeBucketName); err != nil {
+ return errors.Wrap(err, "error while setting up volume store metadata database")
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ }
+
+ vs.restore()
+
+ return vs, nil
+}
+
+func (s *VolumeStore) getNamed(name string) (volume.Volume, bool) {
+ s.globalLock.RLock()
+ v, exists := s.names[name]
+ s.globalLock.RUnlock()
+ return v, exists
+}
+
+func (s *VolumeStore) setNamed(v volume.Volume, ref string) {
+ name := v.Name()
+
+ s.globalLock.Lock()
+ s.names[name] = v
+ if len(ref) > 0 {
+ if s.refs[name] == nil {
+ s.refs[name] = make(map[string]struct{})
+ }
+ s.refs[name][ref] = struct{}{}
+ }
+ s.globalLock.Unlock()
+}
+
+// hasRef returns true if the given name has at least one ref.
+// Callers of this function are expected to hold the name lock.
+func (s *VolumeStore) hasRef(name string) bool {
+ s.globalLock.RLock()
+ l := len(s.refs[name])
+ s.globalLock.RUnlock()
+ return l > 0
+}
+
+// getRefs gets the list of refs for a given name
+// Callers of this function are expected to hold the name lock.
+func (s *VolumeStore) getRefs(name string) []string {
+ s.globalLock.RLock()
+ defer s.globalLock.RUnlock()
+
+ refs := make([]string, 0, len(s.refs[name]))
+ for r := range s.refs[name] {
+ refs = append(refs, r)
+ }
+
+ return refs
+}
+
+// Purge allows the cleanup of internal data on docker in case
+// the internal data is out of sync with volumes driver plugins.
+func (s *VolumeStore) Purge(name string) {
+ s.globalLock.Lock()
+ v, exists := s.names[name]
+ if exists {
+ if _, err := volumedrivers.RemoveDriver(v.DriverName()); err != nil {
+ logrus.Errorf("Error dereferencing volume driver: %v", err)
+ }
+ }
+ if err := s.removeMeta(name); err != nil {
+ logrus.Errorf("Error removing volume metadata for volume %q: %v", name, err)
+ }
+ delete(s.names, name)
+ delete(s.refs, name)
+ delete(s.labels, name)
+ delete(s.options, name)
+ s.globalLock.Unlock()
+}
+
+// VolumeStore is a struct that stores the list of volumes available and keeps track of their usage counts
+type VolumeStore struct {
+ // locks ensures that only one action is being performed on a particular volume at a time without locking the entire store
+ // since actions on volumes can be quite slow, this ensures the store is free to handle requests for other volumes.
+ locks *locker.Locker
+ // globalLock is used to protect access to mutable structures used by the store object
+ globalLock sync.RWMutex
+ // names stores the volume name -> volume relationship.
+ // This is used for making lookups faster so we don't have to probe all drivers
+ names map[string]volume.Volume
+ // refs stores the volume name and the list of things referencing it
+ refs map[string]map[string]struct{}
+ // labels stores volume labels for each volume
+ labels map[string]map[string]string
+ // options stores volume options for each volume
+ options map[string]map[string]string
+ db *bolt.DB
+}
+
+// List proxies to all registered volume drivers to get the full list of volumes
+// If a driver returns a volume that has name which conflicts with another volume from a different driver,
+// the first volume is chosen and the conflicting volume is dropped.
+func (s *VolumeStore) List() ([]volume.Volume, []string, error) {
+ vols, warnings, err := s.list()
+ if err != nil {
+ return nil, nil, &OpErr{Err: err, Op: "list"}
+ }
+ var out []volume.Volume
+
+ for _, v := range vols {
+ name := normaliseVolumeName(v.Name())
+
+ s.locks.Lock(name)
+ storedV, exists := s.getNamed(name)
+ // Note: it's not safe to populate the cache here because the volume may have been
+ // deleted before we acquire a lock on its name
+ if exists && storedV.DriverName() != v.DriverName() {
+ logrus.Warnf("Volume name %s already exists for driver %s, not including volume returned by %s", v.Name(), storedV.DriverName(), v.DriverName())
+ s.locks.Unlock(v.Name())
+ continue
+ }
+
+ out = append(out, v)
+ s.locks.Unlock(v.Name())
+ }
+ return out, warnings, nil
+}
+
+// list goes through each volume driver and asks for its list of volumes.
+func (s *VolumeStore) list() ([]volume.Volume, []string, error) {
+ var (
+ ls []volume.Volume
+ warnings []string
+ )
+
+ drivers, err := volumedrivers.GetAllDrivers()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ type vols struct {
+ vols []volume.Volume
+ err error
+ driverName string
+ }
+ chVols := make(chan vols, len(drivers))
+
+ for _, vd := range drivers {
+ go func(d volume.Driver) {
+ vs, err := d.List()
+ if err != nil {
+ chVols <- vols{driverName: d.Name(), err: &OpErr{Err: err, Name: d.Name(), Op: "list"}}
+ return
+ }
+ for i, v := range vs {
+ s.globalLock.RLock()
+ vs[i] = volumeWrapper{v, s.labels[v.Name()], d.Scope(), s.options[v.Name()]}
+ s.globalLock.RUnlock()
+ }
+
+ chVols <- vols{vols: vs}
+ }(vd)
+ }
+
+ badDrivers := make(map[string]struct{})
+ for i := 0; i < len(drivers); i++ {
+ vs := <-chVols
+
+ if vs.err != nil {
+ warnings = append(warnings, vs.err.Error())
+ badDrivers[vs.driverName] = struct{}{}
+ logrus.Warn(vs.err)
+ }
+ ls = append(ls, vs.vols...)
+ }
+
+ if len(badDrivers) > 0 {
+ s.globalLock.RLock()
+ for _, v := range s.names {
+ if _, exists := badDrivers[v.DriverName()]; exists {
+ ls = append(ls, v)
+ }
+ }
+ s.globalLock.RUnlock()
+ }
+ return ls, warnings, nil
+}
+
+// CreateWithRef creates a volume with the given name and driver and stores the ref
+// This ensures there's no race between creating a volume and then storing a reference.
+func (s *VolumeStore) CreateWithRef(name, driverName, ref string, opts, labels map[string]string) (volume.Volume, error) {
+ name = normaliseVolumeName(name)
+ s.locks.Lock(name)
+ defer s.locks.Unlock(name)
+
+ v, err := s.create(name, driverName, opts, labels)
+ if err != nil {
+ if _, ok := err.(*OpErr); ok {
+ return nil, err
+ }
+ return nil, &OpErr{Err: err, Name: name, Op: "create"}
+ }
+
+ s.setNamed(v, ref)
+ return v, nil
+}
+
+// Create creates a volume with the given name and driver.
+// This is just like CreateWithRef() except we don't store a reference while holding the lock.
+func (s *VolumeStore) Create(name, driverName string, opts, labels map[string]string) (volume.Volume, error) {
+ return s.CreateWithRef(name, driverName, "", opts, labels)
+}
+
+// checkConflict checks the local cache for name collisions with the passed in name,
+// for existing volumes with the same name but in a different driver.
+// This is used by `Create` as a best effort to prevent name collisions for volumes.
+// If a matching volume is found that is not a conflict that is returned so the caller
+// does not need to perform an additional lookup.
+// When no matching volume is found, both returns will be nil
+//
+// Note: This does not probe all the drivers for name collisions because v1 plugins
+// are very slow, particularly if the plugin is down, and cause other issues,
+// particularly around locking the store.
+// TODO(cpuguy83): With v2 plugins this shouldn't be a problem. Could also potentially
+// use a connect timeout for this kind of check to ensure we aren't blocking for a
+// long time.
+func (s *VolumeStore) checkConflict(name, driverName string) (volume.Volume, error) {
+ // check the local cache
+ v, _ := s.getNamed(name)
+ if v == nil {
+ return nil, nil
+ }
+
+ vDriverName := v.DriverName()
+ var conflict bool
+ if driverName != "" {
+ // Retrieve canonical driver name to avoid inconsistencies (for example
+ // "plugin" vs. "plugin:latest")
+ vd, err := volumedrivers.GetDriver(driverName)
+ if err != nil {
+ return nil, err
+ }
+
+ if vDriverName != vd.Name() {
+ conflict = true
+ }
+ }
+
+ // let's check if the found volume ref
+ // is stale by checking with the driver if it still exists
+ exists, err := volumeExists(v)
+ if err != nil {
+ return nil, errors.Wrapf(errNameConflict, "found reference to volume '%s' in driver '%s', but got an error while checking the driver: %v", name, vDriverName, err)
+ }
+
+ if exists {
+ if conflict {
+ return nil, errors.Wrapf(errNameConflict, "driver '%s' already has volume '%s'", vDriverName, name)
+ }
+ return v, nil
+ }
+
+ if s.hasRef(v.Name()) {
+ // Containers are referencing this volume but it doesn't seem to exist anywhere.
+ // Return a conflict error here, the user can fix this with `docker volume rm -f`
+ return nil, errors.Wrapf(errNameConflict, "found references to volume '%s' in driver '%s' but the volume was not found in the driver -- you may need to remove containers referencing this volume or force remove the volume to re-create it", name, vDriverName)
+ }
+
+ // doesn't exist, so purge it from the cache
+ s.Purge(name)
+ return nil, nil
+}
+
+// volumeExists returns if the volume is still present in the driver.
+// An error is returned if there was an issue communicating with the driver.
+func volumeExists(v volume.Volume) (bool, error) {
+ exists, err := lookupVolume(v.DriverName(), v.Name())
+ if err != nil {
+ return false, err
+ }
+ return exists != nil, nil
+}
+
+// create asks the given driver to create a volume with the name/opts.
+// If a volume with the name is already known, it will ask the stored driver for the volume.
+// If the passed in driver name does not match the driver name which is stored
+// for the given volume name, an error is returned after checking if the reference is stale.
+// If the reference is stale, it will be purged and this create can continue.
+// It is expected that callers of this function hold any necessary locks.
+func (s *VolumeStore) create(name, driverName string, opts, labels map[string]string) (volume.Volume, error) {
+ // Validate the name in a platform-specific manner
+ valid, err := volume.IsVolumeNameValid(name)
+ if err != nil {
+ return nil, err
+ }
+ if !valid {
+ return nil, &OpErr{Err: errInvalidName, Name: name, Op: "create"}
+ }
+
+ v, err := s.checkConflict(name, driverName)
+ if err != nil {
+ return nil, err
+ }
+
+ if v != nil {
+ return v, nil
+ }
+
+ // Since there isn't a specified driver name, let's see if any of the existing drivers have this volume name
+ if driverName == "" {
+ v, _ := s.getVolume(name)
+ if v != nil {
+ return v, nil
+ }
+ }
+
+ vd, err := volumedrivers.CreateDriver(driverName)
+
+ if err != nil {
+ return nil, &OpErr{Op: "create", Name: name, Err: err}
+ }
+
+ logrus.Debugf("Registering new volume reference: driver %q, name %q", vd.Name(), name)
+
+ if v, _ := vd.Get(name); v != nil {
+ return v, nil
+ }
+ v, err = vd.Create(name, opts)
+ if err != nil {
+ return nil, err
+ }
+ s.globalLock.Lock()
+ s.labels[name] = labels
+ s.options[name] = opts
+ s.refs[name] = make(map[string]struct{})
+ s.globalLock.Unlock()
+
+ metadata := volumeMetadata{
+ Name: name,
+ Driver: vd.Name(),
+ Labels: labels,
+ Options: opts,
+ }
+
+ if err := s.setMeta(name, metadata); err != nil {
+ return nil, err
+ }
+ return volumeWrapper{v, labels, vd.Scope(), opts}, nil
+}
+
+// GetWithRef gets a volume with the given name from the passed in driver and stores the ref
+// This is just like Get(), but we store the reference while holding the lock.
+// This makes sure there are no races between checking for the existence of a volume and adding a reference for it
+func (s *VolumeStore) GetWithRef(name, driverName, ref string) (volume.Volume, error) {
+ name = normaliseVolumeName(name)
+ s.locks.Lock(name)
+ defer s.locks.Unlock(name)
+
+ vd, err := volumedrivers.GetDriver(driverName)
+ if err != nil {
+ return nil, &OpErr{Err: err, Name: name, Op: "get"}
+ }
+
+ v, err := vd.Get(name)
+ if err != nil {
+ return nil, &OpErr{Err: err, Name: name, Op: "get"}
+ }
+
+ s.setNamed(v, ref)
+
+ s.globalLock.RLock()
+ defer s.globalLock.RUnlock()
+ return volumeWrapper{v, s.labels[name], vd.Scope(), s.options[name]}, 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) {
+ name = normaliseVolumeName(name)
+ s.locks.Lock(name)
+ defer s.locks.Unlock(name)
+
+ v, err := s.getVolume(name)
+ if err != nil {
+ return nil, &OpErr{Err: err, Name: name, Op: "get"}
+ }
+ s.setNamed(v, "")
+ return v, nil
+}
+
+// getVolume requests the volume, if the driver info is stored it just accesses that driver,
+// if the driver is unknown it probes all drivers until it finds the first volume with that name.
+// it is expected that callers of this function hold any necessary locks
+func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
+ var meta volumeMetadata
+ meta, err := s.getMeta(name)
+ if err != nil {
+ return nil, err
+ }
+
+ driverName := meta.Driver
+ if driverName == "" {
+ s.globalLock.RLock()
+ v, exists := s.names[name]
+ s.globalLock.RUnlock()
+ if exists {
+ meta.Driver = v.DriverName()
+ if err := s.setMeta(name, meta); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ if meta.Driver != "" {
+ vol, err := lookupVolume(meta.Driver, name)
+ if err != nil {
+ return nil, err
+ }
+ if vol == nil {
+ s.Purge(name)
+ return nil, errNoSuchVolume
+ }
+
+ var scope string
+ vd, err := volumedrivers.GetDriver(meta.Driver)
+ if err == nil {
+ scope = vd.Scope()
+ }
+ return volumeWrapper{vol, meta.Labels, scope, meta.Options}, nil
+ }
+
+ logrus.Debugf("Probing all drivers for volume with name: %s", name)
+ drivers, err := volumedrivers.GetAllDrivers()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, d := range drivers {
+ v, err := d.Get(name)
+ if err != nil || v == nil {
+ continue
+ }
+ meta.Driver = v.DriverName()
+ if err := s.setMeta(name, meta); err != nil {
+ return nil, err
+ }
+ return volumeWrapper{v, meta.Labels, d.Scope(), meta.Options}, nil
+ }
+ return nil, errNoSuchVolume
+}
+
+// lookupVolume gets the specified volume from the specified driver.
+// This will only return errors related to communications with the driver.
+// If the driver returns an error that is not communication related the
+// error is logged but not returned.
+// If the volume is not found it will return `nil, nil``
+func lookupVolume(driverName, volumeName string) (volume.Volume, error) {
+ vd, err := volumedrivers.GetDriver(driverName)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error while checking if volume %q exists in driver %q", volumeName, driverName)
+ }
+ v, err := vd.Get(volumeName)
+ if err != nil {
+ err = errors.Cause(err)
+ if _, ok := err.(net.Error); ok {
+ if v != nil {
+ volumeName = v.Name()
+ driverName = v.DriverName()
+ }
+ return nil, errors.Wrapf(err, "error while checking if volume %q exists in driver %q", volumeName, driverName)
+ }
+
+ // At this point, the error could be anything from the driver, such as "no such volume"
+ // Let's not check an error here, and instead check if the driver returned a volume
+ logrus.WithError(err).WithField("driver", driverName).WithField("volume", volumeName).Warnf("Error while looking up volume")
+ }
+ return v, nil
+}
+
+// Remove removes the requested volume. A volume is not removed if it has any refs
+func (s *VolumeStore) Remove(v volume.Volume) error {
+ name := normaliseVolumeName(v.Name())
+ s.locks.Lock(name)
+ defer s.locks.Unlock(name)
+
+ if s.hasRef(name) {
+ return &OpErr{Err: errVolumeInUse, Name: v.Name(), Op: "remove", Refs: s.getRefs(name)}
+ }
+
+ vd, err := volumedrivers.GetDriver(v.DriverName())
+ if err != nil {
+ return &OpErr{Err: err, Name: v.DriverName(), Op: "remove"}
+ }
+
+ logrus.Debugf("Removing volume reference: driver %s, name %s", v.DriverName(), name)
+ vol := unwrapVolume(v)
+ if err := vd.Remove(vol); err != nil {
+ return &OpErr{Err: err, Name: name, Op: "remove"}
+ }
+
+ s.Purge(name)
+ return nil
+}
+
+// Dereference removes the specified reference to the volume
+func (s *VolumeStore) Dereference(v volume.Volume, ref string) {
+ name := v.Name()
+
+ s.locks.Lock(name)
+ defer s.locks.Unlock(name)
+
+ s.globalLock.Lock()
+ defer s.globalLock.Unlock()
+
+ if s.refs[name] != nil {
+ delete(s.refs[name], ref)
+ }
+}
+
+// Refs gets the current list of refs for the given volume
+func (s *VolumeStore) Refs(v volume.Volume) []string {
+ name := v.Name()
+
+ s.locks.Lock(name)
+ defer s.locks.Unlock(name)
+
+ return s.getRefs(name)
+}
+
+// FilterByDriver returns the available volumes filtered by driver name
+func (s *VolumeStore) FilterByDriver(name string) ([]volume.Volume, error) {
+ vd, err := volumedrivers.GetDriver(name)
+ if err != nil {
+ return nil, &OpErr{Err: err, Name: name, Op: "list"}
+ }
+ ls, err := vd.List()
+ if err != nil {
+ return nil, &OpErr{Err: err, Name: name, Op: "list"}
+ }
+ for i, v := range ls {
+ options := map[string]string{}
+ s.globalLock.RLock()
+ for key, value := range s.options[v.Name()] {
+ options[key] = value
+ }
+ ls[i] = volumeWrapper{v, s.labels[v.Name()], vd.Scope(), options}
+ s.globalLock.RUnlock()
+ }
+ return ls, nil
+}
+
+// FilterByUsed returns the available volumes filtered by if they are in use or not.
+// `used=true` returns only volumes that are being used, while `used=false` returns
+// only volumes that are not being used.
+func (s *VolumeStore) FilterByUsed(vols []volume.Volume, used bool) []volume.Volume {
+ return s.filter(vols, func(v volume.Volume) bool {
+ s.locks.Lock(v.Name())
+ hasRef := s.hasRef(v.Name())
+ s.locks.Unlock(v.Name())
+ return used == hasRef
+ })
+}
+
+// filterFunc defines a function to allow filter volumes in the store
+type filterFunc func(vol volume.Volume) bool
+
+// filter returns the available volumes filtered by a filterFunc function
+func (s *VolumeStore) filter(vols []volume.Volume, f filterFunc) []volume.Volume {
+ var ls []volume.Volume
+ for _, v := range vols {
+ if f(v) {
+ ls = append(ls, v)
+ }
+ }
+ return ls
+}
+
+func unwrapVolume(v volume.Volume) volume.Volume {
+ if vol, ok := v.(volumeWrapper); ok {
+ return vol.Volume
+ }
+
+ return v
+}
+
+// Shutdown releases all resources used by the volume store
+// It does not make any changes to volumes, drivers, etc.
+func (s *VolumeStore) Shutdown() error {
+ return s.db.Close()
+}
diff --git a/volume/store/store_test.go b/volume/store/store_test.go
new file mode 100644
index 0000000..f5f0025
--- /dev/null
+++ b/volume/store/store_test.go
@@ -0,0 +1,234 @@
+package store
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/volume/drivers"
+ volumetestutils "github.com/docker/docker/volume/testutils"
+)
+
+func TestCreate(t *testing.T) {
+ volumedrivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
+ defer volumedrivers.Unregister("fake")
+ dir, err := ioutil.TempDir("", "test-create")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ s, err := New(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ v, err := s.Create("fake1", "fake", nil, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if v.Name() != "fake1" {
+ t.Fatalf("Expected fake1 volume, got %v", v)
+ }
+ if l, _, _ := s.List(); len(l) != 1 {
+ t.Fatalf("Expected 1 volume in the store, got %v: %v", len(l), l)
+ }
+
+ if _, err := s.Create("none", "none", nil, nil); err == nil {
+ t.Fatalf("Expected unknown driver error, got nil")
+ }
+
+ _, err = s.Create("fakeerror", "fake", map[string]string{"error": "create error"}, nil)
+ expected := &OpErr{Op: "create", Name: "fakeerror", Err: errors.New("create error")}
+ if err != nil && err.Error() != expected.Error() {
+ t.Fatalf("Expected create fakeError: create error, got %v", err)
+ }
+}
+
+func TestRemove(t *testing.T) {
+ volumedrivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
+ volumedrivers.Register(volumetestutils.NewFakeDriver("noop"), "noop")
+ defer volumedrivers.Unregister("fake")
+ defer volumedrivers.Unregister("noop")
+ dir, err := ioutil.TempDir("", "test-remove")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+ s, err := New(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // doing string compare here since this error comes directly from the driver
+ expected := "no such volume"
+ if err := s.Remove(volumetestutils.NoopVolume{}); err == nil || !strings.Contains(err.Error(), expected) {
+ t.Fatalf("Expected error %q, got %v", expected, err)
+ }
+
+ v, err := s.CreateWithRef("fake1", "fake", "fake", nil, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := s.Remove(v); !IsInUse(err) {
+ t.Fatalf("Expected ErrVolumeInUse error, got %v", err)
+ }
+ s.Dereference(v, "fake")
+ if err := s.Remove(v); err != nil {
+ t.Fatal(err)
+ }
+ if l, _, _ := s.List(); len(l) != 0 {
+ t.Fatalf("Expected 0 volumes in the store, got %v, %v", len(l), l)
+ }
+}
+
+func TestList(t *testing.T) {
+ volumedrivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
+ volumedrivers.Register(volumetestutils.NewFakeDriver("fake2"), "fake2")
+ defer volumedrivers.Unregister("fake")
+ defer volumedrivers.Unregister("fake2")
+ dir, err := ioutil.TempDir("", "test-list")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ s, err := New(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Create("test", "fake", nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Create("test2", "fake2", nil, nil); err != nil {
+ t.Fatal(err)
+ }
+
+ ls, _, err := s.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(ls) != 2 {
+ t.Fatalf("expected 2 volumes, got: %d", len(ls))
+ }
+ if err := s.Shutdown(); err != nil {
+ t.Fatal(err)
+ }
+
+ // and again with a new store
+ s, err = New(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ls, _, err = s.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(ls) != 2 {
+ t.Fatalf("expected 2 volumes, got: %d", len(ls))
+ }
+}
+
+func TestFilterByDriver(t *testing.T) {
+ volumedrivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
+ volumedrivers.Register(volumetestutils.NewFakeDriver("noop"), "noop")
+ defer volumedrivers.Unregister("fake")
+ defer volumedrivers.Unregister("noop")
+ dir, err := ioutil.TempDir("", "test-filter-driver")
+ if err != nil {
+ t.Fatal(err)
+ }
+ s, err := New(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := s.Create("fake1", "fake", nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Create("fake2", "fake", nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Create("fake3", "noop", nil, nil); err != nil {
+ t.Fatal(err)
+ }
+
+ if l, _ := s.FilterByDriver("fake"); len(l) != 2 {
+ t.Fatalf("Expected 2 volumes, got %v, %v", len(l), l)
+ }
+
+ if l, _ := s.FilterByDriver("noop"); len(l) != 1 {
+ t.Fatalf("Expected 1 volume, got %v, %v", len(l), l)
+ }
+}
+
+func TestFilterByUsed(t *testing.T) {
+ volumedrivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
+ volumedrivers.Register(volumetestutils.NewFakeDriver("noop"), "noop")
+ dir, err := ioutil.TempDir("", "test-filter-used")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s, err := New(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := s.CreateWithRef("fake1", "fake", "volReference", nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Create("fake2", "fake", nil, nil); err != nil {
+ t.Fatal(err)
+ }
+
+ vols, _, err := s.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dangling := s.FilterByUsed(vols, false)
+ if len(dangling) != 1 {
+ t.Fatalf("expected 1 dangling volume, got %v", len(dangling))
+ }
+ if dangling[0].Name() != "fake2" {
+ t.Fatalf("expected dangling volume fake2, got %s", dangling[0].Name())
+ }
+
+ used := s.FilterByUsed(vols, true)
+ if len(used) != 1 {
+ t.Fatalf("expected 1 used volume, got %v", len(used))
+ }
+ if used[0].Name() != "fake1" {
+ t.Fatalf("expected used volume fake1, got %s", used[0].Name())
+ }
+}
+
+func TestDerefMultipleOfSameRef(t *testing.T) {
+ volumedrivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
+ dir, err := ioutil.TempDir("", "test-same-deref")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+ s, err := New(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ v, err := s.CreateWithRef("fake1", "fake", "volReference", nil, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := s.GetWithRef("fake1", "fake", "volReference"); err != nil {
+ t.Fatal(err)
+ }
+
+ s.Dereference(v, "volReference")
+ if err := s.Remove(v); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/volume/store/store_unix.go b/volume/store/store_unix.go
new file mode 100644
index 0000000..8ebc1f2
--- /dev/null
+++ b/volume/store/store_unix.go
@@ -0,0 +1,9 @@
+// +build linux freebsd solaris
+
+package store
+
+// normaliseVolumeName is a platform specific function to normalise the name
+// of a volume. This is a no-op on Unix-like platforms
+func normaliseVolumeName(name string) string {
+ return name
+}
diff --git a/volume/store/store_windows.go b/volume/store/store_windows.go
new file mode 100644
index 0000000..8601cdd
--- /dev/null
+++ b/volume/store/store_windows.go
@@ -0,0 +1,12 @@
+package store
+
+import "strings"
+
+// normaliseVolumeName is a platform specific function to normalise the name
+// of a volume. On Windows, as NTFS is case insensitive, under
+// c:\ProgramData\Docker\Volumes\, the folders John and john would be synonymous.
+// Hence we can't allow the volume "John" and "john" to be created as separate
+// volumes.
+func normaliseVolumeName(name string) string {
+ return strings.ToLower(name)
+}
diff --git a/volume/testutils/testutils.go b/volume/testutils/testutils.go
new file mode 100644
index 0000000..d54ba44
--- /dev/null
+++ b/volume/testutils/testutils.go
@@ -0,0 +1,123 @@
+package testutils
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/docker/docker/volume"
+)
+
+// NoopVolume is a volume that doesn't perform any operation
+type NoopVolume struct{}
+
+// Name is the name of the volume
+func (NoopVolume) Name() string { return "noop" }
+
+// DriverName is the name of the driver
+func (NoopVolume) DriverName() string { return "noop" }
+
+// Path is the filesystem path to the volume
+func (NoopVolume) Path() string { return "noop" }
+
+// Mount mounts the volume in the container
+func (NoopVolume) Mount(_ string) (string, error) { return "noop", nil }
+
+// Unmount unmounts the volume from the container
+func (NoopVolume) Unmount(_ string) error { return nil }
+
+// Status proivdes low-level details about the volume
+func (NoopVolume) Status() map[string]interface{} { return nil }
+
+// CreatedAt provides the time the volume (directory) was created at
+func (NoopVolume) CreatedAt() (time.Time, error) { return time.Now(), nil }
+
+// FakeVolume is a fake volume with a random name
+type FakeVolume struct {
+ name string
+ driverName string
+}
+
+// NewFakeVolume creates a new fake volume for testing
+func NewFakeVolume(name string, driverName string) volume.Volume {
+ return FakeVolume{name: name, driverName: driverName}
+}
+
+// Name is the name of the volume
+func (f FakeVolume) Name() string { return f.name }
+
+// DriverName is the name of the driver
+func (f FakeVolume) DriverName() string { return f.driverName }
+
+// Path is the filesystem path to the volume
+func (FakeVolume) Path() string { return "fake" }
+
+// Mount mounts the volume in the container
+func (FakeVolume) Mount(_ string) (string, error) { return "fake", nil }
+
+// Unmount unmounts the volume from the container
+func (FakeVolume) Unmount(_ string) error { return nil }
+
+// Status proivdes low-level details about the volume
+func (FakeVolume) Status() map[string]interface{} { return nil }
+
+// CreatedAt provides the time the volume (directory) was created at
+func (FakeVolume) CreatedAt() (time.Time, error) { return time.Now(), nil }
+
+// FakeDriver is a driver that generates fake volumes
+type FakeDriver struct {
+ name string
+ vols map[string]volume.Volume
+}
+
+// NewFakeDriver creates a new FakeDriver with the specified name
+func NewFakeDriver(name string) volume.Driver {
+ return &FakeDriver{
+ name: name,
+ vols: make(map[string]volume.Volume),
+ }
+}
+
+// Name is the name of the driver
+func (d *FakeDriver) Name() string { return d.name }
+
+// Create initializes a fake volume.
+// It returns an error if the options include an "error" key with a message
+func (d *FakeDriver) Create(name string, opts map[string]string) (volume.Volume, error) {
+ if opts != nil && opts["error"] != "" {
+ return nil, fmt.Errorf(opts["error"])
+ }
+ v := NewFakeVolume(name, d.name)
+ d.vols[name] = v
+ return v, nil
+}
+
+// Remove deletes a volume.
+func (d *FakeDriver) Remove(v volume.Volume) error {
+ if _, exists := d.vols[v.Name()]; !exists {
+ return fmt.Errorf("no such volume")
+ }
+ delete(d.vols, v.Name())
+ return nil
+}
+
+// List lists the volumes
+func (d *FakeDriver) List() ([]volume.Volume, error) {
+ var vols []volume.Volume
+ for _, v := range d.vols {
+ vols = append(vols, v)
+ }
+ return vols, nil
+}
+
+// Get gets the volume
+func (d *FakeDriver) Get(name string) (volume.Volume, error) {
+ if v, exists := d.vols[name]; exists {
+ return v, nil
+ }
+ return nil, fmt.Errorf("no such volume")
+}
+
+// Scope returns the local scope
+func (*FakeDriver) Scope() string {
+ return "local"
+}
diff --git a/volume/validate.go b/volume/validate.go
new file mode 100644
index 0000000..42396a0
--- /dev/null
+++ b/volume/validate.go
@@ -0,0 +1,140 @@
+package volume
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/docker/docker/api/types/mount"
+)
+
+var errBindNotExist = errors.New("bind source path does not exist")
+
+type validateOpts struct {
+ skipBindSourceCheck bool
+ skipAbsolutePathCheck bool
+}
+
+func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error {
+ opts := validateOpts{}
+ for _, o := range options {
+ o(&opts)
+ }
+
+ if len(mnt.Target) == 0 {
+ return &errMountConfig{mnt, errMissingField("Target")}
+ }
+
+ if err := validateNotRoot(mnt.Target); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+
+ if !opts.skipAbsolutePathCheck {
+ if err := validateAbsolute(mnt.Target); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+ }
+
+ switch mnt.Type {
+ case mount.TypeBind:
+ if len(mnt.Source) == 0 {
+ return &errMountConfig{mnt, errMissingField("Source")}
+ }
+ // Don't error out just because the propagation mode is not supported on the platform
+ if opts := mnt.BindOptions; opts != nil {
+ if len(opts.Propagation) > 0 && len(propagationModes) > 0 {
+ if _, ok := propagationModes[opts.Propagation]; !ok {
+ return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
+ }
+ }
+ }
+ if mnt.VolumeOptions != nil {
+ return &errMountConfig{mnt, errExtraField("VolumeOptions")}
+ }
+
+ if err := validateAbsolute(mnt.Source); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+
+ // Do not allow binding to non-existent path
+ if !opts.skipBindSourceCheck {
+ fi, err := os.Stat(mnt.Source)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return &errMountConfig{mnt, err}
+ }
+ return &errMountConfig{mnt, errBindNotExist}
+ }
+ if err := validateStat(fi); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+ }
+ case mount.TypeVolume:
+ if mnt.BindOptions != nil {
+ return &errMountConfig{mnt, errExtraField("BindOptions")}
+ }
+
+ if len(mnt.Source) == 0 && mnt.ReadOnly {
+ return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
+ }
+
+ if len(mnt.Source) != 0 {
+ if valid, err := IsVolumeNameValid(mnt.Source); !valid {
+ if err == nil {
+ err = errors.New("invalid volume name")
+ }
+ return &errMountConfig{mnt, err}
+ }
+ }
+ case mount.TypeTmpfs:
+ if len(mnt.Source) != 0 {
+ return &errMountConfig{mnt, errExtraField("Source")}
+ }
+ if err := ValidateTmpfsMountDestination(mnt.Target); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+ if _, err := ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
+ return &errMountConfig{mnt, err}
+ }
+ default:
+ return &errMountConfig{mnt, errors.New("mount type unknown")}
+ }
+ return nil
+}
+
+type errMountConfig struct {
+ mount *mount.Mount
+ err error
+}
+
+func (e *errMountConfig) Error() string {
+ return fmt.Sprintf("invalid mount config for type %q: %v", e.mount.Type, e.err.Error())
+}
+
+func errExtraField(name string) error {
+ return fmt.Errorf("field %s must not be specified", name)
+}
+func errMissingField(name string) error {
+ return fmt.Errorf("field %s must not be empty", name)
+}
+
+func validateAbsolute(p string) error {
+ p = convertSlash(p)
+ if filepath.IsAbs(p) {
+ return nil
+ }
+ return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
+}
+
+// ValidateTmpfsMountDestination validates the destination of tmpfs mount.
+// Currently, we have only two obvious rule for validation:
+// - path must not be "/"
+// - path must be absolute
+// We should add more rules carefully (#30166)
+func ValidateTmpfsMountDestination(dest string) error {
+ if err := validateNotRoot(dest); err != nil {
+ return err
+ }
+ return validateAbsolute(dest)
+}
diff --git a/volume/validate_test.go b/volume/validate_test.go
new file mode 100644
index 0000000..8732500
--- /dev/null
+++ b/volume/validate_test.go
@@ -0,0 +1,43 @@
+package volume
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/api/types/mount"
+)
+
+func TestValidateMount(t *testing.T) {
+ testDir, err := ioutil.TempDir("", "test-validate-mount")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(testDir)
+
+ cases := []struct {
+ input mount.Mount
+ expected error
+ }{
+ {mount.Mount{Type: mount.TypeVolume}, errMissingField("Target")},
+ {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath, Source: "hello"}, nil},
+ {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, nil},
+ {mount.Mount{Type: mount.TypeBind}, errMissingField("Target")},
+ {mount.Mount{Type: mount.TypeBind, Target: testDestinationPath}, errMissingField("Source")},
+ {mount.Mount{Type: mount.TypeBind, Target: testDestinationPath, Source: testSourcePath, VolumeOptions: &mount.VolumeOptions{}}, errExtraField("VolumeOptions")},
+ {mount.Mount{Type: mount.TypeBind, Source: testSourcePath, Target: testDestinationPath}, errBindNotExist},
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, nil},
+ {mount.Mount{Type: "invalid", Target: testDestinationPath}, errors.New("mount type unknown")},
+ }
+ for i, x := range cases {
+ err := validateMountConfig(&x.input)
+ if err == nil && x.expected == nil {
+ continue
+ }
+ if (err == nil && x.expected != nil) || (x.expected == nil && err != nil) || !strings.Contains(err.Error(), x.expected.Error()) {
+ t.Fatalf("expected %q, got %q, case: %d", x.expected, err, i)
+ }
+ }
+}
diff --git a/volume/validate_test_unix.go b/volume/validate_test_unix.go
new file mode 100644
index 0000000..dd1de2f
--- /dev/null
+++ b/volume/validate_test_unix.go
@@ -0,0 +1,8 @@
+// +build !windows
+
+package volume
+
+var (
+ testDestinationPath = "/foo"
+ testSourcePath = "/foo"
+)
diff --git a/volume/validate_test_windows.go b/volume/validate_test_windows.go
new file mode 100644
index 0000000..d5f86ac
--- /dev/null
+++ b/volume/validate_test_windows.go
@@ -0,0 +1,6 @@
+package volume
+
+var (
+ testDestinationPath = `c:\foo`
+ testSourcePath = `c:\foo`
+)
diff --git a/volume/volume.go b/volume/volume.go
new file mode 100644
index 0000000..2de248a
--- /dev/null
+++ b/volume/volume.go
@@ -0,0 +1,374 @@
+package volume
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "time"
+
+ mounttypes "github.com/docker/docker/api/types/mount"
+ "github.com/docker/docker/pkg/idtools"
+ "github.com/docker/docker/pkg/stringid"
+ "github.com/opencontainers/selinux/go-selinux/label"
+ "github.com/pkg/errors"
+)
+
+// DefaultDriverName is the driver name used for the driver
+// implemented in the local package.
+const DefaultDriverName = "local"
+
+// Scopes define if a volume has is cluster-wide (global) or local only.
+// Scopes are returned by the volume driver when it is queried for capabilities and then set on a volume
+const (
+ LocalScope = "local"
+ GlobalScope = "global"
+)
+
+// Driver is for creating and removing volumes.
+type Driver interface {
+ // Name returns the name of the volume driver.
+ Name() string
+ // Create makes a new volume with the given name.
+ Create(name string, opts map[string]string) (Volume, error)
+ // Remove deletes the volume.
+ Remove(vol Volume) (err error)
+ // List lists all the volumes the driver has
+ List() ([]Volume, error)
+ // Get retrieves the volume with the requested name
+ Get(name string) (Volume, error)
+ // Scope returns the scope of the driver (e.g. `global` or `local`).
+ // Scope determines how the driver is handled at a cluster level
+ Scope() string
+}
+
+// Capability defines a set of capabilities that a driver is able to handle.
+type Capability struct {
+ // Scope is the scope of the driver, `global` or `local`
+ // A `global` scope indicates that the driver manages volumes across the cluster
+ // A `local` scope indicates that the driver only manages volumes resources local to the host
+ // Scope is declared by the driver
+ Scope string
+}
+
+// Volume is a place to store data. It is backed by a specific driver, and can be mounted.
+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(id string) (string, error)
+ // Unmount unmounts the volume when it is no longer in use.
+ Unmount(id string) error
+ // CreatedAt returns Volume Creation time
+ CreatedAt() (time.Time, error)
+ // Status returns low-level status information about a volume
+ Status() map[string]interface{}
+}
+
+// DetailedVolume wraps a Volume with user-defined labels, options, and cluster scope (e.g., `local` or `global`)
+type DetailedVolume interface {
+ Labels() map[string]string
+ Options() map[string]string
+ Scope() string
+ 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
+// be mounted.
+type MountPoint struct {
+ // Source is the source path of the mount.
+ // E.g. `mount --bind /foo /bar`, `/foo` is the `Source`.
+ Source string
+ // Destination is the path relative to the container root (`/`) to the mount point
+ // It is where the `Source` is mounted to
+ Destination string
+ // RW is set to true when the mountpoint should be mounted as read-write
+ RW bool
+ // Name is the name reference to the underlying data defined by `Source`
+ // e.g., the volume name
+ Name string
+ // Driver is the volume driver used to create the volume (if it is a volume)
+ Driver string
+ // Type of mount to use, see `Type<foo>` definitions in github.com/docker/docker/api/types/mount
+ Type mounttypes.Type `json:",omitempty"`
+ // Volume is the volume providing data to this mountpoint.
+ // This is nil unless `Type` is set to `TypeVolume`
+ Volume Volume `json:"-"`
+
+ // Mode is the comma separated list of options supplied by the user when creating
+ // the bind/volume mount.
+ // Note Mode is not used on Windows
+ Mode string `json:"Relabel,omitempty"` // Originally field was `Relabel`"
+
+ // Propagation describes how the mounts are propagated from the host into the
+ // mount point, and vice-versa.
+ // See https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
+ // Note Propagation is not used on Windows
+ Propagation mounttypes.Propagation `json:",omitempty"` // Mount propagation string
+
+ // Specifies if data should be copied from the container before the first mount
+ // Use a pointer here so we can tell if the user set this value explicitly
+ // This allows us to error out when the user explicitly enabled copy but we can't copy due to the volume being populated
+ CopyData bool `json:"-"`
+ // ID is the opaque ID used to pass to the volume driver.
+ // This should be set by calls to `Mount` and unset by calls to `Unmount`
+ ID string `json:",omitempty"`
+
+ // Sepc is a copy of the API request that created this mount.
+ Spec mounttypes.Mount
+
+ // Track usage of this mountpoint
+ // Specicially needed for containers which are running and calls to `docker cp`
+ // because both these actions require mounting the volumes.
+ active int
+}
+
+// Cleanup frees resources used by the mountpoint
+func (m *MountPoint) Cleanup() error {
+ if m.Volume == nil || m.ID == "" {
+ return nil
+ }
+
+ if err := m.Volume.Unmount(m.ID); err != nil {
+ return errors.Wrapf(err, "error unmounting volume %s", m.Volume.Name())
+ }
+
+ m.active--
+ if m.active == 0 {
+ m.ID = ""
+ }
+ return nil
+}
+
+// Setup sets up a mount point by either mounting the volume if it is
+// configured, or creating the source directory if supplied.
+// The, optional, checkFun parameter allows doing additional checking
+// before creating the source directory on the host.
+func (m *MountPoint) Setup(mountLabel string, rootIDs idtools.IDPair, checkFun func(m *MountPoint) error) (path string, err error) {
+ defer func() {
+ if err != nil || !label.RelabelNeeded(m.Mode) {
+ return
+ }
+
+ err = label.Relabel(m.Source, mountLabel, label.IsShared(m.Mode))
+ if err == syscall.ENOTSUP {
+ err = nil
+ }
+ if err != nil {
+ path = ""
+ err = errors.Wrapf(err, "error setting label on mount source '%s'", m.Source)
+ }
+ }()
+
+ if m.Volume != nil {
+ id := m.ID
+ if id == "" {
+ id = stringid.GenerateNonCryptoID()
+ }
+ path, err := m.Volume.Mount(id)
+ if err != nil {
+ return "", errors.Wrapf(err, "error while mounting volume '%s'", m.Source)
+ }
+
+ m.ID = id
+ m.active++
+ return path, nil
+ }
+
+ if len(m.Source) == 0 {
+ return "", fmt.Errorf("Unable to setup mount point, neither source nor volume defined")
+ }
+
+ // system.MkdirAll() produces an error if m.Source exists and is a file (not a directory),
+ if m.Type == mounttypes.TypeBind {
+ // Before creating the source directory on the host, invoke checkFun if it's not nil. One of
+ // the use case is to forbid creating the daemon socket as a directory if the daemon is in
+ // the process of shutting down.
+ if checkFun != nil {
+ if err := checkFun(m); err != nil {
+ return "", err
+ }
+ }
+ // idtools.MkdirAllNewAs() produces an error if m.Source exists and is a file (not a directory)
+ // also, makes sure that if the directory is created, the correct remapped rootUID/rootGID will own it
+ if err := idtools.MkdirAllAndChownNew(m.Source, 0755, rootIDs); err != nil {
+ if perr, ok := err.(*os.PathError); ok {
+ if perr.Err != syscall.ENOTDIR {
+ return "", errors.Wrapf(err, "error while creating mount source path '%s'", m.Source)
+ }
+ }
+ }
+ }
+ return m.Source, nil
+}
+
+// Path returns the path of a volume in a mount point.
+func (m *MountPoint) Path() string {
+ if m.Volume != nil {
+ return m.Volume.Path()
+ }
+ return m.Source
+}
+
+// ParseVolumesFrom ensures that the supplied volumes-from is valid.
+func ParseVolumesFrom(spec string) (string, string, error) {
+ if len(spec) == 0 {
+ return "", "", fmt.Errorf("volumes-from specification cannot be an empty string")
+ }
+
+ specParts := strings.SplitN(spec, ":", 2)
+ id := specParts[0]
+ mode := "rw"
+
+ if len(specParts) == 2 {
+ mode = specParts[1]
+ if !ValidMountMode(mode) {
+ return "", "", errInvalidMode(mode)
+ }
+ // For now don't allow propagation properties while importing
+ // volumes from data container. These volumes will inherit
+ // the same propagation property as of the original volume
+ // in data container. This probably can be relaxed in future.
+ if HasPropagation(mode) {
+ return "", "", errInvalidMode(mode)
+ }
+ // Do not allow copy modes on volumes-from
+ if _, isSet := getCopyMode(mode); isSet {
+ return "", "", errInvalidMode(mode)
+ }
+ }
+ return id, mode, nil
+}
+
+// ParseMountRaw parses a raw volume spec (e.g. `-v /foo:/bar:shared`) into a
+// structured spec. Once the raw spec is parsed it relies on `ParseMountSpec` to
+// validate the spec and create a MountPoint
+func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
+ arr, err := splitRawSpec(convertSlash(raw))
+ if err != nil {
+ return nil, err
+ }
+
+ var spec mounttypes.Mount
+ var mode string
+ switch len(arr) {
+ case 1:
+ // Just a destination path in the container
+ spec.Target = arr[0]
+ case 2:
+ if ValidMountMode(arr[1]) {
+ // Destination + Mode is not a valid volume - volumes
+ // cannot include a mode. e.g. /foo:rw
+ return nil, errInvalidSpec(raw)
+ }
+ // Host Source Path or Name + Destination
+ spec.Source = arr[0]
+ spec.Target = arr[1]
+ case 3:
+ // HostSourcePath+DestinationPath+Mode
+ spec.Source = arr[0]
+ spec.Target = arr[1]
+ mode = arr[2]
+ default:
+ return nil, errInvalidSpec(raw)
+ }
+
+ if !ValidMountMode(mode) {
+ return nil, errInvalidMode(mode)
+ }
+
+ if filepath.IsAbs(spec.Source) {
+ spec.Type = mounttypes.TypeBind
+ } else {
+ spec.Type = mounttypes.TypeVolume
+ }
+
+ spec.ReadOnly = !ReadWrite(mode)
+
+ // cannot assume that if a volume driver is passed in that we should set it
+ if volumeDriver != "" && spec.Type == mounttypes.TypeVolume {
+ spec.VolumeOptions = &mounttypes.VolumeOptions{
+ DriverConfig: &mounttypes.Driver{Name: volumeDriver},
+ }
+ }
+
+ if copyData, isSet := getCopyMode(mode); isSet {
+ if spec.VolumeOptions == nil {
+ spec.VolumeOptions = &mounttypes.VolumeOptions{}
+ }
+ spec.VolumeOptions.NoCopy = !copyData
+ }
+ if HasPropagation(mode) {
+ spec.BindOptions = &mounttypes.BindOptions{
+ Propagation: GetPropagation(mode),
+ }
+ }
+
+ mp, err := ParseMountSpec(spec, platformRawValidationOpts...)
+ if mp != nil {
+ mp.Mode = mode
+ }
+ if err != nil {
+ err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
+ }
+ return mp, err
+}
+
+// ParseMountSpec reads a mount config, validates it, and configures a mountpoint from it.
+func ParseMountSpec(cfg mounttypes.Mount, options ...func(*validateOpts)) (*MountPoint, error) {
+ if err := validateMountConfig(&cfg, options...); err != nil {
+ return nil, err
+ }
+ mp := &MountPoint{
+ RW: !cfg.ReadOnly,
+ Destination: clean(convertSlash(cfg.Target)),
+ Type: cfg.Type,
+ Spec: cfg,
+ }
+
+ switch cfg.Type {
+ case mounttypes.TypeVolume:
+ if cfg.Source == "" {
+ mp.Name = stringid.GenerateNonCryptoID()
+ } else {
+ mp.Name = cfg.Source
+ }
+ mp.CopyData = DefaultCopyMode
+
+ if cfg.VolumeOptions != nil {
+ if cfg.VolumeOptions.DriverConfig != nil {
+ mp.Driver = cfg.VolumeOptions.DriverConfig.Name
+ }
+ if cfg.VolumeOptions.NoCopy {
+ mp.CopyData = false
+ }
+ }
+ case mounttypes.TypeBind:
+ mp.Source = clean(convertSlash(cfg.Source))
+ if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
+ mp.Propagation = cfg.BindOptions.Propagation
+ } else {
+ // If user did not specify a propagation mode, get
+ // default propagation mode.
+ mp.Propagation = DefaultPropagationMode
+ }
+ case mounttypes.TypeTmpfs:
+ // NOP
+ }
+ return mp, nil
+}
+
+func errInvalidMode(mode string) error {
+ return fmt.Errorf("invalid mode: %v", mode)
+}
+
+func errInvalidSpec(spec string) error {
+ return fmt.Errorf("invalid volume specification: '%s'", spec)
+}
diff --git a/volume/volume_copy.go b/volume/volume_copy.go
new file mode 100644
index 0000000..77f06a0
--- /dev/null
+++ b/volume/volume_copy.go
@@ -0,0 +1,23 @@
+package volume
+
+import "strings"
+
+// {<copy mode>=isEnabled}
+var copyModes = map[string]bool{
+ "nocopy": false,
+}
+
+func copyModeExists(mode string) bool {
+ _, exists := copyModes[mode]
+ return exists
+}
+
+// GetCopyMode gets the copy mode from the mode string for mounts
+func getCopyMode(mode string) (bool, bool) {
+ for _, o := range strings.Split(mode, ",") {
+ if isEnabled, exists := copyModes[o]; exists {
+ return isEnabled, true
+ }
+ }
+ return DefaultCopyMode, false
+}
diff --git a/volume/volume_copy_unix.go b/volume/volume_copy_unix.go
new file mode 100644
index 0000000..ad66e17
--- /dev/null
+++ b/volume/volume_copy_unix.go
@@ -0,0 +1,8 @@
+// +build !windows
+
+package volume
+
+const (
+ // DefaultCopyMode is the copy mode used by default for normal/named volumes
+ DefaultCopyMode = true
+)
diff --git a/volume/volume_copy_windows.go b/volume/volume_copy_windows.go
new file mode 100644
index 0000000..798638c
--- /dev/null
+++ b/volume/volume_copy_windows.go
@@ -0,0 +1,6 @@
+package volume
+
+const (
+ // DefaultCopyMode is the copy mode used by default for normal/named volumes
+ DefaultCopyMode = false
+)
diff --git a/volume/volume_linux.go b/volume/volume_linux.go
new file mode 100644
index 0000000..d4b4d80
--- /dev/null
+++ b/volume/volume_linux.go
@@ -0,0 +1,56 @@
+// +build linux
+
+package volume
+
+import (
+ "fmt"
+ "strings"
+
+ mounttypes "github.com/docker/docker/api/types/mount"
+)
+
+// ConvertTmpfsOptions converts *mounttypes.TmpfsOptions to the raw option string
+// for mount(2).
+func ConvertTmpfsOptions(opt *mounttypes.TmpfsOptions, readOnly bool) (string, error) {
+ var rawOpts []string
+ if readOnly {
+ rawOpts = append(rawOpts, "ro")
+ }
+
+ if opt != nil && opt.Mode != 0 {
+ rawOpts = append(rawOpts, fmt.Sprintf("mode=%o", opt.Mode))
+ }
+
+ if opt != nil && opt.SizeBytes != 0 {
+ // calculate suffix here, making this linux specific, but that is
+ // okay, since API is that way anyways.
+
+ // we do this by finding the suffix that divides evenly into the
+ // value, returing the value itself, with no suffix, if it fails.
+ //
+ // For the most part, we don't enforce any semantic to this values.
+ // The operating system will usually align this and enforce minimum
+ // and maximums.
+ var (
+ size = opt.SizeBytes
+ suffix string
+ )
+ for _, r := range []struct {
+ suffix string
+ divisor int64
+ }{
+ {"g", 1 << 30},
+ {"m", 1 << 20},
+ {"k", 1 << 10},
+ } {
+ if size%r.divisor == 0 {
+ size = size / r.divisor
+ suffix = r.suffix
+ break
+ }
+ }
+
+ rawOpts = append(rawOpts, fmt.Sprintf("size=%d%s", size, suffix))
+ }
+ return strings.Join(rawOpts, ","), nil
+}
diff --git a/volume/volume_linux_test.go b/volume/volume_linux_test.go
new file mode 100644
index 0000000..40ce552
--- /dev/null
+++ b/volume/volume_linux_test.go
@@ -0,0 +1,51 @@
+// +build linux
+
+package volume
+
+import (
+ "strings"
+ "testing"
+
+ mounttypes "github.com/docker/docker/api/types/mount"
+)
+
+func TestConvertTmpfsOptions(t *testing.T) {
+ type testCase struct {
+ opt mounttypes.TmpfsOptions
+ readOnly bool
+ expectedSubstrings []string
+ unexpectedSubstrings []string
+ }
+ cases := []testCase{
+ {
+ opt: mounttypes.TmpfsOptions{SizeBytes: 1024 * 1024, Mode: 0700},
+ readOnly: false,
+ expectedSubstrings: []string{"size=1m", "mode=700"},
+ unexpectedSubstrings: []string{"ro"},
+ },
+ {
+ opt: mounttypes.TmpfsOptions{},
+ readOnly: true,
+ expectedSubstrings: []string{"ro"},
+ unexpectedSubstrings: []string{},
+ },
+ }
+ for _, c := range cases {
+ data, err := ConvertTmpfsOptions(&c.opt, c.readOnly)
+ if err != nil {
+ t.Fatalf("could not convert %+v (readOnly: %v) to string: %v",
+ c.opt, c.readOnly, err)
+ }
+ t.Logf("data=%q", data)
+ for _, s := range c.expectedSubstrings {
+ if !strings.Contains(data, s) {
+ t.Fatalf("expected substring: %s, got %v (case=%+v)", s, data, c)
+ }
+ }
+ for _, s := range c.unexpectedSubstrings {
+ if strings.Contains(data, s) {
+ t.Fatalf("unexpected substring: %s, got %v (case=%+v)", s, data, c)
+ }
+ }
+ }
+}
diff --git a/volume/volume_propagation_linux.go b/volume/volume_propagation_linux.go
new file mode 100644
index 0000000..1de57ab
--- /dev/null
+++ b/volume/volume_propagation_linux.go
@@ -0,0 +1,47 @@
+// +build linux
+
+package volume
+
+import (
+ "strings"
+
+ mounttypes "github.com/docker/docker/api/types/mount"
+)
+
+// DefaultPropagationMode defines what propagation mode should be used by
+// default if user has not specified one explicitly.
+// propagation modes
+const DefaultPropagationMode = mounttypes.PropagationRPrivate
+
+var propagationModes = map[mounttypes.Propagation]bool{
+ mounttypes.PropagationPrivate: true,
+ mounttypes.PropagationRPrivate: true,
+ mounttypes.PropagationSlave: true,
+ mounttypes.PropagationRSlave: true,
+ mounttypes.PropagationShared: true,
+ mounttypes.PropagationRShared: true,
+}
+
+// GetPropagation extracts and returns the mount propagation mode. If there
+// are no specifications, then by default it is "private".
+func GetPropagation(mode string) mounttypes.Propagation {
+ for _, o := range strings.Split(mode, ",") {
+ prop := mounttypes.Propagation(o)
+ if propagationModes[prop] {
+ return prop
+ }
+ }
+ return DefaultPropagationMode
+}
+
+// HasPropagation checks if there is a valid propagation mode present in
+// passed string. Returns true if a valid propagation mode specifier is
+// present, false otherwise.
+func HasPropagation(mode string) bool {
+ for _, o := range strings.Split(mode, ",") {
+ if propagationModes[mounttypes.Propagation(o)] {
+ return true
+ }
+ }
+ return false
+}
diff --git a/volume/volume_propagation_linux_test.go b/volume/volume_propagation_linux_test.go
new file mode 100644
index 0000000..46d0265
--- /dev/null
+++ b/volume/volume_propagation_linux_test.go
@@ -0,0 +1,65 @@
+// +build linux
+
+package volume
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestParseMountRawPropagation(t *testing.T) {
+ var (
+ valid []string
+ invalid map[string]string
+ )
+
+ valid = []string{
+ "/hostPath:/containerPath:shared",
+ "/hostPath:/containerPath:rshared",
+ "/hostPath:/containerPath:slave",
+ "/hostPath:/containerPath:rslave",
+ "/hostPath:/containerPath:private",
+ "/hostPath:/containerPath:rprivate",
+ "/hostPath:/containerPath:ro,shared",
+ "/hostPath:/containerPath:ro,slave",
+ "/hostPath:/containerPath:ro,private",
+ "/hostPath:/containerPath:ro,z,shared",
+ "/hostPath:/containerPath:ro,Z,slave",
+ "/hostPath:/containerPath:Z,ro,slave",
+ "/hostPath:/containerPath:slave,Z,ro",
+ "/hostPath:/containerPath:Z,slave,ro",
+ "/hostPath:/containerPath:slave,ro,Z",
+ "/hostPath:/containerPath:rslave,ro,Z",
+ "/hostPath:/containerPath:ro,rshared,Z",
+ "/hostPath:/containerPath:ro,Z,rprivate",
+ }
+ invalid = map[string]string{
+ "/path:/path:ro,rshared,rslave": `invalid mode`,
+ "/path:/path:ro,z,rshared,rslave": `invalid mode`,
+ "/path:shared": "invalid volume specification",
+ "/path:slave": "invalid volume specification",
+ "/path:private": "invalid volume specification",
+ "name:/absolute-path:shared": "invalid volume specification",
+ "name:/absolute-path:rshared": "invalid volume specification",
+ "name:/absolute-path:slave": "invalid volume specification",
+ "name:/absolute-path:rslave": "invalid volume specification",
+ "name:/absolute-path:private": "invalid volume specification",
+ "name:/absolute-path:rprivate": "invalid volume specification",
+ }
+
+ for _, path := range valid {
+ if _, err := ParseMountRaw(path, "local"); err != nil {
+ t.Fatalf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
+ }
+ }
+
+ for path, expectedError := range invalid {
+ if _, err := ParseMountRaw(path, "local"); err == nil {
+ t.Fatalf("ParseMountRaw(`%q`) should have failed validation. Err %v", path, err)
+ } else {
+ if !strings.Contains(err.Error(), expectedError) {
+ t.Fatalf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
+ }
+ }
+ }
+}
diff --git a/volume/volume_propagation_unsupported.go b/volume/volume_propagation_unsupported.go
new file mode 100644
index 0000000..7311ffc
--- /dev/null
+++ b/volume/volume_propagation_unsupported.go
@@ -0,0 +1,24 @@
+// +build !linux
+
+package volume
+
+import mounttypes "github.com/docker/docker/api/types/mount"
+
+// DefaultPropagationMode is used only in linux. In other cases it returns
+// empty string.
+const DefaultPropagationMode mounttypes.Propagation = ""
+
+// propagation modes not supported on this platform.
+var propagationModes = map[mounttypes.Propagation]bool{}
+
+// GetPropagation is not supported. Return empty string.
+func GetPropagation(mode string) mounttypes.Propagation {
+ return DefaultPropagationMode
+}
+
+// HasPropagation checks if there is a valid propagation mode present in
+// passed string. Returns true if a valid propagation mode specifier is
+// present, false otherwise.
+func HasPropagation(mode string) bool {
+ return false
+}
diff --git a/volume/volume_test.go b/volume/volume_test.go
new file mode 100644
index 0000000..5c3e0e3
--- /dev/null
+++ b/volume/volume_test.go
@@ -0,0 +1,269 @@
+package volume
+
+import (
+ "io/ioutil"
+ "os"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/api/types/mount"
+)
+
+func TestParseMountRaw(t *testing.T) {
+ var (
+ valid []string
+ invalid map[string]string
+ )
+
+ if runtime.GOOS == "windows" {
+ valid = []string{
+ `d:\`,
+ `d:`,
+ `d:\path`,
+ `d:\path with space`,
+ `c:\:d:\`,
+ `c:\windows\:d:`,
+ `c:\windows:d:\s p a c e`,
+ `c:\windows:d:\s p a c e:RW`,
+ `c:\program files:d:\s p a c e i n h o s t d i r`,
+ `0123456789name:d:`,
+ `MiXeDcAsEnAmE:d:`,
+ `name:D:`,
+ `name:D::rW`,
+ `name:D::RW`,
+ `name:D::RO`,
+ `c:/:d:/forward/slashes/are/good/too`,
+ `c:/:d:/including with/spaces:ro`,
+ `c:\Windows`, // With capital
+ `c:\Program Files (x86)`, // With capitals and brackets
+ }
+ invalid = map[string]string{
+ ``: "invalid volume specification: ",
+ `.`: "invalid volume specification: ",
+ `..\`: "invalid volume specification: ",
+ `c:\:..\`: "invalid volume specification: ",
+ `c:\:d:\:xyzzy`: "invalid volume specification: ",
+ `c:`: "cannot be `c:`",
+ `c:\`: "cannot be `c:`",
+ `c:\notexist:d:`: `source path does not exist`,
+ `c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`,
+ `name<:d:`: `invalid volume specification`,
+ `name>:d:`: `invalid volume specification`,
+ `name::d:`: `invalid volume specification`,
+ `name":d:`: `invalid volume specification`,
+ `name\:d:`: `invalid volume specification`,
+ `name*:d:`: `invalid volume specification`,
+ `name|:d:`: `invalid volume specification`,
+ `name?:d:`: `invalid volume specification`,
+ `name/:d:`: `invalid volume specification`,
+ `d:\pathandmode:rw`: `invalid volume specification`,
+ `d:\pathandmode:ro`: `invalid volume specification`,
+ `con:d:`: `cannot be a reserved word for Windows filenames`,
+ `PRN:d:`: `cannot be a reserved word for Windows filenames`,
+ `aUx:d:`: `cannot be a reserved word for Windows filenames`,
+ `nul:d:`: `cannot be a reserved word for Windows filenames`,
+ `com1:d:`: `cannot be a reserved word for Windows filenames`,
+ `com2:d:`: `cannot be a reserved word for Windows filenames`,
+ `com3:d:`: `cannot be a reserved word for Windows filenames`,
+ `com4:d:`: `cannot be a reserved word for Windows filenames`,
+ `com5:d:`: `cannot be a reserved word for Windows filenames`,
+ `com6:d:`: `cannot be a reserved word for Windows filenames`,
+ `com7:d:`: `cannot be a reserved word for Windows filenames`,
+ `com8:d:`: `cannot be a reserved word for Windows filenames`,
+ `com9:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt1:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt2:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt3:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt4:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt5:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt6:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt7:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt8:d:`: `cannot be a reserved word for Windows filenames`,
+ `lpt9:d:`: `cannot be a reserved word for Windows filenames`,
+ `c:\windows\system32\ntdll.dll`: `Only directories can be mapped on this platform`,
+ }
+
+ } else {
+ valid = []string{
+ "/home",
+ "/home:/home",
+ "/home:/something/else",
+ "/with space",
+ "/home:/with space",
+ "relative:/absolute-path",
+ "hostPath:/containerPath:ro",
+ "/hostPath:/containerPath:rw",
+ "/rw:/ro",
+ }
+ invalid = map[string]string{
+ "": "invalid volume specification",
+ "./": "mount path must be absolute",
+ "../": "mount path must be absolute",
+ "/:../": "mount path must be absolute",
+ "/:path": "mount path must be absolute",
+ ":": "invalid volume specification",
+ "/tmp:": "invalid volume specification",
+ ":test": "invalid volume specification",
+ ":/test": "invalid volume specification",
+ "tmp:": "invalid volume specification",
+ ":test:": "invalid volume specification",
+ "::": "invalid volume specification",
+ ":::": "invalid volume specification",
+ "/tmp:::": "invalid volume specification",
+ ":/tmp::": "invalid volume specification",
+ "/path:rw": "invalid volume specification",
+ "/path:ro": "invalid volume specification",
+ "/rw:rw": "invalid volume specification",
+ "path:ro": "invalid volume specification",
+ "/path:/path:sw": `invalid mode`,
+ "/path:/path:rwz": `invalid mode`,
+ }
+ }
+
+ for _, path := range valid {
+ if _, err := ParseMountRaw(path, "local"); err != nil {
+ t.Fatalf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
+ }
+ }
+
+ for path, expectedError := range invalid {
+ if mp, err := ParseMountRaw(path, "local"); err == nil {
+ t.Fatalf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
+ } else {
+ if !strings.Contains(err.Error(), expectedError) {
+ t.Fatalf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
+ }
+ }
+ }
+}
+
+// testParseMountRaw is a structure used by TestParseMountRawSplit for
+// specifying test cases for the ParseMountRaw() function.
+type testParseMountRaw struct {
+ bind string
+ driver string
+ expDest string
+ expSource string
+ expName string
+ expDriver string
+ expRW bool
+ fail bool
+}
+
+func TestParseMountRawSplit(t *testing.T) {
+ var cases []testParseMountRaw
+ if runtime.GOOS == "windows" {
+ cases = []testParseMountRaw{
+ {`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false},
+ {`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false},
+ {`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false},
+ {`c:\:d:\:rw`, "local", `d:\`, `c:\`, ``, "", true, false},
+ {`c:\:d:\:foo`, "local", `d:\`, `c:\`, ``, "", false, true},
+ {`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false},
+ {`name:d:`, "local", `d:`, ``, `name`, "local", true, false},
+ {`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false},
+ {`name:c:`, "", ``, ``, ``, "", true, true},
+ {`driver/name:c:`, "", ``, ``, ``, "", true, true},
+ }
+ } else {
+ cases = []testParseMountRaw{
+ {"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false},
+ {"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false},
+ {"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false},
+ {"/tmp:/tmp4:foo", "", "", "", "", "", false, true},
+ {"name:/named1", "", "/named1", "", "name", "", true, false},
+ {"name:/named2", "external", "/named2", "", "name", "external", true, false},
+ {"name:/named3:ro", "local", "/named3", "", "name", "local", false, false},
+ {"local/name:/tmp:rw", "", "/tmp", "", "local/name", "", true, false},
+ {"/tmp:tmp", "", "", "", "", "", true, true},
+ }
+ }
+
+ for i, c := range cases {
+ t.Logf("case %d", i)
+ m, err := ParseMountRaw(c.bind, c.driver)
+ if c.fail {
+ if err == nil {
+ t.Fatalf("Expected error, was nil, for spec %s\n", c.bind)
+ }
+ continue
+ }
+
+ if m == nil || err != nil {
+ t.Fatalf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err.Error())
+ continue
+ }
+
+ if m.Destination != c.expDest {
+ t.Fatalf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
+ }
+
+ if m.Source != c.expSource {
+ t.Fatalf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind)
+ }
+
+ if m.Name != c.expName {
+ t.Fatalf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind)
+ }
+
+ if m.Driver != c.expDriver {
+ t.Fatalf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind)
+ }
+
+ if m.RW != c.expRW {
+ t.Fatalf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind)
+ }
+ }
+}
+
+func TestParseMountSpec(t *testing.T) {
+ type c struct {
+ input mount.Mount
+ expected MountPoint
+ }
+ testDir, err := ioutil.TempDir("", "test-mount-config")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(testDir)
+
+ cases := []c{
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: DefaultPropagationMode}},
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true, Propagation: DefaultPropagationMode}},
+ {mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: DefaultPropagationMode}},
+ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath + string(os.PathSeparator), ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: DefaultPropagationMode}},
+ {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: DefaultCopyMode}},
+ {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath + string(os.PathSeparator)}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: DefaultCopyMode}},
+ }
+
+ for i, c := range cases {
+ t.Logf("case %d", i)
+ mp, err := ParseMountSpec(c.input)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if c.expected.Type != mp.Type {
+ t.Fatalf("Expected mount types to match. Expected: '%s', Actual: '%s'", c.expected.Type, mp.Type)
+ }
+ if c.expected.Destination != mp.Destination {
+ t.Fatalf("Expected mount destination to match. Expected: '%s', Actual: '%s'", c.expected.Destination, mp.Destination)
+ }
+ if c.expected.Source != mp.Source {
+ t.Fatalf("Expected mount source to match. Expected: '%s', Actual: '%s'", c.expected.Source, mp.Source)
+ }
+ if c.expected.RW != mp.RW {
+ t.Fatalf("Expected mount writable to match. Expected: '%v', Actual: '%v'", c.expected.RW, mp.RW)
+ }
+ if c.expected.Propagation != mp.Propagation {
+ t.Fatalf("Expected mount propagation to match. Expected: '%v', Actual: '%s'", c.expected.Propagation, mp.Propagation)
+ }
+ if c.expected.Driver != mp.Driver {
+ t.Fatalf("Expected mount driver to match. Expected: '%v', Actual: '%s'", c.expected.Driver, mp.Driver)
+ }
+ if c.expected.CopyData != mp.CopyData {
+ t.Fatalf("Expected mount copy data to match. Expected: '%v', Actual: '%v'", c.expected.CopyData, mp.CopyData)
+ }
+ }
+}
diff --git a/volume/volume_unix.go b/volume/volume_unix.go
new file mode 100644
index 0000000..e35b70c
--- /dev/null
+++ b/volume/volume_unix.go
@@ -0,0 +1,148 @@
+// +build linux freebsd darwin solaris
+
+package volume
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ mounttypes "github.com/docker/docker/api/types/mount"
+)
+
+var platformRawValidationOpts = []func(o *validateOpts){
+ // need to make sure to not error out if the bind source does not exist on unix
+ // this is supported for historical reasons, the path will be automatically
+ // created later.
+ func(o *validateOpts) { o.skipBindSourceCheck = true },
+}
+
+// read-write modes
+var rwModes = map[string]bool{
+ "rw": true,
+ "ro": true,
+}
+
+// label modes
+var labelModes = map[string]bool{
+ "Z": true,
+ "z": true,
+}
+
+// consistency modes
+var consistencyModes = map[mounttypes.Consistency]bool{
+ mounttypes.ConsistencyFull: true,
+ mounttypes.ConsistencyCached: true,
+ mounttypes.ConsistencyDelegated: true,
+}
+
+// BackwardsCompatible decides whether this mount point can be
+// used in old versions of Docker or not.
+// Only bind mounts and local volumes can be used in old versions of Docker.
+func (m *MountPoint) BackwardsCompatible() bool {
+ return len(m.Source) > 0 || m.Driver == DefaultDriverName
+}
+
+// HasResource checks whether the given absolute path for a container is in
+// this mount point. If the relative path starts with `../` then the resource
+// is outside of this mount point, but we can't simply check for this prefix
+// because it misses `..` which is also outside of the mount, so check both.
+func (m *MountPoint) HasResource(absolutePath string) bool {
+ relPath, err := filepath.Rel(m.Destination, absolutePath)
+ return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
+}
+
+// IsVolumeNameValid checks a volume name in a platform specific manner.
+func IsVolumeNameValid(name string) (bool, error) {
+ return true, nil
+}
+
+// ValidMountMode will make sure the mount mode is valid.
+// returns if it's a valid mount mode or not.
+func ValidMountMode(mode string) bool {
+ if mode == "" {
+ return true
+ }
+
+ rwModeCount := 0
+ labelModeCount := 0
+ propagationModeCount := 0
+ copyModeCount := 0
+ consistencyModeCount := 0
+
+ for _, o := range strings.Split(mode, ",") {
+ switch {
+ case rwModes[o]:
+ rwModeCount++
+ case labelModes[o]:
+ labelModeCount++
+ case propagationModes[mounttypes.Propagation(o)]:
+ propagationModeCount++
+ case copyModeExists(o):
+ copyModeCount++
+ case consistencyModes[mounttypes.Consistency(o)]:
+ consistencyModeCount++
+ default:
+ return false
+ }
+ }
+
+ // Only one string for each mode is allowed.
+ if rwModeCount > 1 || labelModeCount > 1 || propagationModeCount > 1 || copyModeCount > 1 || consistencyModeCount > 1 {
+ return false
+ }
+ return true
+}
+
+// ReadWrite tells you if a mode string is a valid read-write mode or not.
+// If there are no specifications w.r.t read write mode, then by default
+// it returns true.
+func ReadWrite(mode string) bool {
+ if !ValidMountMode(mode) {
+ return false
+ }
+
+ for _, o := range strings.Split(mode, ",") {
+ if o == "ro" {
+ return false
+ }
+ }
+ return true
+}
+
+func validateNotRoot(p string) error {
+ p = filepath.Clean(convertSlash(p))
+ if p == "/" {
+ return fmt.Errorf("invalid specification: destination can't be '/'")
+ }
+ return nil
+}
+
+func validateCopyMode(mode bool) error {
+ return nil
+}
+
+func convertSlash(p string) string {
+ return filepath.ToSlash(p)
+}
+
+func splitRawSpec(raw string) ([]string, error) {
+ if strings.Count(raw, ":") > 2 {
+ return nil, errInvalidSpec(raw)
+ }
+
+ arr := strings.SplitN(raw, ":", 3)
+ if arr[0] == "" {
+ return nil, errInvalidSpec(raw)
+ }
+ return arr, nil
+}
+
+func clean(p string) string {
+ return filepath.Clean(p)
+}
+
+func validateStat(fi os.FileInfo) error {
+ return nil
+}
diff --git a/volume/volume_unsupported.go b/volume/volume_unsupported.go
new file mode 100644
index 0000000..ff9d6af
--- /dev/null
+++ b/volume/volume_unsupported.go
@@ -0,0 +1,16 @@
+// +build !linux
+
+package volume
+
+import (
+ "fmt"
+ "runtime"
+
+ mounttypes "github.com/docker/docker/api/types/mount"
+)
+
+// ConvertTmpfsOptions converts *mounttypes.TmpfsOptions to the raw option string
+// for mount(2).
+func ConvertTmpfsOptions(opt *mounttypes.TmpfsOptions, readOnly bool) (string, error) {
+ return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS)
+}
diff --git a/volume/volume_windows.go b/volume/volume_windows.go
new file mode 100644
index 0000000..22f6fc7
--- /dev/null
+++ b/volume/volume_windows.go
@@ -0,0 +1,201 @@
+package volume
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+// read-write modes
+var rwModes = map[string]bool{
+ "rw": true,
+}
+
+// read-only modes
+var roModes = map[string]bool{
+ "ro": true,
+}
+
+var platformRawValidationOpts = []func(*validateOpts){
+ // filepath.IsAbs is weird on Windows:
+ // `c:` is not considered an absolute path
+ // `c:\` is considered an absolute path
+ // In any case, the regex matching below ensures absolute paths
+ // TODO: consider this a bug with filepath.IsAbs (?)
+ func(o *validateOpts) { o.skipAbsolutePathCheck = true },
+}
+
+const (
+ // Spec should be in the format [source:]destination[:mode]
+ //
+ // Examples: c:\foo bar:d:rw
+ // c:\foo:d:\bar
+ // myname:d:
+ // d:\
+ //
+ // Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See
+ // https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to
+ // test is https://regex-golang.appspot.com/assets/html/index.html
+ //
+ // Useful link for referencing named capturing groups:
+ // http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex
+ //
+ // There are three match groups: source, destination and mode.
+ //
+
+ // RXHostDir is the first option of a source
+ RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*`
+ // RXName is the second option of a source
+ RXName = `[^\\/:*?"<>|\r\n]+`
+ // RXReservedNames are reserved names not possible on Windows
+ RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])`
+
+ // RXSource is the combined possibilities for a source
+ RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `))):)?`
+
+ // Source. Can be either a host directory, a name, or omitted:
+ // HostDir:
+ // - Essentially using the folder solution from
+ // https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
+ // but adding case insensitivity.
+ // - Must be an absolute path such as c:\path
+ // - Can include spaces such as `c:\program files`
+ // - And then followed by a colon which is not in the capture group
+ // - And can be optional
+ // Name:
+ // - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
+ // - And then followed by a colon which is not in the capture group
+ // - And can be optional
+
+ // RXDestination is the regex expression for the mount destination
+ RXDestination = `(?P<destination>([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))`
+ // Destination (aka container path):
+ // - Variation on hostdir but can be a drive followed by colon as well
+ // - If a path, must be absolute. Can include spaces
+ // - Drive cannot be c: (explicitly checked in code, not RegEx)
+
+ // RXMode is the regex expression for the mode of the mount
+ // Mode (optional):
+ // - Hopefully self explanatory in comparison to above regex's.
+ // - Colon is not in the capture group
+ RXMode = `(:(?P<mode>(?i)ro|rw))?`
+)
+
+// BackwardsCompatible decides whether this mount point can be
+// used in old versions of Docker or not.
+// Windows volumes are never backwards compatible.
+func (m *MountPoint) BackwardsCompatible() bool {
+ return false
+}
+
+func splitRawSpec(raw string) ([]string, error) {
+ specExp := regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`)
+ match := specExp.FindStringSubmatch(strings.ToLower(raw))
+
+ // Must have something back
+ if len(match) == 0 {
+ return nil, errInvalidSpec(raw)
+ }
+
+ var split []string
+ matchgroups := make(map[string]string)
+ // Pull out the sub expressions from the named capture groups
+ for i, name := range specExp.SubexpNames() {
+ matchgroups[name] = strings.ToLower(match[i])
+ }
+ if source, exists := matchgroups["source"]; exists {
+ if source != "" {
+ split = append(split, source)
+ }
+ }
+ if destination, exists := matchgroups["destination"]; exists {
+ if destination != "" {
+ split = append(split, destination)
+ }
+ }
+ if mode, exists := matchgroups["mode"]; exists {
+ if mode != "" {
+ split = append(split, mode)
+ }
+ }
+ // Fix #26329. If the destination appears to be a file, and the source is null,
+ // it may be because we've fallen through the possible naming regex and hit a
+ // situation where the user intention was to map a file into a container through
+ // a local volume, but this is not supported by the platform.
+ if matchgroups["source"] == "" && matchgroups["destination"] != "" {
+ validName, err := IsVolumeNameValid(matchgroups["destination"])
+ if err != nil {
+ return nil, err
+ }
+ if !validName {
+ if fi, err := os.Stat(matchgroups["destination"]); err == nil {
+ if !fi.IsDir() {
+ return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
+ }
+ }
+ }
+ }
+ return split, nil
+}
+
+// IsVolumeNameValid checks a volume name in a platform specific manner.
+func IsVolumeNameValid(name string) (bool, error) {
+ nameExp := regexp.MustCompile(`^` + RXName + `$`)
+ if !nameExp.MatchString(name) {
+ return false, nil
+ }
+ nameExp = regexp.MustCompile(`^` + RXReservedNames + `$`)
+ if nameExp.MatchString(name) {
+ return false, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
+ }
+ return true, nil
+}
+
+// ValidMountMode will make sure the mount mode is valid.
+// returns if it's a valid mount mode or not.
+func ValidMountMode(mode string) bool {
+ if mode == "" {
+ return true
+ }
+ return roModes[strings.ToLower(mode)] || rwModes[strings.ToLower(mode)]
+}
+
+// ReadWrite tells you if a mode string is a valid read-write mode or not.
+func ReadWrite(mode string) bool {
+ return rwModes[strings.ToLower(mode)] || mode == ""
+}
+
+func validateNotRoot(p string) error {
+ p = strings.ToLower(convertSlash(p))
+ if p == "c:" || p == `c:\` {
+ return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
+ }
+ return nil
+}
+
+func validateCopyMode(mode bool) error {
+ if mode {
+ return fmt.Errorf("Windows does not support copying image path content")
+ }
+ return nil
+}
+
+func convertSlash(p string) string {
+ return filepath.FromSlash(p)
+}
+
+func clean(p string) string {
+ if match, _ := regexp.MatchString("^[a-z]:$", p); match {
+ return p
+ }
+ return filepath.Clean(p)
+}
+
+func validateStat(fi os.FileInfo) error {
+ if !fi.IsDir() {
+ return fmt.Errorf("source path must be a directory")
+ }
+ return nil
+}