Initial commit

This commit is contained in:
Ria Bhatia
2017-12-04 13:32:57 -06:00
committed by Erik St. Martin
commit 0075e5b0f3
9056 changed files with 2523100 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
package volumedrivers
import (
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/volume"
)
type volumeDriverAdapter struct {
name string
proxy *volumeDriverProxy
}
func (a *volumeDriverAdapter) Name() string {
return a.name
}
func (a *volumeDriverAdapter) Create(name string, opts map[string]string) (volume.Volume, error) {
err := a.proxy.Create(name, opts)
if err != nil {
return nil, err
}
return &volumeAdapter{
proxy: a.proxy,
name: name,
driverName: a.name}, nil
}
func (a *volumeDriverAdapter) Remove(v volume.Volume) error {
return a.proxy.Remove(v.Name())
}
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,
driverName: a.name,
eMount: vp.Mountpoint,
})
}
return out, nil
}
func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) {
v, err := a.proxy.Get(name)
if err != nil {
// TODO: remove this hack. Allows back compat with volume drivers that don't support this call
if !plugins.IsNotFound(err) {
return nil, err
}
return a.Create(name, nil)
}
return &volumeAdapter{
proxy: a.proxy,
name: v.Name,
driverName: a.Name(),
eMount: v.Mountpoint,
}, nil
}
type volumeAdapter struct {
proxy *volumeDriverProxy
name string
driverName string
eMount string // ephemeral host volume path
}
type proxyVolume struct {
Name string
Mountpoint string
}
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 {
return a.eMount
}
m, _ := a.proxy.Path(a.name)
return m
}
func (a *volumeAdapter) Mount() (string, error) {
var err error
a.eMount, err = a.proxy.Mount(a.name)
return a.eMount, err
}
func (a *volumeAdapter) Unmount() error {
return a.proxy.Unmount(a.name)
}

View File

@@ -0,0 +1,164 @@
//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type volumeDriver -name VolumeDriver
package volumedrivers
import (
"fmt"
"sync"
"github.com/docker/docker/pkg/locker"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/volume"
)
// currently created by hand. generation tool would generate this like:
// $ extpoint-gen Driver > volume/extpoint.go
var drivers = &driverExtpoint{extensions: make(map[string]volume.Driver), driverLock: &locker.Locker{}}
const extName = "VolumeDriver"
// NewVolumeDriver returns a driver has the given name mapped on the given client.
func NewVolumeDriver(name string, c client) volume.Driver {
proxy := &volumeDriverProxy{c}
return &volumeDriverAdapter{name, proxy}
}
type opts map[string]string
type list []*proxyVolume
// 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 opts) (err error)
// Remove the volume with the given name
Remove(name string) (err error)
// Get the mountpoint of the given volume
Path(name string) (mountpoint string, err error)
// Mount the given volume and return the mountpoint
Mount(name string) (mountpoint string, err error)
// Unmount the given volume
Unmount(name string) (err error)
// List lists all the volumes known to the driver
List() (volumes list, err error)
// Get retreives the volume with the requested name
Get(name string) (volume *proxyVolume, err error)
}
type driverExtpoint struct {
extensions map[string]volume.Driver
sync.Mutex
driverLock *locker.Locker
}
// 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
}
drivers.extensions[name] = extension
return true
}
// Unregister dissociates the name from it's 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) (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
}
pl, err := plugins.Get(name, extName)
if err != nil {
return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err)
}
drivers.Lock()
defer drivers.Unlock()
if ext, ok := drivers.extensions[name]; ok {
return ext, nil
}
d := NewVolumeDriver(name, pl.Client)
drivers.extensions[name] = d
return d, nil
}
// GetDriver returns a volume driver by it's 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)
}
// 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()
return driverList
}
// GetAllDrivers lists all the registered drivers
func GetAllDrivers() ([]volume.Driver, error) {
plugins, err := plugins.GetAll(extName)
if err != nil {
return nil, err
}
var ds []volume.Driver
drivers.Lock()
defer drivers.Unlock()
for _, d := range drivers.extensions {
ds = append(ds, d)
}
for _, p := range plugins {
ext, ok := drivers.extensions[p.Name]
if ok {
continue
}
ext = NewVolumeDriver(p.Name, p.Client)
drivers.extensions[p.Name] = ext
ds = append(ds, ext)
}
return ds, nil
}

View File

@@ -0,0 +1,23 @@
package volumedrivers
import (
"testing"
"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())
}
}

View File

@@ -0,0 +1,207 @@
// generated code - DO NOT EDIT
package volumedrivers
import "errors"
type client interface {
Call(string, interface{}, interface{}) error
}
type volumeDriverProxy struct {
client
}
type volumeDriverProxyCreateRequest struct {
Name string
Opts opts
}
type volumeDriverProxyCreateResponse struct {
Err string
}
func (pp *volumeDriverProxy) Create(name string, opts opts) (err error) {
var (
req volumeDriverProxyCreateRequest
ret volumeDriverProxyCreateResponse
)
req.Name = name
req.Opts = opts
if err = pp.Call("VolumeDriver.Create", req, &ret); err != nil {
return
}
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
}
type volumeDriverProxyMountResponse struct {
Mountpoint string
Err string
}
func (pp *volumeDriverProxy) Mount(name string) (mountpoint string, err error) {
var (
req volumeDriverProxyMountRequest
ret volumeDriverProxyMountResponse
)
req.Name = name
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
}
type volumeDriverProxyUnmountResponse struct {
Err string
}
func (pp *volumeDriverProxy) Unmount(name string) (err error) {
var (
req volumeDriverProxyUnmountRequest
ret volumeDriverProxyUnmountResponse
)
req.Name = name
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 list
Err string
}
func (pp *volumeDriverProxy) List() (volumes list, 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
}

View File

@@ -0,0 +1,122 @@
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"}`)
})
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")
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")
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)
}
}

View File

@@ -0,0 +1,232 @@
// 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 (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
derr "github.com/docker/docker/errors"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/utils"
"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 = errors.New("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 = utils.RestrictedVolumeNamePattern
)
// 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, rootUID, rootGID int) (*Root, error) {
rootDirectory := filepath.Join(scope, volumesPathName)
if err := idtools.MkdirAllAs(rootDirectory, 0700, rootUID, rootGID); err != nil {
return nil, err
}
r := &Root{
scope: scope,
path: rootDirectory,
volumes: make(map[string]*localVolume),
rootUID: rootUID,
rootGID: rootGID,
}
dirs, err := ioutil.ReadDir(rootDirectory)
if err != nil {
return nil, err
}
for _, d := range dirs {
name := filepath.Base(d.Name())
r.volumes[name] = &localVolume{
driverName: r.Name(),
name: name,
path: r.DataPath(name),
}
}
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
rootUID int
rootGID int
}
// List lists all the volumes
func (r *Root) List() ([]volume.Volume, error) {
var ls []volume.Volume
for _, v := range r.volumes {
ls = append(ls, v)
}
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, _ 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.MkdirAllAs(path, 0755, r.rootUID, r.rootGID); err != nil {
if os.IsExist(err) {
return nil, fmt.Errorf("volume already exists under %s", filepath.Dir(path))
}
return nil, err
}
v = &localVolume{
driverName: r.Name(),
name: name,
path: path,
}
r.volumes[name] = v
return v, nil
}
// 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 errors.New("unknown volume type")
}
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 err
}
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
}
func (r *Root) validateName(name string) error {
if !volumeNameRegex.MatchString(name) {
return derr.ErrorCodeVolumeName.WithArgs(name, utils.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
usedCount int
// unique name of the volume
name string
// path is the path on the host where the data lives
path string
// driverName is the name of the driver that created the volume.
driverName string
}
// 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.
func (v *localVolume) Mount() (string, error) {
return v.path, nil
}
// Umount is for satisfying the localVolume interface and does not do anything in this driver.
func (v *localVolume) Unmount() error {
return nil
}

View File

@@ -0,0 +1,147 @@
package local
import (
"io/ioutil"
"os"
"testing"
)
func TestRemove(t *testing.T) {
rootDir, err := ioutil.TempDir("", "local-volume-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootDir)
r, err := New(rootDir, 0, 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, 0, 0)
if err != nil {
t.Fatal(err)
}
vol, err := r.Create("testing", nil)
if err != nil {
t.Fatal(err)
}
r, err = New(rootDir, 0, 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, 0, 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)
}
}
}
}
func TestValidateName(t *testing.T) {
r := &Root{}
names := map[string]bool{
"/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)
}
}
}

View File

@@ -0,0 +1,29 @@
// +build linux freebsd
// 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 (
"path/filepath"
"strings"
)
var oldVfsDir = filepath.Join("vfs", "dir")
// 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
}

View File

@@ -0,0 +1,18 @@
// 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 (
"path/filepath"
"strings"
)
// 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
}

View File

@@ -0,0 +1,74 @@
package store
import (
"errors"
"strings"
)
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("conflict: 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 {
switch pe := err.(type) {
case nil:
return false
case *OpErr:
err = pe.Err
}
return err == expected
}

View File

@@ -0,0 +1,366 @@
package store
import (
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/locker"
"github.com/docker/docker/volume"
"github.com/docker/docker/volume/drivers"
)
// New initializes a VolumeStore to keep
// reference counting of volumes in the system.
func New() *VolumeStore {
return &VolumeStore{
locks: &locker.Locker{},
names: make(map[string]volume.Volume),
refs: make(map[string][]string),
}
}
func (s *VolumeStore) getNamed(name string) (volume.Volume, bool) {
s.globalLock.Lock()
v, exists := s.names[name]
s.globalLock.Unlock()
return v, exists
}
func (s *VolumeStore) setNamed(v volume.Volume, ref string) {
s.globalLock.Lock()
s.names[v.Name()] = v
if len(ref) > 0 {
s.refs[v.Name()] = append(s.refs[v.Name()], ref)
}
s.globalLock.Unlock()
}
func (s *VolumeStore) purge(name string) {
s.globalLock.Lock()
delete(s.names, name)
delete(s.refs, 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 *locker.Locker
globalLock sync.Mutex
// names stores the volume name -> driver name 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][]string
}
// 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 a 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)
if !exists {
s.setNamed(v, "")
}
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) {
drivers, err := volumedrivers.GetAllDrivers()
if err != nil {
return nil, nil, err
}
var (
ls []volume.Volume
warnings []string
)
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
}
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 {
for _, v := range s.names {
if _, exists := badDrivers[v.DriverName()]; exists {
ls = append(ls, v)
}
}
}
return ls, warnings, nil
}
// CreateWithRef creates a volume with the given name and driver and stores the ref
// This is just like Create() except we store the reference while holding the lock.
// This ensures there's no race between creating a volume and then storing a reference.
func (s *VolumeStore) CreateWithRef(name, driverName, ref string, opts 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)
if err != nil {
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.
func (s *VolumeStore) Create(name, driverName string, opts 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)
if err != nil {
return nil, &OpErr{Err: err, Name: name, Op: "create"}
}
s.setNamed(v, "")
return v, 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.
// It is expected that callers of this function hold any neccessary locks.
func (s *VolumeStore) create(name, driverName string, opts 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"}
}
if v, exists := s.getNamed(name); exists {
if v.DriverName() != driverName && driverName != "" && driverName != volume.DefaultDriverName {
return nil, errNameConflict
}
return v, nil
}
logrus.Debugf("Registering new volume reference: driver %s, name %s", driverName, name)
vd, err := volumedrivers.GetDriver(driverName)
if err != nil {
return nil, &OpErr{Op: "create", Name: name, Err: err}
}
if v, err := vd.Get(name); err == nil {
return v, nil
}
return vd.Create(name, opts)
}
// 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 existance 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)
return v, nil
}
// Get looks if a volume with the given name exists and returns it if so
func (s *VolumeStore) Get(name string) (volume.Volume, error) {
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
}
// get requests the volume, if the driver info is stored it just access 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 neccessary locks
func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
logrus.Debugf("Getting volume reference for name: %s", name)
if v, exists := s.names[name]; exists {
vd, err := volumedrivers.GetDriver(v.DriverName())
if err != nil {
return nil, err
}
return vd.Get(name)
}
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 {
continue
}
return v, nil
}
return nil, errNoSuchVolume
}
// 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 refs, exists := s.refs[name]; exists && len(refs) > 0 {
return &OpErr{Err: errVolumeInUse, Name: v.Name(), Op: "remove", Refs: refs}
}
vd, err := volumedrivers.GetDriver(v.DriverName())
if err != nil {
return &OpErr{Err: err, Name: vd.Name(), Op: "remove"}
}
logrus.Debugf("Removing volume reference: driver %s, name %s", v.DriverName(), name)
if err := vd.Remove(v); 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) {
s.locks.Lock(v.Name())
defer s.locks.Unlock(v.Name())
s.globalLock.Lock()
defer s.globalLock.Unlock()
refs, exists := s.refs[v.Name()]
if !exists {
return
}
for i, r := range refs {
if r == ref {
s.refs[v.Name()] = append(s.refs[v.Name()][:i], s.refs[v.Name()][i+1:]...)
}
}
}
// Refs gets the current list of refs for the given volume
func (s *VolumeStore) Refs(v volume.Volume) []string {
s.locks.Lock(v.Name())
defer s.locks.Unlock(v.Name())
s.globalLock.Lock()
defer s.globalLock.Unlock()
refs, exists := s.refs[v.Name()]
if !exists {
return nil
}
refsOut := make([]string, len(refs))
copy(refsOut, refs)
return refsOut
}
// 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"}
}
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())
l := len(s.refs[v.Name()])
s.locks.Unlock(v.Name())
if (used && l > 0) || (!used && l == 0) {
return true
}
return false
})
}
// 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
}

View File

@@ -0,0 +1,159 @@
package store
import (
"errors"
"strings"
"testing"
"github.com/docker/docker/volume/drivers"
vt "github.com/docker/docker/volume/testutils"
)
func TestCreate(t *testing.T) {
volumedrivers.Register(vt.NewFakeDriver("fake"), "fake")
defer volumedrivers.Unregister("fake")
s := New()
v, err := s.Create("fake1", "fake", 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); err == nil {
t.Fatalf("Expected unknown driver error, got nil")
}
_, err = s.Create("fakeerror", "fake", map[string]string{"error": "create error"})
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(vt.NewFakeDriver("fake"), "fake")
volumedrivers.Register(vt.NewFakeDriver("noop"), "noop")
defer volumedrivers.Unregister("fake")
defer volumedrivers.Unregister("noop")
s := New()
// doing string compare here since this error comes directly from the driver
expected := "no such volume"
if err := s.Remove(vt.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)
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(vt.NewFakeDriver("fake"), "fake")
volumedrivers.Register(vt.NewFakeDriver("fake2"), "fake2")
defer volumedrivers.Unregister("fake")
defer volumedrivers.Unregister("fake2")
s := New()
if _, err := s.Create("test", "fake", nil); err != nil {
t.Fatal(err)
}
if _, err := s.Create("test2", "fake2", 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))
}
// and again with a new store
s = New()
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(vt.NewFakeDriver("fake"), "fake")
volumedrivers.Register(vt.NewFakeDriver("noop"), "noop")
defer volumedrivers.Unregister("fake")
defer volumedrivers.Unregister("noop")
s := New()
if _, err := s.Create("fake1", "fake", nil); err != nil {
t.Fatal(err)
}
if _, err := s.Create("fake2", "fake", nil); err != nil {
t.Fatal(err)
}
if _, err := s.Create("fake3", "noop", 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(vt.NewFakeDriver("fake"), "fake")
volumedrivers.Register(vt.NewFakeDriver("noop"), "noop")
s := New()
if _, err := s.CreateWithRef("fake1", "fake", "volReference", nil); err != nil {
t.Fatal(err)
}
if _, err := s.Create("fake2", "fake", 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 danging volume, got %v", len(dangling))
}
if dangling[0].Name() != "fake2" {
t.Fatalf("expected danging 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())
}
}

View File

@@ -0,0 +1,9 @@
// +build linux freebsd
package store
// normaliseVolumeName is a platform specific function to normalise the name
// of a volume. This is a no-op on Unix-like platforms
func normaliseVolumeName(name string) string {
return name
}

View File

@@ -0,0 +1,12 @@
package store
import "strings"
// normaliseVolumeName is a platform specific function to normalise the name
// of a volume. On Windows, as NTFS is case insensitive, under
// c:\ProgramData\Docker\Volumes\, the folders John and john would be synonymous.
// Hence we can't allow the volume "John" and "john" to be created as separate
// volumes.
func normaliseVolumeName(name string) string {
return strings.ToLower(name)
}

View File

@@ -0,0 +1,104 @@
package volumetestutils
import (
"fmt"
"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, error) { return "noop", nil }
// Unmount unmounts the volume from the container
func (NoopVolume) Unmount() error { return nil }
// FakeVolume is a fake volume with a random name
type FakeVolume struct {
name string
}
// NewFakeVolume creates a new fake volume for testing
func NewFakeVolume(name string) volume.Volume {
return FakeVolume{name: name}
}
// Name is the name of the volume
func (f FakeVolume) Name() string { return f.name }
// DriverName is the name of the driver
func (FakeVolume) DriverName() string { return "fake" }
// 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, error) { return "fake", nil }
// Unmount unmounts the volume from the container
func (FakeVolume) Unmount() error { return 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.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")
}

120
vendor/github.com/hyperhq/hypercli/volume/volume.go generated vendored Normal file
View File

@@ -0,0 +1,120 @@
package volume
import (
"os"
"runtime"
"strings"
"github.com/Sirupsen/logrus"
derr "github.com/docker/docker/errors"
"github.com/docker/docker/pkg/system"
)
// DefaultDriverName is the driver name used for the driver
// implemented in the local package.
const DefaultDriverName string = "local"
// 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 id.
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 retreives the volume with the requested name
Get(name string) (Volume, error)
}
// 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() (string, error)
// Unmount unmounts the volume when it is no longer in use.
Unmount() error
}
// MountPoint is the intersection point between a volume and a container. It
// specifies which volume is to be used and where inside a container it should
// be mounted.
type MountPoint struct {
Source string // Container host directory
Destination string // Inside the container
RW bool // True if writable
Name string // Name set by user
Driver string // Volume driver to use
Volume Volume `json:"-"`
// Note Mode is not used on Windows
Mode string `json:"Relabel"` // Originally field was `Relabel`"
// Note Propagation is not used on Windows
Propagation string // Mount propagation string
Named bool // specifies if the mountpoint was specified by name
}
// Setup sets up a mount point by either mounting the volume if it is
// configured, or creating the source directory if supplied.
func (m *MountPoint) Setup() (string, error) {
if m.Volume != nil {
return m.Volume.Mount()
}
if len(m.Source) > 0 {
if _, err := os.Stat(m.Source); err != nil {
if !os.IsNotExist(err) {
return "", err
}
if runtime.GOOS != "windows" { // Windows does not have deprecation issues here
logrus.Warnf("Auto-creating non-existent volume host path %s, this is deprecated and will be removed soon", m.Source)
if err := system.MkdirAll(m.Source, 0755); err != nil {
return "", err
}
}
}
return m.Source, nil
}
return "", derr.ErrorCodeMountSetup
}
// Path returns the path of a volume in a mount point.
func (m *MountPoint) Path() string {
if m.Volume != nil {
return m.Volume.Path()
}
return m.Source
}
// ParseVolumesFrom ensure that the supplied volumes-from is valid.
func ParseVolumesFrom(spec string) (string, string, error) {
if len(spec) == 0 {
return "", "", derr.ErrorCodeVolumeFromBlank.WithArgs(spec)
}
specParts := strings.SplitN(spec, ":", 2)
id := specParts[0]
mode := "rw"
if len(specParts) == 2 {
mode = specParts[1]
if !ValidMountMode(mode) {
return "", "", derr.ErrorCodeVolumeInvalidMode.WithArgs(mode)
}
// 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 "", "", derr.ErrorCodeVolumeInvalidMode.WithArgs(mode)
}
}
return id, mode, nil
}

View File

@@ -0,0 +1,44 @@
// +build linux
package volume
import (
"strings"
)
// DefaultPropagationMode defines what propagation mode should be used by
// default if user has not specified one explicitly.
const DefaultPropagationMode string = "rprivate"
// propagation modes
var propagationModes = map[string]bool{
"private": true,
"rprivate": true,
"slave": true,
"rslave": true,
"shared": true,
"rshared": true,
}
// GetPropagation extracts and returns the mount propagation mode. If there
// are no specifications, then by default it is "private".
func GetPropagation(mode string) string {
for _, o := range strings.Split(mode, ",") {
if propagationModes[o] {
return o
}
}
return DefaultPropagationMode
}
// HasPropagation checks if there is a valid propagation mode present in
// passed string. Returns true if a valid propagatio mode specifier is
// present, false otherwise.
func HasPropagation(mode string) bool {
for _, o := range strings.Split(mode, ",") {
if propagationModes[o] {
return true
}
}
return false
}

View File

@@ -0,0 +1,65 @@
// +build linux
package volume
import (
"strings"
"testing"
)
func TestParseMountSpecPropagation(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: "ro,rshared,rslave"`,
"/path:/path:ro,z,rshared,rslave": `invalid mode: "ro,z,rshared,rslave"`,
"/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 := ParseMountSpec(path, "local"); err != nil {
t.Fatalf("ParseMountSpec(`%q`) should succeed: error %q", path, err)
}
}
for path, expectedError := range invalid {
if _, err := ParseMountSpec(path, "local"); err == nil {
t.Fatalf("ParseMountSpec(`%q`) should have failed validation. Err %v", path, err)
} else {
if !strings.Contains(err.Error(), expectedError) {
t.Fatalf("ParseMountSpec(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
}
}
}
}

View File

@@ -0,0 +1,22 @@
// +build !linux
package volume
// DefaultPropagationMode is used only in linux. In other cases it returns
// empty string.
const DefaultPropagationMode string = ""
// propagation modes not supported on this platform.
var propagationModes = map[string]bool{}
// GetPropagation is not supported. Return empty string.
func GetPropagation(mode string) string {
return DefaultPropagationMode
}
// HasPropagation checks if there is a valid propagation mode present in
// passed string. Returns true if a valid propagatio mode specifier is
// present, false otherwise.
func HasPropagation(mode string) bool {
return false
}

View File

@@ -0,0 +1,212 @@
package volume
import (
"runtime"
"strings"
"testing"
)
func TestParseMountSpec(t *testing.T) {
var (
valid []string
invalid map[string]string
)
if runtime.GOOS == "windows" {
valid = []string{
`d:\`,
`d:`,
`d:\path`,
`d:\path with space`,
// TODO Windows post TP4 - readonly support `d:\pathandmode:ro`,
`c:\:d:\`,
`c:\windows\:d:`,
`c:\windows:d:\s p a c e`,
`c:\windows:d:\s p a c e:RW`,
`c:\program files:d:\s p a c e i n h o s t d i r`,
`0123456789name:d:`,
`MiXeDcAsEnAmE:d:`,
`name:D:`,
`name:D::rW`,
`name:D::RW`,
// TODO Windows post TP4 - readonly support `name:D::RO`,
`c:/:d:/forward/slashes/are/good/too`,
// TODO Windows post TP4 - readonly support `c:/:d:/including with/spaces:ro`,
`c:\Windows`, // With capital
`c:\Program Files (x86)`, // With capitals and brackets
}
invalid = map[string]string{
``: "Invalid volume specification: ",
`.`: "Invalid volume specification: ",
`..\`: "Invalid volume specification: ",
`c:\:..\`: "Invalid volume specification: ",
`c:\:d:\:xyzzy`: "Invalid volume specification: ",
`c:`: "cannot be c:",
`c:\`: `cannot be c:\`,
`c:\notexist:d:`: `The system cannot find the file specified`,
`c:\windows\system32\ntdll.dll:d:`: `Source 'c:\windows\system32\ntdll.dll' is not a directory`,
`name<:d:`: `Invalid volume specification`,
`name>:d:`: `Invalid volume specification`,
`name::d:`: `Invalid volume specification`,
`name":d:`: `Invalid volume specification`,
`name\:d:`: `Invalid volume specification`,
`name*:d:`: `Invalid volume specification`,
`name|:d:`: `Invalid volume specification`,
`name?:d:`: `Invalid volume specification`,
`name/:d:`: `Invalid volume specification`,
`d:\pathandmode:rw`: `Invalid volume specification`,
`con:d:`: `cannot be a reserved word for Windows filenames`,
`PRN:d:`: `cannot be a reserved word for Windows filenames`,
`aUx:d:`: `cannot be a reserved word for Windows filenames`,
`nul:d:`: `cannot be a reserved word for Windows filenames`,
`com1:d:`: `cannot be a reserved word for Windows filenames`,
`com2:d:`: `cannot be a reserved word for Windows filenames`,
`com3:d:`: `cannot be a reserved word for Windows filenames`,
`com4:d:`: `cannot be a reserved word for Windows filenames`,
`com5:d:`: `cannot be a reserved word for Windows filenames`,
`com6:d:`: `cannot be a reserved word for Windows filenames`,
`com7:d:`: `cannot be a reserved word for Windows filenames`,
`com8:d:`: `cannot be a reserved word for Windows filenames`,
`com9:d:`: `cannot be a reserved word for Windows filenames`,
`lpt1:d:`: `cannot be a reserved word for Windows filenames`,
`lpt2:d:`: `cannot be a reserved word for Windows filenames`,
`lpt3:d:`: `cannot be a reserved word for Windows filenames`,
`lpt4:d:`: `cannot be a reserved word for Windows filenames`,
`lpt5:d:`: `cannot be a reserved word for Windows filenames`,
`lpt6:d:`: `cannot be a reserved word for Windows filenames`,
`lpt7:d:`: `cannot be a reserved word for Windows filenames`,
`lpt8:d:`: `cannot be a reserved word for Windows filenames`,
`lpt9:d:`: `cannot be a reserved word for Windows filenames`,
}
} else {
valid = []string{
"/home",
"/home:/home",
"/home:/something/else",
"/with space",
"/home:/with space",
"relative:/absolute-path",
"hostPath:/containerPath:ro",
"/hostPath:/containerPath:rw",
"/rw:/ro",
}
invalid = map[string]string{
"": "Invalid volume specification",
"./": "Invalid volume destination",
"../": "Invalid volume destination",
"/:../": "Invalid volume destination",
"/:path": "Invalid volume destination",
":": "Invalid volume specification",
"/tmp:": "Invalid volume destination",
":test": "Invalid volume specification",
":/test": "Invalid volume specification",
"tmp:": "Invalid volume destination",
":test:": "Invalid volume specification",
"::": "Invalid volume specification",
":::": "Invalid volume specification",
"/tmp:::": "Invalid volume specification",
":/tmp::": "Invalid volume specification",
"/path:rw": "Invalid volume specification",
"/path:ro": "Invalid volume specification",
"/rw:rw": "Invalid volume specification",
"path:ro": "Invalid volume specification",
"/path:/path:sw": `invalid mode: "sw"`,
"/path:/path:rwz": `invalid mode: "rwz"`,
}
}
for _, path := range valid {
if _, err := ParseMountSpec(path, "local"); err != nil {
t.Fatalf("ParseMountSpec(`%q`) should succeed: error %q", path, err)
}
}
for path, expectedError := range invalid {
if _, err := ParseMountSpec(path, "local"); err == nil {
t.Fatalf("ParseMountSpec(`%q`) should have failed validation. Err %v", path, err)
} else {
if !strings.Contains(err.Error(), expectedError) {
t.Fatalf("ParseMountSpec(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
}
}
}
}
// testParseMountSpec is a structure used by TestParseMountSpecSplit for
// specifying test cases for the ParseMountSpec() function.
type testParseMountSpec struct {
bind string
driver string
expDest string
expSource string
expName string
expDriver string
expRW bool
fail bool
}
func TestParseMountSpecSplit(t *testing.T) {
var cases []testParseMountSpec
if runtime.GOOS == "windows" {
cases = []testParseMountSpec{
{`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false},
{`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false},
// TODO Windows post TP4 - Add readonly support {`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false},
{`c:\:d:\:rw`, "local", `d:\`, `c:\`, ``, "", true, false},
{`c:\:d:\:foo`, "local", `d:\`, `c:\`, ``, "", false, true},
{`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false},
{`name:d:`, "local", `d:`, ``, `name`, "local", true, false},
// TODO Windows post TP4 - Add readonly support {`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false},
{`name:c:`, "", ``, ``, ``, "", true, true},
{`driver/name:c:`, "", ``, ``, ``, "", true, true},
}
} else {
cases = []testParseMountSpec{
{"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false},
{"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false},
{"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false},
{"/tmp:/tmp4:foo", "", "", "", "", "", false, true},
{"name:/named1", "", "/named1", "", "name", "local", true, false},
{"name:/named2", "external", "/named2", "", "name", "external", true, false},
{"name:/named3:ro", "local", "/named3", "", "name", "local", false, false},
{"local/name:/tmp:rw", "", "/tmp", "", "local/name", "local", true, false},
{"/tmp:tmp", "", "", "", "", "", true, true},
}
}
for _, c := range cases {
m, err := ParseMountSpec(c.bind, c.driver)
if c.fail {
if err == nil {
t.Fatalf("Expected error, was nil, for spec %s\n", c.bind)
}
continue
}
if m == nil || err != nil {
t.Fatalf("ParseMountSpec failed for spec %s driver %s error %v\n", c.bind, c.driver, err.Error())
continue
}
if m.Destination != c.expDest {
t.Fatalf("Expected destination %s, was %s, for spec %s\n", c.expDest, m.Destination, c.bind)
}
if m.Source != c.expSource {
t.Fatalf("Expected source %s, was %s, for spec %s\n", c.expSource, m.Source, c.bind)
}
if m.Name != c.expName {
t.Fatalf("Expected name %s, was %s for spec %s\n", c.expName, m.Name, c.bind)
}
if m.Driver != c.expDriver {
t.Fatalf("Expected driver %s, was %s, for spec %s\n", c.expDriver, m.Driver, c.bind)
}
if m.RW != c.expRW {
t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind)
}
}
}

View File

@@ -0,0 +1,182 @@
// +build linux freebsd darwin solaris
package volume
import (
"fmt"
"path/filepath"
"strings"
derr "github.com/docker/docker/errors"
)
// read-write modes
var rwModes = map[string]bool{
"rw": true,
"ro": true,
}
// label modes
var labelModes = map[string]bool{
"Z": true,
"z": true,
}
// BackwardsCompatible decides whether this mount point can be
// used in old versions of Docker or not.
// Only bind mounts and local volumes can be used in old versions of Docker.
func (m *MountPoint) BackwardsCompatible() bool {
return len(m.Source) > 0 || m.Driver == DefaultDriverName
}
// HasResource checks whether the given absolute path for a container is in
// this mount point. If the relative path starts with `../` then the resource
// is outside of this mount point, but we can't simply check for this prefix
// because it misses `..` which is also outside of the mount, so check both.
func (m *MountPoint) HasResource(absolutePath string) bool {
relPath, err := filepath.Rel(m.Destination, absolutePath)
return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
}
// ParseMountSpec validates the configuration of mount information is valid.
func ParseMountSpec(spec, volumeDriver string) (*MountPoint, error) {
spec = filepath.ToSlash(spec)
mp := &MountPoint{
RW: true,
Propagation: DefaultPropagationMode,
}
if strings.Count(spec, ":") > 2 {
return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec)
}
arr := strings.SplitN(spec, ":", 3)
if arr[0] == "" {
return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec)
}
switch len(arr) {
case 1:
// Just a destination path in the container
mp.Destination = filepath.Clean(arr[0])
case 2:
if isValid := ValidMountMode(arr[1]); isValid {
// Destination + Mode is not a valid volume - volumes
// cannot include a mode. eg /foo:rw
return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec)
}
// Host Source Path or Name + Destination
mp.Source = arr[0]
mp.Destination = arr[1]
case 3:
// HostSourcePath+DestinationPath+Mode
mp.Source = arr[0]
mp.Destination = arr[1]
mp.Mode = arr[2] // Mode field is used by SELinux to decide whether to apply label
if !ValidMountMode(mp.Mode) {
return nil, derr.ErrorCodeVolumeInvalidMode.WithArgs(mp.Mode)
}
mp.RW = ReadWrite(mp.Mode)
mp.Propagation = GetPropagation(mp.Mode)
default:
return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec)
}
//validate the volumes destination path
mp.Destination = filepath.Clean(mp.Destination)
if !filepath.IsAbs(mp.Destination) {
return nil, derr.ErrorCodeVolumeAbs.WithArgs(mp.Destination)
}
// Destination cannot be "/"
if mp.Destination == "/" {
return nil, derr.ErrorCodeVolumeSlash.WithArgs(spec)
}
name, source := ParseVolumeSource(mp.Source)
if len(source) == 0 {
mp.Source = "" // Clear it out as we previously assumed it was not a name
mp.Driver = volumeDriver
if len(mp.Driver) == 0 {
mp.Driver = DefaultDriverName
}
// Named volumes can't have propagation properties specified.
// Their defaults will be decided by docker. This is just a
// safeguard. Don't want to get into situations where named
// volumes were mounted as '[r]shared' inside container and
// container does further mounts under that volume and these
// mounts become visible on host and later original volume
// cleanup becomes an issue if container does not unmount
// submounts explicitly.
if HasPropagation(mp.Mode) {
return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec)
}
} else {
mp.Source = filepath.Clean(source)
}
mp.Name = name
return mp, nil
}
// ParseVolumeSource parses the origin sources that's mounted into the container.
// It returns a name and a source. It looks to see if the spec passed in
// is an absolute file. If it is, it assumes the spec is a source. If not,
// it assumes the spec is a name.
func ParseVolumeSource(spec string) (string, string) {
if !filepath.IsAbs(spec) {
return spec, ""
}
return "", spec
}
// IsVolumeNameValid checks a volume name in a platform specific manner.
func IsVolumeNameValid(name string) (bool, error) {
return true, nil
}
// ValidMountMode will make sure the mount mode is valid.
// returns if it's a valid mount mode or not.
func ValidMountMode(mode string) bool {
rwModeCount := 0
labelModeCount := 0
propagationModeCount := 0
for _, o := range strings.Split(mode, ",") {
if rwModes[o] {
rwModeCount++
continue
} else if labelModes[o] {
labelModeCount++
continue
} else if propagationModes[o] {
propagationModeCount++
continue
}
return false
}
// Only one string for each mode is allowed.
if rwModeCount > 1 || labelModeCount > 1 || propagationModeCount > 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 returs true.
func ReadWrite(mode string) bool {
if !ValidMountMode(mode) {
return false
}
for _, o := range strings.Split(mode, ",") {
if o == "ro" {
return false
}
}
return true
}

View File

@@ -0,0 +1,199 @@
package volume
import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/Sirupsen/logrus"
derr "github.com/docker/docker/errors"
)
// read-write modes
var rwModes = map[string]bool{
"rw": true,
}
// read-only modes
var roModes = map[string]bool{
"ro": true,
}
const (
// Spec should be in the format [source:]destination[:mode]
//
// Examples: c:\foo bar:d:rw
// c:\foo:d:\bar
// myname:d:
// d:\
//
// Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See
// https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to
// test is https://regex-golang.appspot.com/assets/html/index.html
//
// Useful link for referencing named capturing groups:
// http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex
//
// There are three match groups: source, destination and mode.
//
// RXHostDir is the first option of a source
RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*`
// RXName is the second option of a source
RXName = `[^\\/:*?"<>|\r\n]+`
// RXReservedNames are reserved names not possible on Windows
RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])`
// RXSource is the combined 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
RXMode = `(:(?P<mode>(?i)rw))?`
// Temporarily for TP4, disabling the use of ro as it's not supported yet
// in the platform. TODO Windows: `(:(?P<mode>(?i)ro|rw))?`
// mode (optional)
// - Hopefully self explanatory in comparison to above.
// - Colon is not in the capture group
//
)
// 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
}
// ParseMountSpec validates the configuration of mount information is valid.
func ParseMountSpec(spec string, volumeDriver string) (*MountPoint, error) {
var specExp = regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`)
// Ensure in platform semantics for matching. The CLI will send in Unix semantics.
match := specExp.FindStringSubmatch(filepath.FromSlash(strings.ToLower(spec)))
// Must have something back
if len(match) == 0 {
return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec)
}
// Pull out the sub expressions from the named capture groups
matchgroups := make(map[string]string)
for i, name := range specExp.SubexpNames() {
matchgroups[name] = strings.ToLower(match[i])
}
mp := &MountPoint{
Source: matchgroups["source"],
Destination: matchgroups["destination"],
RW: true,
}
if strings.ToLower(matchgroups["mode"]) == "ro" {
mp.RW = false
}
// Volumes cannot include an explicitly supplied mode eg c:\path:rw
if mp.Source == "" && mp.Destination != "" && matchgroups["mode"] != "" {
return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec)
}
// Note: No need to check if destination is absolute as it must be by
// definition of matching the regex.
if filepath.VolumeName(mp.Destination) == mp.Destination {
// Ensure the destination path, if a drive letter, is not the c drive
if strings.ToLower(mp.Destination) == "c:" {
return nil, derr.ErrorCodeVolumeDestIsC.WithArgs(spec)
}
} else {
// So we know the destination is a path, not drive letter. Clean it up.
mp.Destination = filepath.Clean(mp.Destination)
// Ensure the destination path, if a path, is not the c root directory
if strings.ToLower(mp.Destination) == `c:\` {
return nil, derr.ErrorCodeVolumeDestIsCRoot.WithArgs(spec)
}
}
// See if the source is a name instead of a host directory
if len(mp.Source) > 0 {
validName, err := IsVolumeNameValid(mp.Source)
if err != nil {
return nil, err
}
if validName {
// OK, so the source is a name.
mp.Name = mp.Source
mp.Source = ""
// Set the driver accordingly
mp.Driver = volumeDriver
if len(mp.Driver) == 0 {
mp.Driver = DefaultDriverName
}
} else {
// OK, so the source must be a host directory. Make sure it's clean.
mp.Source = filepath.Clean(mp.Source)
}
}
// Ensure the host path source, if supplied, exists and is a directory
if len(mp.Source) > 0 {
var fi os.FileInfo
var err error
if fi, err = os.Stat(mp.Source); err != nil {
return nil, derr.ErrorCodeVolumeSourceNotFound.WithArgs(mp.Source, err)
}
if !fi.IsDir() {
return nil, derr.ErrorCodeVolumeSourceNotDirectory.WithArgs(mp.Source)
}
}
logrus.Debugf("MP: Source '%s', Dest '%s', RW %t, Name '%s', Driver '%s'", mp.Source, mp.Destination, mp.RW, mp.Name, mp.Driver)
return mp, nil
}
// IsVolumeNameValid checks a volume name in a platform specific manner.
func IsVolumeNameValid(name string) (bool, error) {
nameExp := regexp.MustCompile(`^` + RXName + `$`)
if !nameExp.MatchString(name) {
return false, nil
}
nameExp = regexp.MustCompile(`^` + RXReservedNames + `$`)
if nameExp.MatchString(name) {
return false, derr.ErrorCodeVolumeNameReservedWord.WithArgs(name)
}
return true, nil
}
// ValidMountMode will make sure the mount mode is valid.
// returns if it's a valid mount mode or not.
func ValidMountMode(mode string) bool {
return roModes[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)]
}