update vendor

Signed-off-by: Jess Frazelle <acidburn@microsoft.com>
This commit is contained in:
Jess Frazelle 2018-09-25 12:27:46 -04:00
parent 19a32db84d
commit 94d1cfbfbf
No known key found for this signature in database
GPG key ID: 18F3685C0022BFF3
10501 changed files with 2307943 additions and 29279 deletions

View file

@ -0,0 +1,176 @@
package drivers // import "github.com/docker/docker/volume/drivers"
import (
"errors"
"strings"
"time"
"github.com/docker/docker/volume"
"github.com/sirupsen/logrus"
)
var (
errNoSuchVolume = errors.New("no such volume")
)
type volumeDriverAdapter struct {
name string
scopePath func(s string) string
capabilities *volume.Capability
proxy volumeDriver
}
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,
scopePath: a.scopePath,
}, 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,
scopePath: a.scopePath,
driverName: a.name,
eMount: a.scopePath(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,
scopePath: a.scopePath,
}, 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.WithError(err).WithField("driver", a.name).Debug("Volume driver returned an error while trying to query its capabilities, using default capabilities")
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.WithField("driver", a.Name()).WithField("scope", a.Scope).Warn("Volume driver returned an invalid scope")
cap.Scope = volume.LocalScope
}
a.capabilities = &cap
return cap
}
type volumeAdapter struct {
proxy volumeDriver
name string
scopePath func(string) 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 = a.scopePath(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 = a.scopePath(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
}

View file

@ -0,0 +1,235 @@
//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type volumeDriver -name VolumeDriver
package drivers // import "github.com/docker/docker/volume/drivers"
import (
"fmt"
"sort"
"sync"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/locker"
getter "github.com/docker/docker/pkg/plugingetter"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/volume"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const extName = "VolumeDriver"
// 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.
// nolint: deadcode
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)
}
// Store is an in-memory store for volume drivers
type Store struct {
extensions map[string]volume.Driver
mu sync.Mutex
driverLock *locker.Locker
pluginGetter getter.PluginGetter
}
// NewStore creates a new volume driver store
func NewStore(pg getter.PluginGetter) *Store {
return &Store{
extensions: make(map[string]volume.Driver),
driverLock: locker.New(),
pluginGetter: pg,
}
}
type driverNotFoundError string
func (e driverNotFoundError) Error() string {
return "volume driver not found: " + string(e)
}
func (driverNotFoundError) NotFound() {}
// 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 (s *Store) lookup(name string, mode int) (volume.Driver, error) {
if name == "" {
return nil, errdefs.InvalidParameter(errors.New("driver name cannot be empty"))
}
s.driverLock.Lock(name)
defer s.driverLock.Unlock(name)
s.mu.Lock()
ext, ok := s.extensions[name]
s.mu.Unlock()
if ok {
return ext, nil
}
if s.pluginGetter != nil {
p, err := s.pluginGetter.Get(name, extName, mode)
if err != nil {
return nil, errors.Wrap(err, "error looking up volume plugin "+name)
}
d, err := makePluginAdapter(p)
if err != nil {
return nil, errors.Wrap(err, "error making plugin client")
}
if err := validateDriver(d); err != nil {
if mode > 0 {
// Undo any reference count changes from the initial `Get`
if _, err := s.pluginGetter.Get(name, extName, mode*-1); err != nil {
logrus.WithError(err).WithField("action", "validate-driver").WithField("plugin", name).Error("error releasing reference to plugin")
}
}
return nil, err
}
if p.IsV1() {
s.mu.Lock()
s.extensions[name] = d
s.mu.Unlock()
}
return d, nil
}
return nil, driverNotFoundError(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
}
// Register associates the given driver to the given name, checking if
// the name is already associated
func (s *Store) Register(d volume.Driver, name string) bool {
if name == "" {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.extensions[name]; exists {
return false
}
if err := validateDriver(d); err != nil {
return false
}
s.extensions[name] = d
return true
}
// GetDriver returns a volume driver by its name.
// If the driver is empty, it looks for the local driver.
func (s *Store) GetDriver(name string) (volume.Driver, error) {
return s.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 (s *Store) CreateDriver(name string) (volume.Driver, error) {
return s.lookup(name, getter.Acquire)
}
// ReleaseDriver returns a volume driver by its name and decrements RefCount..
// If the driver is empty, it looks for the local driver.
func (s *Store) ReleaseDriver(name string) (volume.Driver, error) {
return s.lookup(name, getter.Release)
}
// GetDriverList returns list of volume drivers registered.
// If no driver is registered, empty string list will be returned.
func (s *Store) GetDriverList() []string {
var driverList []string
s.mu.Lock()
defer s.mu.Unlock()
for driverName := range s.extensions {
driverList = append(driverList, driverName)
}
sort.Strings(driverList)
return driverList
}
// GetAllDrivers lists all the registered drivers
func (s *Store) GetAllDrivers() ([]volume.Driver, error) {
var plugins []getter.CompatPlugin
if s.pluginGetter != nil {
var err error
plugins, err = s.pluginGetter.GetAllByCap(extName)
if err != nil {
return nil, fmt.Errorf("error listing plugins: %v", err)
}
}
var ds []volume.Driver
s.mu.Lock()
defer s.mu.Unlock()
for _, d := range s.extensions {
ds = append(ds, d)
}
for _, p := range plugins {
name := p.Name()
if _, ok := s.extensions[name]; ok {
continue
}
ext, err := makePluginAdapter(p)
if err != nil {
return nil, errors.Wrap(err, "error making plugin client")
}
if p.IsV1() {
s.extensions[name] = ext
}
ds = append(ds, ext)
}
return ds, nil
}
func makePluginAdapter(p getter.CompatPlugin) (*volumeDriverAdapter, error) {
if pc, ok := p.(getter.PluginWithV1Client); ok {
return &volumeDriverAdapter{name: p.Name(), scopePath: p.ScopedPath, proxy: &volumeDriverProxy{pc.Client()}}, nil
}
pa, ok := p.(getter.PluginAddr)
if !ok {
return nil, errdefs.System(errors.Errorf("got unknown plugin instance %T", p))
}
if pa.Protocol() != plugins.ProtocolSchemeHTTPV1 {
return nil, errors.Errorf("plugin protocol not supported: %s", p)
}
addr := pa.Addr()
client, err := plugins.NewClientWithTimeout(addr.Network()+"://"+addr.String(), nil, pa.Timeout())
if err != nil {
return nil, errors.Wrap(err, "error creating plugin client")
}
return &volumeDriverAdapter{name: p.Name(), scopePath: p.ScopedPath, proxy: &volumeDriverProxy{client}}, nil
}

View file

@ -0,0 +1,24 @@
package drivers // import "github.com/docker/docker/volume/drivers"
import (
"testing"
volumetestutils "github.com/docker/docker/volume/testutils"
)
func TestGetDriver(t *testing.T) {
s := NewStore(nil)
_, err := s.GetDriver("missing")
if err == nil {
t.Fatal("Expected error, was nil")
}
s.Register(volumetestutils.NewFakeDriver("fake"), "fake")
d, err := s.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,255 @@
// generated code - DO NOT EDIT
package drivers // import "github.com/docker/docker/volume/drivers"
import (
"errors"
"time"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/volume"
)
const (
longTimeout = 2 * time.Minute
shortTimeout = 1 * time.Minute
)
type client interface {
CallWithOptions(string, interface{}, interface{}, ...func(*plugins.RequestOpts)) 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.CallWithOptions("VolumeDriver.Create", req, &ret, plugins.WithRequestTimeout(longTimeout)); 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.CallWithOptions("VolumeDriver.Remove", req, &ret, plugins.WithRequestTimeout(shortTimeout)); 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.CallWithOptions("VolumeDriver.Path", req, &ret, plugins.WithRequestTimeout(shortTimeout)); 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.CallWithOptions("VolumeDriver.Mount", req, &ret, plugins.WithRequestTimeout(longTimeout)); 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.CallWithOptions("VolumeDriver.Unmount", req, &ret, plugins.WithRequestTimeout(shortTimeout)); 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.CallWithOptions("VolumeDriver.List", req, &ret, plugins.WithRequestTimeout(shortTimeout)); 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.CallWithOptions("VolumeDriver.Get", req, &ret, plugins.WithRequestTimeout(shortTimeout)); 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.CallWithOptions("VolumeDriver.Capabilities", req, &ret, plugins.WithRequestTimeout(shortTimeout)); err != nil {
return
}
capabilities = ret.Capabilities
if ret.Err != "" {
err = errors.New(ret.Err)
}
return
}

View file

@ -0,0 +1,132 @@
package drivers // import "github.com/docker/docker/volume/drivers"
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)
}
}

View file

@ -0,0 +1,378 @@
// 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 "github.com/docker/docker/volume/local"
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"github.com/docker/docker/daemon/names"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/mount"
"github.com/docker/docker/volume"
"github.com/pkg/errors"
)
// 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 = names.RestrictedNamePattern
)
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, rootIdentity idtools.Identity) (*Root, error) {
rootDirectory := filepath.Join(scope, volumesPathName)
if err := idtools.MkdirAllAndChown(rootDirectory, 0700, rootIdentity); err != nil {
return nil, err
}
r := &Root{
scope: scope,
path: rootDirectory,
volumes: make(map[string]*localVolume),
rootIdentity: rootIdentity,
}
dirs, err := ioutil.ReadDir(rootDirectory)
if err != nil {
return nil, 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)
mount.Unmount(v.path)
}
}
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
rootIdentity idtools.Identity
}
// 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.rootIdentity); err != nil {
return nil, errors.Wrapf(errdefs.System(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, errdefs.System(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 errdefs.System(errors.Errorf("unknown volume type %T", v))
}
if lv.active.count > 0 {
return errdefs.System(errors.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 errdefs.System(errors.Errorf("Unable to remove a directory outside of the local volume 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 errdefs.System(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
}
type validationError string
func (e validationError) Error() string {
return string(e)
}
func (e validationError) InvalidParameter() {}
func (r *Root) validateName(name string) error {
if len(name) == 1 {
return validationError("volume name is too short, names should be at least two alphanumeric characters")
}
if !volumeNameRegex.MatchString(name) {
return validationError(fmt.Sprintf("%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, names.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
}
// CachedPath returns the data location
func (v *localVolume) CachedPath() 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 "", errdefs.System(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 errdefs.System(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.Sprintf("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 ""
}

View file

@ -0,0 +1,335 @@
package local // import "github.com/docker/docker/volume/local"
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/mount"
"gotest.tools/skip"
)
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) {
skip.If(t, runtime.GOOS == "windows", "FIXME: investigate why this test fails on CI")
rootDir, err := ioutil.TempDir("", "local-volume-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootDir)
r, err := New(rootDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
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.Identity{UID: os.Geteuid(), GID: os.Getegid()})
if err != nil {
t.Fatal(err)
}
vol, err := r.Create("testing", nil)
if err != nil {
t.Fatal(err)
}
r, err = New(rootDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
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.Identity{UID: os.Geteuid(), GID: os.Getegid()})
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.Identity{UID: os.Geteuid(), GID: os.Getegid()})
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) {
skip.If(t, runtime.GOOS == "windows")
skip.If(t, os.Getuid() != 0, "requires mounts")
rootDir, err := ioutil.TempDir("", "local-volume-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(rootDir)
r, err := New(rootDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
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(mount.SingleEntryFilter(dir))
if err != nil {
t.Fatal(err)
}
if len(mountInfos) != 1 {
t.Fatalf("expected 1 mount, found %d: %+v", len(mountInfos), mountInfos)
}
info := mountInfos[0]
t.Logf("%+v", info)
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)
}
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.Identity{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 TestRelaodNoOpts(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.Identity{UID: os.Geteuid(), GID: os.Getegid()})
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.Identity{UID: os.Geteuid(), GID: os.Getegid()})
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)
}
}
}

View file

@ -0,0 +1,99 @@
// +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 "github.com/docker/docker/volume/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
}

View file

@ -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 "github.com/docker/docker/volume/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
}

View file

@ -0,0 +1,34 @@
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"errors"
"path"
"github.com/docker/docker/api/types/mount"
)
var lcowSpecificValidators mountValidator = func(m *mount.Mount) error {
if path.Clean(m.Target) == "/" {
return ErrVolumeTargetIsRoot
}
if m.Type == mount.TypeNamedPipe {
return errors.New("Linux containers on Windows do not support named pipe mounts")
}
return nil
}
type lcowParser struct {
windowsParser
}
func (p *lcowParser) ValidateMountConfig(mnt *mount.Mount) error {
return p.validateMountConfigReg(mnt, rxLCOWDestination, lcowSpecificValidators)
}
func (p *lcowParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
return p.parseMountRaw(raw, volumeDriver, rxLCOWDestination, false, lcowSpecificValidators)
}
func (p *lcowParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
return p.parseMountSpec(cfg, rxLCOWDestination, false, lcowSpecificValidators)
}

View file

@ -0,0 +1,417 @@
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"errors"
"fmt"
"path"
"path/filepath"
"strings"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/volume"
)
type linuxParser struct {
}
func linuxSplitRawSpec(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 linuxValidateNotRoot(p string) error {
p = path.Clean(strings.Replace(p, `\`, `/`, -1))
if p == "/" {
return ErrVolumeTargetIsRoot
}
return nil
}
func linuxValidateAbsolute(p string) error {
p = strings.Replace(p, `\`, `/`, -1)
if path.IsAbs(p) {
return nil
}
return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
}
func (p *linuxParser) ValidateMountConfig(mnt *mount.Mount) error {
// there was something looking like a bug in existing codebase:
// - validateMountConfig on linux was called with options skipping bind source existence when calling ParseMountRaw
// - but not when calling ParseMountSpec directly... nor when the unit test called it directly
return p.validateMountConfigImpl(mnt, true)
}
func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSourceExists bool) error {
if len(mnt.Target) == 0 {
return &errMountConfig{mnt, errMissingField("Target")}
}
if err := linuxValidateNotRoot(mnt.Target); err != nil {
return &errMountConfig{mnt, err}
}
if err := linuxValidateAbsolute(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(linuxPropagationModes) > 0 {
if _, ok := linuxPropagationModes[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 := linuxValidateAbsolute(mnt.Source); err != nil {
return &errMountConfig{mnt, err}
}
if validateBindSourceExists {
exists, _, _ := currentFileInfoProvider.fileInfo(mnt.Source)
if !exists {
return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)}
}
}
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")}
}
case mount.TypeTmpfs:
if len(mnt.Source) != 0 {
return &errMountConfig{mnt, errExtraField("Source")}
}
if _, err := p.ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
return &errMountConfig{mnt, err}
}
default:
return &errMountConfig{mnt, errors.New("mount type unknown")}
}
return nil
}
// read-write modes
var rwModes = map[string]bool{
"rw": true,
"ro": true,
}
// label modes
var linuxLabelModes = map[string]bool{
"Z": true,
"z": true,
}
// consistency modes
var linuxConsistencyModes = map[mount.Consistency]bool{
mount.ConsistencyFull: true,
mount.ConsistencyCached: true,
mount.ConsistencyDelegated: true,
}
var linuxPropagationModes = map[mount.Propagation]bool{
mount.PropagationPrivate: true,
mount.PropagationRPrivate: true,
mount.PropagationSlave: true,
mount.PropagationRSlave: true,
mount.PropagationShared: true,
mount.PropagationRShared: true,
}
const linuxDefaultPropagationMode = mount.PropagationRPrivate
func linuxGetPropagation(mode string) mount.Propagation {
for _, o := range strings.Split(mode, ",") {
prop := mount.Propagation(o)
if linuxPropagationModes[prop] {
return prop
}
}
return linuxDefaultPropagationMode
}
func linuxHasPropagation(mode string) bool {
for _, o := range strings.Split(mode, ",") {
if linuxPropagationModes[mount.Propagation(o)] {
return true
}
}
return false
}
func linuxValidMountMode(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 linuxLabelModes[o]:
labelModeCount++
case linuxPropagationModes[mount.Propagation(o)]:
propagationModeCount++
case copyModeExists(o):
copyModeCount++
case linuxConsistencyModes[mount.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
}
func (p *linuxParser) ReadWrite(mode string) bool {
if !linuxValidMountMode(mode) {
return false
}
for _, o := range strings.Split(mode, ",") {
if o == "ro" {
return false
}
}
return true
}
func (p *linuxParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
arr, err := linuxSplitRawSpec(raw)
if err != nil {
return nil, err
}
var spec mount.Mount
var mode string
switch len(arr) {
case 1:
// Just a destination path in the container
spec.Target = arr[0]
case 2:
if linuxValidMountMode(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 !linuxValidMountMode(mode) {
return nil, errInvalidMode(mode)
}
if path.IsAbs(spec.Source) {
spec.Type = mount.TypeBind
} else {
spec.Type = mount.TypeVolume
}
spec.ReadOnly = !p.ReadWrite(mode)
// cannot assume that if a volume driver is passed in that we should set it
if volumeDriver != "" && spec.Type == mount.TypeVolume {
spec.VolumeOptions = &mount.VolumeOptions{
DriverConfig: &mount.Driver{Name: volumeDriver},
}
}
if copyData, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
if spec.VolumeOptions == nil {
spec.VolumeOptions = &mount.VolumeOptions{}
}
spec.VolumeOptions.NoCopy = !copyData
}
if linuxHasPropagation(mode) {
spec.BindOptions = &mount.BindOptions{
Propagation: linuxGetPropagation(mode),
}
}
mp, err := p.parseMountSpec(spec, false)
if mp != nil {
mp.Mode = mode
}
if err != nil {
err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
}
return mp, err
}
func (p *linuxParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
return p.parseMountSpec(cfg, true)
}
func (p *linuxParser) parseMountSpec(cfg mount.Mount, validateBindSourceExists bool) (*MountPoint, error) {
if err := p.validateMountConfigImpl(&cfg, validateBindSourceExists); err != nil {
return nil, err
}
mp := &MountPoint{
RW: !cfg.ReadOnly,
Destination: path.Clean(filepath.ToSlash(cfg.Target)),
Type: cfg.Type,
Spec: cfg,
}
switch cfg.Type {
case mount.TypeVolume:
if cfg.Source == "" {
mp.Name = stringid.GenerateNonCryptoID()
} else {
mp.Name = cfg.Source
}
mp.CopyData = p.DefaultCopyMode()
if cfg.VolumeOptions != nil {
if cfg.VolumeOptions.DriverConfig != nil {
mp.Driver = cfg.VolumeOptions.DriverConfig.Name
}
if cfg.VolumeOptions.NoCopy {
mp.CopyData = false
}
}
case mount.TypeBind:
mp.Source = path.Clean(filepath.ToSlash(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 = linuxDefaultPropagationMode
}
case mount.TypeTmpfs:
// NOP
}
return mp, nil
}
func (p *linuxParser) 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 !linuxValidMountMode(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 linuxHasPropagation(mode) {
return "", "", errInvalidMode(mode)
}
// Do not allow copy modes on volumes-from
if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
return "", "", errInvalidMode(mode)
}
}
return id, mode, nil
}
func (p *linuxParser) DefaultPropagationMode() mount.Propagation {
return linuxDefaultPropagationMode
}
func (p *linuxParser) ConvertTmpfsOptions(opt *mount.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, returning 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
}
func (p *linuxParser) DefaultCopyMode() bool {
return true
}
func (p *linuxParser) ValidateVolumeName(name string) error {
return nil
}
func (p *linuxParser) IsBackwardCompatible(m *MountPoint) bool {
return len(m.Source) > 0 || m.Driver == volume.DefaultDriverName
}
func (p *linuxParser) ValidateTmpfsMountDestination(dest string) error {
if err := linuxValidateNotRoot(dest); err != nil {
return err
}
return linuxValidateAbsolute(dest)
}

View file

@ -0,0 +1,181 @@
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"fmt"
"os"
"path/filepath"
"syscall"
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/volume"
"github.com/opencontainers/selinux/go-selinux/label"
"github.com/pkg/errors"
)
// 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.
//
// Note that this type is embedded in `container.Container` object and persisted to disk.
// Changes to this struct need to by synced with on disk state.
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.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
// Some bind mounts should not be automatically created.
// (Some are auto-created for backwards-compatibility)
// This is checked on the API but setting this here prevents race conditions.
// where a bind dir existed during validation was removed before reaching the setup code.
SkipMountpointCreation bool
// Track usage of this mountpoint
// Specifically 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.Identity, checkFun func(m *MountPoint) error) (path string, err error) {
if m.SkipMountpointCreation {
return m.Source, nil
}
defer func() {
if err != nil || !label.RelabelNeeded(m.Mode) {
return
}
var sourcePath string
sourcePath, err = filepath.EvalSymlinks(m.Source)
if err != nil {
path = ""
err = errors.Wrapf(err, "error evaluating symlinks from mount source %q", m.Source)
return
}
err = label.Relabel(sourcePath, 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'", sourcePath)
}
}()
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")
}
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
}
func errInvalidMode(mode string) error {
return errors.Errorf("invalid mode: %v", mode)
}
func errInvalidSpec(spec string) error {
return errors.Errorf("invalid volume specification: '%s'", spec)
}

View file

@ -0,0 +1,47 @@
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"errors"
"runtime"
"github.com/docker/docker/api/types/mount"
)
const (
// OSLinux is the same as runtime.GOOS on linux
OSLinux = "linux"
// OSWindows is the same as runtime.GOOS on windows
OSWindows = "windows"
)
// ErrVolumeTargetIsRoot is returned when the target destination is root.
// It's used by both LCOW and Linux parsers.
var ErrVolumeTargetIsRoot = errors.New("invalid specification: destination can't be '/'")
// Parser represents a platform specific parser for mount expressions
type Parser interface {
ParseMountRaw(raw, volumeDriver string) (*MountPoint, error)
ParseMountSpec(cfg mount.Mount) (*MountPoint, error)
ParseVolumesFrom(spec string) (string, string, error)
DefaultPropagationMode() mount.Propagation
ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error)
DefaultCopyMode() bool
ValidateVolumeName(name string) error
ReadWrite(mode string) bool
IsBackwardCompatible(m *MountPoint) bool
HasResource(m *MountPoint, absPath string) bool
ValidateTmpfsMountDestination(dest string) error
ValidateMountConfig(mt *mount.Mount) error
}
// NewParser creates a parser for a given container OS, depending on the current host OS (linux on a windows host will resolve to an lcowParser)
func NewParser(containerOS string) Parser {
switch containerOS {
case OSWindows:
return &windowsParser{}
}
if runtime.GOOS == OSWindows {
return &lcowParser{}
}
return &linuxParser{}
}

View file

@ -0,0 +1,480 @@
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"github.com/docker/docker/api/types/mount"
)
type parseMountRawTestSet struct {
valid []string
invalid map[string]string
}
func TestConvertTmpfsOptions(t *testing.T) {
type testCase struct {
opt mount.TmpfsOptions
readOnly bool
expectedSubstrings []string
unexpectedSubstrings []string
}
cases := []testCase{
{
opt: mount.TmpfsOptions{SizeBytes: 1024 * 1024, Mode: 0700},
readOnly: false,
expectedSubstrings: []string{"size=1m", "mode=700"},
unexpectedSubstrings: []string{"ro"},
},
{
opt: mount.TmpfsOptions{},
readOnly: true,
expectedSubstrings: []string{"ro"},
unexpectedSubstrings: []string{},
},
}
p := &linuxParser{}
for _, c := range cases {
data, err := p.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)
}
}
}
}
type mockFiProvider struct{}
func (mockFiProvider) fileInfo(path string) (exists, isDir bool, err error) {
dirs := map[string]struct{}{
`c:\`: {},
`c:\windows\`: {},
`c:\windows`: {},
`c:\program files`: {},
`c:\Windows`: {},
`c:\Program Files (x86)`: {},
`\\?\c:\windows\`: {},
}
files := map[string]struct{}{
`c:\windows\system32\ntdll.dll`: {},
}
if _, ok := dirs[path]; ok {
return true, true, nil
}
if _, ok := files[path]; ok {
return true, false, nil
}
return false, false, nil
}
func TestParseMountRaw(t *testing.T) {
previousProvider := currentFileInfoProvider
defer func() { currentFileInfoProvider = previousProvider }()
currentFileInfoProvider = mockFiProvider{}
windowsSet := parseMountRawTestSet{
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
`\\?\c:\windows\:d:`, // Long path handling (source)
`c:\windows\:\\?\d:\`, // Long path handling (target)
`\\.\pipe\foo:\\.\pipe\foo`, // named pipe
`//./pipe/foo://./pipe/foo`, // named pipe forward slashes
},
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:`: `bind mount source path does not exist: c:\notexist`,
`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`,
`\\.\pipe\foo:c:\pipe`: `'c:\pipe' is not a valid pipe path`,
},
}
lcowSet := parseMountRawTestSet{
valid: []string{
`/foo`,
`/foo/`,
`/foo bar`,
`c:\:/foo`,
`c:\windows\:/foo`,
`c:\windows:/s p a c e`,
`c:\windows:/s p a c e:RW`,
`c:\program files:/s p a c e i n h o s t d i r`,
`0123456789name:/foo`,
`MiXeDcAsEnAmE:/foo`,
`name:/foo`,
`name:/foo:rW`,
`name:/foo:RW`,
`name:/foo:RO`,
`c:/:/forward/slashes/are/good/too`,
`c:/:/including with/spaces:ro`,
`/Program Files (x86)`, // With capitals and brackets
},
invalid: map[string]string{
``: "invalid volume specification: ",
`.`: "invalid volume specification: ",
`c:`: "invalid volume specification: ",
`c:\`: "invalid volume specification: ",
`../`: "invalid volume specification: ",
`c:\:../`: "invalid volume specification: ",
`c:\:/foo:xyzzy`: "invalid volume specification: ",
`/`: "destination can't be '/'",
`/..`: "destination can't be '/'",
`c:\notexist:/foo`: `bind mount source path does not exist: c:\notexist`,
`c:\windows\system32\ntdll.dll:/foo`: `source path must be a directory`,
`name<:/foo`: `invalid volume specification`,
`name>:/foo`: `invalid volume specification`,
`name::/foo`: `invalid volume specification`,
`name":/foo`: `invalid volume specification`,
`name\:/foo`: `invalid volume specification`,
`name*:/foo`: `invalid volume specification`,
`name|:/foo`: `invalid volume specification`,
`name?:/foo`: `invalid volume specification`,
`name/:/foo`: `invalid volume specification`,
`/foo:rw`: `invalid volume specification`,
`/foo:ro`: `invalid volume specification`,
`con:/foo`: `cannot be a reserved word for Windows filenames`,
`PRN:/foo`: `cannot be a reserved word for Windows filenames`,
`aUx:/foo`: `cannot be a reserved word for Windows filenames`,
`nul:/foo`: `cannot be a reserved word for Windows filenames`,
`com1:/foo`: `cannot be a reserved word for Windows filenames`,
`com2:/foo`: `cannot be a reserved word for Windows filenames`,
`com3:/foo`: `cannot be a reserved word for Windows filenames`,
`com4:/foo`: `cannot be a reserved word for Windows filenames`,
`com5:/foo`: `cannot be a reserved word for Windows filenames`,
`com6:/foo`: `cannot be a reserved word for Windows filenames`,
`com7:/foo`: `cannot be a reserved word for Windows filenames`,
`com8:/foo`: `cannot be a reserved word for Windows filenames`,
`com9:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt1:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt2:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt3:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt4:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt5:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt6:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt7:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt8:/foo`: `cannot be a reserved word for Windows filenames`,
`lpt9:/foo`: `cannot be a reserved word for Windows filenames`,
`\\.\pipe\foo:/foo`: `Linux containers on Windows do not support named pipe mounts`,
},
}
linuxSet := parseMountRawTestSet{
valid: []string{
"/home",
"/home:/home",
"/home:/something/else",
"/with space",
"/home:/with space",
"relative:/absolute-path",
"hostPath:/containerPath:ro",
"/hostPath:/containerPath:rw",
"/rw:/ro",
"/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{
"": "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`,
"/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",
},
}
linParser := &linuxParser{}
winParser := &windowsParser{}
lcowParser := &lcowParser{}
tester := func(parser Parser, set parseMountRawTestSet) {
for _, path := range set.valid {
if _, err := parser.ParseMountRaw(path, "local"); err != nil {
t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
}
}
for path, expectedError := range set.invalid {
if mp, err := parser.ParseMountRaw(path, "local"); err == nil {
t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
} else {
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
}
}
}
}
tester(linParser, linuxSet)
tester(winParser, windowsSet)
tester(lcowParser, lcowSet)
}
// testParseMountRaw is a structure used by TestParseMountRawSplit for
// specifying test cases for the ParseMountRaw() function.
type testParseMountRaw struct {
bind string
driver string
expType mount.Type
expDest string
expSource string
expName string
expDriver string
expRW bool
fail bool
}
func TestParseMountRawSplit(t *testing.T) {
previousProvider := currentFileInfoProvider
defer func() { currentFileInfoProvider = previousProvider }()
currentFileInfoProvider = mockFiProvider{}
windowsCases := []testParseMountRaw{
{`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false},
{`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
{`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false},
{`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
{`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true},
{`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
{`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
{`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false},
{`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
{`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
{`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false},
{`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
{`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
}
lcowCases := []testParseMountRaw{
{`c:\:/foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false},
{`c:\:/foo:ro`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, false},
{`c:\:/foo:rw`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false},
{`c:\:/foo:foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, true},
{`name:/foo:rw`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false},
{`name:/foo`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false},
{`name:/foo:ro`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", false, false},
{`name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
{`driver/name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
{`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, true},
{`\\.\pipe\foo:/data`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
{`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
}
linuxCases := []testParseMountRaw{
{"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false},
{"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false},
{"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false},
{"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true},
{"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false},
{"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false},
{"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false},
{"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false},
{"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true},
}
linParser := &linuxParser{}
winParser := &windowsParser{}
lcowParser := &lcowParser{}
tester := func(parser Parser, cases []testParseMountRaw) {
for i, c := range cases {
t.Logf("case %d", i)
m, err := parser.ParseMountRaw(c.bind, c.driver)
if c.fail {
if err == nil {
t.Errorf("Expected error, was nil, for spec %s\n", c.bind)
}
continue
}
if m == nil || err != nil {
t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err.Error())
continue
}
if m.Destination != c.expDest {
t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
}
if m.Source != c.expSource {
t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind)
}
if m.Name != c.expName {
t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind)
}
if m.Driver != c.expDriver {
t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind)
}
if m.RW != c.expRW {
t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind)
}
if m.Type != c.expType {
t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind)
}
}
}
tester(linParser, linuxCases)
tester(winParser, windowsCases)
tester(lcowParser, lcowCases)
}
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)
parser := NewParser(runtime.GOOS)
cases := []c{
{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true, Propagation: parser.DefaultPropagationMode()}},
{mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath + string(os.PathSeparator), ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}},
{mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: parser.DefaultCopyMode()}},
{mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath + string(os.PathSeparator)}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: parser.DefaultCopyMode()}},
}
for i, c := range cases {
t.Logf("case %d", i)
mp, err := parser.ParseMountSpec(c.input)
if err != nil {
t.Error(err)
}
if c.expected.Type != mp.Type {
t.Errorf("Expected mount types to match. Expected: '%s', Actual: '%s'", c.expected.Type, mp.Type)
}
if c.expected.Destination != mp.Destination {
t.Errorf("Expected mount destination to match. Expected: '%s', Actual: '%s'", c.expected.Destination, mp.Destination)
}
if c.expected.Source != mp.Source {
t.Errorf("Expected mount source to match. Expected: '%s', Actual: '%s'", c.expected.Source, mp.Source)
}
if c.expected.RW != mp.RW {
t.Errorf("Expected mount writable to match. Expected: '%v', Actual: '%v'", c.expected.RW, mp.RW)
}
if c.expected.Propagation != mp.Propagation {
t.Errorf("Expected mount propagation to match. Expected: '%v', Actual: '%s'", c.expected.Propagation, mp.Propagation)
}
if c.expected.Driver != mp.Driver {
t.Errorf("Expected mount driver to match. Expected: '%v', Actual: '%s'", c.expected.Driver, mp.Driver)
}
if c.expected.CopyData != mp.CopyData {
t.Errorf("Expected mount copy data to match. Expected: '%v', Actual: '%v'", c.expected.CopyData, mp.CopyData)
}
}
}

View file

@ -0,0 +1,28 @@
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"fmt"
"github.com/docker/docker/api/types/mount"
"github.com/pkg/errors"
)
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 errBindSourceDoesNotExist(path string) error {
return errors.Errorf("bind mount source path does not exist: %s", path)
}
func errExtraField(name string) error {
return errors.Errorf("field %s must not be specified", name)
}
func errMissingField(name string) error {
return errors.Errorf("field %s must not be empty", name)
}

View file

@ -0,0 +1,73 @@
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"errors"
"io/ioutil"
"os"
"runtime"
"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: testDir, Target: testDestinationPath}, nil},
{mount.Mount{Type: "invalid", Target: testDestinationPath}, errors.New("mount type unknown")},
{mount.Mount{Type: mount.TypeBind, Source: testSourcePath, Target: testDestinationPath}, errBindSourceDoesNotExist(testSourcePath)},
}
lcowCases := []struct {
input mount.Mount
expected error
}{
{mount.Mount{Type: mount.TypeVolume}, errMissingField("Target")},
{mount.Mount{Type: mount.TypeVolume, Target: "/foo", Source: "hello"}, nil},
{mount.Mount{Type: mount.TypeVolume, Target: "/foo"}, nil},
{mount.Mount{Type: mount.TypeBind}, errMissingField("Target")},
{mount.Mount{Type: mount.TypeBind, Target: "/foo"}, errMissingField("Source")},
{mount.Mount{Type: mount.TypeBind, Target: "/foo", Source: "c:\\foo", VolumeOptions: &mount.VolumeOptions{}}, errExtraField("VolumeOptions")},
{mount.Mount{Type: mount.TypeBind, Source: "c:\\foo", Target: "/foo"}, errBindSourceDoesNotExist("c:\\foo")},
{mount.Mount{Type: mount.TypeBind, Source: testDir, Target: "/foo"}, nil},
{mount.Mount{Type: "invalid", Target: "/foo"}, errors.New("mount type unknown")},
}
parser := NewParser(runtime.GOOS)
for i, x := range cases {
err := parser.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.Errorf("expected %q, got %q, case: %d", x.expected, err, i)
}
}
if runtime.GOOS == "windows" {
parser = &lcowParser{}
for i, x := range lcowCases {
err := parser.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.Errorf("expected %q, got %q, case: %d", x.expected, err, i)
}
}
}
}

View file

@ -0,0 +1,8 @@
// +build !windows
package mounts // import "github.com/docker/docker/volume/mounts"
var (
testDestinationPath = "/foo"
testSourcePath = "/foo"
)

View file

@ -0,0 +1,6 @@
package mounts // import "github.com/docker/docker/volume/mounts"
var (
testDestinationPath = `c:\foo`
testSourcePath = `c:\foo`
)

View file

@ -0,0 +1,23 @@
package mounts // import "github.com/docker/docker/volume/mounts"
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, def bool) (bool, bool) {
for _, o := range strings.Split(mode, ",") {
if isEnabled, exists := copyModes[o]; exists {
return isEnabled, true
}
}
return def, false
}

View file

@ -0,0 +1,18 @@
// +build linux freebsd darwin
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"fmt"
"path/filepath"
"strings"
)
func (p *linuxParser) HasResource(m *MountPoint, absolutePath string) bool {
relPath, err := filepath.Rel(m.Destination, absolutePath)
return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
}
func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool {
return false
}

View file

@ -0,0 +1,8 @@
package mounts // import "github.com/docker/docker/volume/mounts"
func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool {
return false
}
func (p *linuxParser) HasResource(m *MountPoint, absolutePath string) bool {
return false
}

View file

@ -0,0 +1,456 @@
package mounts // import "github.com/docker/docker/volume/mounts"
import (
"errors"
"fmt"
"os"
"regexp"
"runtime"
"strings"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/pkg/stringid"
)
type windowsParser struct {
}
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])`
// rxPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \)
rxPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+`
// rxSource is the combined possibilities for a source
rxSource = `((?P<source>((` + rxHostDir + `)|(` + rxName + `)|(` + rxPipe + `))):)?`
// 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]+)*[\\/]?))|(` + rxPipe + `))`
rxLCOWDestination = `(?P<destination>/(?:[^\\/:*?"<>\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))?`
)
type mountValidator func(mnt *mount.Mount) error
func windowsSplitRawSpec(raw, destRegex string) ([]string, error) {
specExp := regexp.MustCompile(`^` + rxSource + destRegex + 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"] != "" {
volExp := regexp.MustCompile(`^` + rxName + `$`)
reservedNameExp := regexp.MustCompile(`^` + rxReservedNames + `$`)
if volExp.MatchString(matchgroups["destination"]) {
if reservedNameExp.MatchString(matchgroups["destination"]) {
return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"])
}
} else {
exists, isDir, _ := currentFileInfoProvider.fileInfo(matchgroups["destination"])
if exists && !isDir {
return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
}
}
}
return split, nil
}
func windowsValidMountMode(mode string) bool {
if mode == "" {
return true
}
return rwModes[strings.ToLower(mode)]
}
func windowsValidateNotRoot(p string) error {
p = strings.ToLower(strings.Replace(p, `/`, `\`, -1))
if p == "c:" || p == `c:\` {
return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
}
return nil
}
var windowsSpecificValidators mountValidator = func(mnt *mount.Mount) error {
return windowsValidateNotRoot(mnt.Target)
}
func windowsValidateRegex(p, r string) error {
if regexp.MustCompile(`^` + r + `$`).MatchString(strings.ToLower(p)) {
return nil
}
return fmt.Errorf("invalid mount path: '%s'", p)
}
func windowsValidateAbsolute(p string) error {
if err := windowsValidateRegex(p, rxDestination); err != nil {
return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
}
return nil
}
func windowsDetectMountType(p string) mount.Type {
if strings.HasPrefix(p, `\\.\pipe\`) {
return mount.TypeNamedPipe
} else if regexp.MustCompile(`^` + rxHostDir + `$`).MatchString(p) {
return mount.TypeBind
} else {
return mount.TypeVolume
}
}
func (p *windowsParser) ReadWrite(mode string) bool {
return strings.ToLower(mode) != "ro"
}
// IsVolumeNameValid checks a volume name in a platform specific manner.
func (p *windowsParser) ValidateVolumeName(name string) error {
nameExp := regexp.MustCompile(`^` + rxName + `$`)
if !nameExp.MatchString(name) {
return errors.New("invalid volume name")
}
nameExp = regexp.MustCompile(`^` + rxReservedNames + `$`)
if nameExp.MatchString(name) {
return fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
}
return nil
}
func (p *windowsParser) ValidateMountConfig(mnt *mount.Mount) error {
return p.validateMountConfigReg(mnt, rxDestination, windowsSpecificValidators)
}
type fileInfoProvider interface {
fileInfo(path string) (exist, isDir bool, err error)
}
type defaultFileInfoProvider struct {
}
func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) {
fi, err := os.Stat(path)
if err != nil {
if !os.IsNotExist(err) {
return false, false, err
}
return false, false, nil
}
return true, fi.IsDir(), nil
}
var currentFileInfoProvider fileInfoProvider = defaultFileInfoProvider{}
func (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, destRegex string, additionalValidators ...mountValidator) error {
for _, v := range additionalValidators {
if err := v(mnt); err != nil {
return &errMountConfig{mnt, err}
}
}
if len(mnt.Target) == 0 {
return &errMountConfig{mnt, errMissingField("Target")}
}
if err := windowsValidateRegex(mnt.Target, destRegex); 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 {
return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
}
}
if mnt.VolumeOptions != nil {
return &errMountConfig{mnt, errExtraField("VolumeOptions")}
}
if err := windowsValidateAbsolute(mnt.Source); err != nil {
return &errMountConfig{mnt, err}
}
exists, isdir, err := currentFileInfoProvider.fileInfo(mnt.Source)
if err != nil {
return &errMountConfig{mnt, err}
}
if !exists {
return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)}
}
if !isdir {
return &errMountConfig{mnt, fmt.Errorf("source path must be a directory")}
}
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 err := p.ValidateVolumeName(mnt.Source); err != nil {
return &errMountConfig{mnt, err}
}
}
case mount.TypeNamedPipe:
if len(mnt.Source) == 0 {
return &errMountConfig{mnt, errMissingField("Source")}
}
if mnt.BindOptions != nil {
return &errMountConfig{mnt, errExtraField("BindOptions")}
}
if mnt.ReadOnly {
return &errMountConfig{mnt, errExtraField("ReadOnly")}
}
if windowsDetectMountType(mnt.Source) != mount.TypeNamedPipe {
return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)}
}
if windowsDetectMountType(mnt.Target) != mount.TypeNamedPipe {
return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Target)}
}
default:
return &errMountConfig{mnt, errors.New("mount type unknown")}
}
return nil
}
func (p *windowsParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
return p.parseMountRaw(raw, volumeDriver, rxDestination, true, windowsSpecificValidators)
}
func (p *windowsParser) parseMountRaw(raw, volumeDriver, destRegex string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
arr, err := windowsSplitRawSpec(raw, destRegex)
if err != nil {
return nil, err
}
var spec mount.Mount
var mode string
switch len(arr) {
case 1:
// Just a destination path in the container
spec.Target = arr[0]
case 2:
if windowsValidMountMode(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 = strings.Replace(arr[0], `/`, `\`, -1)
spec.Target = arr[1]
case 3:
// HostSourcePath+DestinationPath+Mode
spec.Source = strings.Replace(arr[0], `/`, `\`, -1)
spec.Target = arr[1]
mode = arr[2]
default:
return nil, errInvalidSpec(raw)
}
if convertTargetToBackslash {
spec.Target = strings.Replace(spec.Target, `/`, `\`, -1)
}
if !windowsValidMountMode(mode) {
return nil, errInvalidMode(mode)
}
spec.Type = windowsDetectMountType(spec.Source)
spec.ReadOnly = !p.ReadWrite(mode)
// cannot assume that if a volume driver is passed in that we should set it
if volumeDriver != "" && spec.Type == mount.TypeVolume {
spec.VolumeOptions = &mount.VolumeOptions{
DriverConfig: &mount.Driver{Name: volumeDriver},
}
}
if copyData, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
if spec.VolumeOptions == nil {
spec.VolumeOptions = &mount.VolumeOptions{}
}
spec.VolumeOptions.NoCopy = !copyData
}
mp, err := p.parseMountSpec(spec, destRegex, convertTargetToBackslash, additionalValidators...)
if mp != nil {
mp.Mode = mode
}
if err != nil {
err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
}
return mp, err
}
func (p *windowsParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
return p.parseMountSpec(cfg, rxDestination, true, windowsSpecificValidators)
}
func (p *windowsParser) parseMountSpec(cfg mount.Mount, destRegex string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
if err := p.validateMountConfigReg(&cfg, destRegex, additionalValidators...); err != nil {
return nil, err
}
mp := &MountPoint{
RW: !cfg.ReadOnly,
Destination: cfg.Target,
Type: cfg.Type,
Spec: cfg,
}
if convertTargetToBackslash {
mp.Destination = strings.Replace(cfg.Target, `/`, `\`, -1)
}
switch cfg.Type {
case mount.TypeVolume:
if cfg.Source == "" {
mp.Name = stringid.GenerateNonCryptoID()
} else {
mp.Name = cfg.Source
}
mp.CopyData = p.DefaultCopyMode()
if cfg.VolumeOptions != nil {
if cfg.VolumeOptions.DriverConfig != nil {
mp.Driver = cfg.VolumeOptions.DriverConfig.Name
}
if cfg.VolumeOptions.NoCopy {
mp.CopyData = false
}
}
case mount.TypeBind:
mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1)
case mount.TypeNamedPipe:
mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1)
}
// cleanup trailing `\` except for paths like `c:\`
if len(mp.Source) > 3 && mp.Source[len(mp.Source)-1] == '\\' {
mp.Source = mp.Source[:len(mp.Source)-1]
}
if len(mp.Destination) > 3 && mp.Destination[len(mp.Destination)-1] == '\\' {
mp.Destination = mp.Destination[:len(mp.Destination)-1]
}
return mp, nil
}
func (p *windowsParser) 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 !windowsValidMountMode(mode) {
return "", "", errInvalidMode(mode)
}
// Do not allow copy modes on volumes-from
if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
return "", "", errInvalidMode(mode)
}
}
return id, mode, nil
}
func (p *windowsParser) DefaultPropagationMode() mount.Propagation {
return mount.Propagation("")
}
func (p *windowsParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) {
return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS)
}
func (p *windowsParser) DefaultCopyMode() bool {
return false
}
func (p *windowsParser) IsBackwardCompatible(m *MountPoint) bool {
return false
}
func (p *windowsParser) ValidateTmpfsMountDestination(dest string) error {
return errors.New("Platform does not support tmpfs")
}

View file

@ -0,0 +1,89 @@
package service // import "github.com/docker/docker/volume/service"
import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/volume"
)
// By is an interface which is used to implement filtering on volumes.
type By interface {
isBy()
}
// ByDriver is `By` that filters based on the driver names that are passed in
func ByDriver(drivers ...string) By {
return byDriver(drivers)
}
type byDriver []string
func (byDriver) isBy() {}
// ByReferenced is a `By` that filters based on if the volume has references
type ByReferenced bool
func (ByReferenced) isBy() {}
// And creates a `By` combining all the passed in bys using AND logic.
func And(bys ...By) By {
and := make(andCombinator, 0, len(bys))
for _, by := range bys {
and = append(and, by)
}
return and
}
type andCombinator []By
func (andCombinator) isBy() {}
// Or creates a `By` combining all the passed in bys using OR logic.
func Or(bys ...By) By {
or := make(orCombinator, 0, len(bys))
for _, by := range bys {
or = append(or, by)
}
return or
}
type orCombinator []By
func (orCombinator) isBy() {}
// CustomFilter is a `By` that is used by callers to provide custom filtering
// logic.
type CustomFilter filterFunc
func (CustomFilter) isBy() {}
// FromList returns a By which sets the initial list of volumes to use
func FromList(ls *[]volume.Volume, by By) By {
return &fromList{by: by, ls: ls}
}
type fromList struct {
by By
ls *[]volume.Volume
}
func (fromList) isBy() {}
func byLabelFilter(filter filters.Args) By {
return CustomFilter(func(v volume.Volume) bool {
dv, ok := v.(volume.DetailedVolume)
if !ok {
return false
}
labels := dv.Labels()
if !filter.MatchKVList("label", labels) {
return false
}
if filter.Contains("label!") {
if filter.MatchKVList("label!", labels) {
return false
}
}
return true
})
}

View file

@ -0,0 +1,132 @@
package service
import (
"context"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/directory"
"github.com/docker/docker/volume"
"github.com/sirupsen/logrus"
)
// convertOpts are used to pass options to `volumeToAPI`
type convertOpt interface {
isConvertOpt()
}
type useCachedPath bool
func (useCachedPath) isConvertOpt() {}
type calcSize bool
func (calcSize) isConvertOpt() {}
type pathCacher interface {
CachedPath() string
}
func (s *VolumesService) volumesToAPI(ctx context.Context, volumes []volume.Volume, opts ...convertOpt) []*types.Volume {
var (
out = make([]*types.Volume, 0, len(volumes))
getSize bool
cachedPath bool
)
for _, o := range opts {
switch t := o.(type) {
case calcSize:
getSize = bool(t)
case useCachedPath:
cachedPath = bool(t)
}
}
for _, v := range volumes {
select {
case <-ctx.Done():
return nil
default:
}
apiV := volumeToAPIType(v)
if cachedPath {
if vv, ok := v.(pathCacher); ok {
apiV.Mountpoint = vv.CachedPath()
}
} else {
apiV.Mountpoint = v.Path()
}
if getSize {
p := v.Path()
if apiV.Mountpoint == "" {
apiV.Mountpoint = p
}
sz, err := directory.Size(ctx, p)
if err != nil {
logrus.WithError(err).WithField("volume", v.Name()).Warnf("Failed to determine size of volume")
sz = -1
}
apiV.UsageData = &types.VolumeUsageData{Size: sz, RefCount: int64(s.vs.CountReferences(v))}
}
out = append(out, &apiV)
}
return out
}
func volumeToAPIType(v volume.Volume) types.Volume {
createdAt, _ := v.CreatedAt()
tv := types.Volume{
Name: v.Name(),
Driver: v.DriverName(),
CreatedAt: createdAt.Format(time.RFC3339),
}
if v, ok := v.(volume.DetailedVolume); ok {
tv.Labels = v.Labels()
tv.Options = v.Options()
tv.Scope = v.Scope()
}
if cp, ok := v.(pathCacher); ok {
tv.Mountpoint = cp.CachedPath()
}
return tv
}
func filtersToBy(filter filters.Args, acceptedFilters map[string]bool) (By, error) {
if err := filter.Validate(acceptedFilters); err != nil {
return nil, err
}
var bys []By
if drivers := filter.Get("driver"); len(drivers) > 0 {
bys = append(bys, ByDriver(drivers...))
}
if filter.Contains("name") {
bys = append(bys, CustomFilter(func(v volume.Volume) bool {
return filter.Match("name", v.Name())
}))
}
bys = append(bys, byLabelFilter(filter))
if filter.Contains("dangling") {
var dangling bool
if filter.ExactMatch("dangling", "true") || filter.ExactMatch("dangling", "1") {
dangling = true
} else if !filter.ExactMatch("dangling", "false") && !filter.ExactMatch("dangling", "0") {
return nil, invalidFilter{"dangling", filter.Get("dangling")}
}
bys = append(bys, ByReferenced(!dangling))
}
var by By
switch len(bys) {
case 0:
case 1:
by = bys[0]
default:
by = And(bys...)
}
return by, nil
}

View file

@ -0,0 +1,95 @@
package service // import "github.com/docker/docker/volume/service"
import (
"encoding/json"
"github.com/boltdb/bolt"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
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, err := tx.CreateBucketIfNotExists(volumeBucketName)
if err != nil {
return errors.Wrap(err, "error creating volume bucket")
}
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)
if b == nil {
return errdefs.NotFound(errors.New("volume bucket does not exist"))
}
val := b.Get([]byte(name))
if len(val) == 0 {
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
}

View file

@ -0,0 +1,52 @@
package service // import "github.com/docker/docker/volume/service"
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/boltdb/bolt"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestSetGetMeta(t *testing.T) {
t.Parallel()
dir, err := ioutil.TempDir("", "test-set-get")
assert.NilError(t, err)
defer os.RemoveAll(dir)
db, err := bolt.Open(filepath.Join(dir, "db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
assert.NilError(t, err)
store := &VolumeStore{db: db}
_, err = store.getMeta("test")
assert.Assert(t, is.ErrorContains(err, ""))
err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucket(volumeBucketName)
return err
})
assert.NilError(t, err)
meta, err := store.getMeta("test")
assert.NilError(t, err)
assert.DeepEqual(t, volumeMetadata{}, meta)
testMeta := volumeMetadata{
Name: "test",
Driver: "fake",
Labels: map[string]string{"a": "1", "b": "2"},
Options: map[string]string{"foo": "bar"},
}
err = store.setMeta("test", testMeta)
assert.NilError(t, err)
meta, err = store.getMeta("test")
assert.NilError(t, err)
assert.DeepEqual(t, testMeta, meta)
}

View file

@ -0,0 +1,21 @@
// +build linux windows
package service // import "github.com/docker/docker/volume/service"
import (
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/volume"
"github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/local"
"github.com/pkg/errors"
)
func setupDefaultDriver(store *drivers.Store, root string, rootIDs idtools.Identity) error {
d, err := local.New(root, rootIDs)
if err != nil {
return errors.Wrap(err, "error setting up default driver")
}
if !store.Register(d, volume.DefaultDriverName) {
return errors.New("local volume driver could not be registered")
}
return nil
}

View file

@ -0,0 +1,10 @@
// +build !linux,!windows
package service // import "github.com/docker/docker/volume/service"
import (
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/volume/drivers"
)
func setupDefaultDriver(_ *drivers.Store, _ string, _ idtools.Identity) error { return nil }

View file

@ -0,0 +1,111 @@
package service // import "github.com/docker/docker/volume/service"
import (
"fmt"
"strings"
)
const (
// errVolumeInUse is a typed error returned when trying to remove a volume that is currently in use by a container
errVolumeInUse conflictError = "volume is in use"
// errNoSuchVolume is a typed error returned if the requested volume doesn't exist in the volume store
errNoSuchVolume notFoundError = "no such volume"
// errNameConflict is a typed error returned on create when a volume exists with the given name, but for a different driver
errNameConflict conflictError = "volume name must be unique"
)
type conflictError string
func (e conflictError) Error() string {
return string(e)
}
func (conflictError) Conflict() {}
type notFoundError string
func (e notFoundError) Error() string {
return string(e)
}
func (notFoundError) NotFound() {}
// 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
}
// Cause returns the error the caused this error
func (e *OpErr) Cause() error {
return e.Err
}
// 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)
}
type causal interface {
Cause() error
}
func isErr(err error, expected error) bool {
switch pe := err.(type) {
case nil:
return false
case causal:
return isErr(pe.Cause(), expected)
}
return err == expected
}
type invalidFilter struct {
filter string
value interface{}
}
func (e invalidFilter) Error() string {
msg := "Invalid filter '" + e.filter
if e.value != nil {
msg += fmt.Sprintf("=%s", e.value)
}
return msg + "'"
}
func (e invalidFilter) InvalidParameter() {}

View file

@ -0,0 +1,89 @@
package opts
// CreateOption is used to pass options in when creating a volume
type CreateOption func(*CreateConfig)
// CreateConfig is the set of config options that can be set when creating
// a volume
type CreateConfig struct {
Options map[string]string
Labels map[string]string
Reference string
}
// WithCreateLabels creates a CreateOption which sets the labels to the
// passed in value
func WithCreateLabels(labels map[string]string) CreateOption {
return func(cfg *CreateConfig) {
cfg.Labels = labels
}
}
// WithCreateOptions creates a CreateOption which sets the options passed
// to the volume driver when creating a volume to the options passed in.
func WithCreateOptions(opts map[string]string) CreateOption {
return func(cfg *CreateConfig) {
cfg.Options = opts
}
}
// WithCreateReference creats a CreateOption which sets a reference to use
// when creating a volume. This ensures that the volume is created with a reference
// already attached to it to prevent race conditions with Create and volume cleanup.
func WithCreateReference(ref string) CreateOption {
return func(cfg *CreateConfig) {
cfg.Reference = ref
}
}
// GetConfig is used with `GetOption` to set options for the volumes service's
// `Get` implementation.
type GetConfig struct {
Driver string
Reference string
ResolveStatus bool
}
// GetOption is passed to the service `Get` add extra details on the get request
type GetOption func(*GetConfig)
// WithGetDriver provides the driver to get the volume from
// If no driver is provided to `Get`, first the available metadata is checked
// to see which driver it belongs to, if that is not available all drivers are
// probed to find the volume.
func WithGetDriver(name string) GetOption {
return func(o *GetConfig) {
o.Driver = name
}
}
// WithGetReference indicates to `Get` to increment the reference count for the
// retreived volume with the provided reference ID.
func WithGetReference(ref string) GetOption {
return func(o *GetConfig) {
o.Reference = ref
}
}
// WithGetResolveStatus indicates to `Get` to also fetch the volume status.
// This can cause significant overhead in the volume lookup.
func WithGetResolveStatus(cfg *GetConfig) {
cfg.ResolveStatus = true
}
// RemoveConfig is used by `RemoveOption` to store config options for remove
type RemoveConfig struct {
PurgeOnError bool
}
// RemoveOption is used to pass options to the volumes service `Remove` implementation
type RemoveOption func(*RemoveConfig)
// WithPurgeOnError is an option passed to `Remove` which will purge all cached
// data about a volume even if there was an error while attempting to remove the
// volume.
func WithPurgeOnError(b bool) RemoveOption {
return func(o *RemoveConfig) {
o.PurgeOnError = b
}
}

View file

@ -0,0 +1,85 @@
package service // import "github.com/docker/docker/volume/service"
import (
"context"
"sync"
"github.com/boltdb/bolt"
"github.com/docker/docker/volume"
"github.com/sirupsen/logrus"
)
// 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
})
ctx := context.Background()
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(ctx, s.drivers, 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(ctx, meta.Name, meta.Driver)
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
s.drivers.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.refs[v.Name()] = make(map[string]struct{})
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
})
}

View file

@ -0,0 +1,58 @@
package service // import "github.com/docker/docker/volume/service"
import (
"context"
"io/ioutil"
"os"
"testing"
"github.com/docker/docker/volume"
volumedrivers "github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/service/opts"
volumetestutils "github.com/docker/docker/volume/testutils"
"gotest.tools/assert"
)
func TestRestore(t *testing.T) {
t.Parallel()
dir, err := ioutil.TempDir("", "test-restore")
assert.NilError(t, err)
defer os.RemoveAll(dir)
drivers := volumedrivers.NewStore(nil)
driverName := "test-restore"
drivers.Register(volumetestutils.NewFakeDriver(driverName), driverName)
s, err := NewStore(dir, drivers)
assert.NilError(t, err)
defer s.Shutdown()
ctx := context.Background()
_, err = s.Create(ctx, "test1", driverName)
assert.NilError(t, err)
testLabels := map[string]string{"a": "1"}
testOpts := map[string]string{"foo": "bar"}
_, err = s.Create(ctx, "test2", driverName, opts.WithCreateOptions(testOpts), opts.WithCreateLabels(testLabels))
assert.NilError(t, err)
s.Shutdown()
s, err = NewStore(dir, drivers)
assert.NilError(t, err)
v, err := s.Get(ctx, "test1")
assert.NilError(t, err)
dv := v.(volume.DetailedVolume)
var nilMap map[string]string
assert.DeepEqual(t, nilMap, dv.Options())
assert.DeepEqual(t, nilMap, dv.Labels())
v, err = s.Get(ctx, "test2")
assert.NilError(t, err)
dv = v.(volume.DetailedVolume)
assert.DeepEqual(t, testOpts, dv.Options())
assert.DeepEqual(t, testLabels, dv.Labels())
}

View file

@ -0,0 +1,243 @@
package service // import "github.com/docker/docker/volume/service"
import (
"context"
"sync/atomic"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/directory"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/plugingetter"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/volume"
"github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/service/opts"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type ds interface {
GetDriverList() []string
}
type volumeEventLogger interface {
LogVolumeEvent(volumeID, action string, attributes map[string]string)
}
// VolumesService manages access to volumes
type VolumesService struct {
vs *VolumeStore
ds ds
pruneRunning int32
eventLogger volumeEventLogger
}
// NewVolumeService creates a new volume service
func NewVolumeService(root string, pg plugingetter.PluginGetter, rootIDs idtools.Identity, logger volumeEventLogger) (*VolumesService, error) {
ds := drivers.NewStore(pg)
if err := setupDefaultDriver(ds, root, rootIDs); err != nil {
return nil, err
}
vs, err := NewStore(root, ds)
if err != nil {
return nil, err
}
return &VolumesService{vs: vs, ds: ds, eventLogger: logger}, nil
}
// GetDriverList gets the list of registered volume drivers
func (s *VolumesService) GetDriverList() []string {
return s.ds.GetDriverList()
}
// Create creates a volume
func (s *VolumesService) Create(ctx context.Context, name, driverName string, opts ...opts.CreateOption) (*types.Volume, error) {
if name == "" {
name = stringid.GenerateNonCryptoID()
}
v, err := s.vs.Create(ctx, name, driverName, opts...)
if err != nil {
return nil, err
}
s.eventLogger.LogVolumeEvent(v.Name(), "create", map[string]string{"driver": v.DriverName()})
apiV := volumeToAPIType(v)
return &apiV, nil
}
// Get gets a volume
func (s *VolumesService) Get(ctx context.Context, name string, getOpts ...opts.GetOption) (*types.Volume, error) {
v, err := s.vs.Get(ctx, name, getOpts...)
if err != nil {
return nil, err
}
vol := volumeToAPIType(v)
var cfg opts.GetConfig
for _, o := range getOpts {
o(&cfg)
}
if cfg.ResolveStatus {
vol.Status = v.Status()
}
return &vol, nil
}
// Mount mounts the volume
func (s *VolumesService) Mount(ctx context.Context, vol *types.Volume, ref string) (string, error) {
v, err := s.vs.Get(ctx, vol.Name, opts.WithGetDriver(vol.Driver))
if err != nil {
if IsNotExist(err) {
err = errdefs.NotFound(err)
}
return "", err
}
return v.Mount(ref)
}
// Unmount unmounts the volume.
// Note that depending on the implementation, the volume may still be mounted due to other resources using it.
func (s *VolumesService) Unmount(ctx context.Context, vol *types.Volume, ref string) error {
v, err := s.vs.Get(ctx, vol.Name, opts.WithGetDriver(vol.Driver))
if err != nil {
if IsNotExist(err) {
err = errdefs.NotFound(err)
}
return err
}
return v.Unmount(ref)
}
// Release releases a volume reference
func (s *VolumesService) Release(ctx context.Context, name string, ref string) error {
return s.vs.Release(ctx, name, ref)
}
// Remove removes a volume
func (s *VolumesService) Remove(ctx context.Context, name string, rmOpts ...opts.RemoveOption) error {
var cfg opts.RemoveConfig
for _, o := range rmOpts {
o(&cfg)
}
v, err := s.vs.Get(ctx, name)
if err != nil {
if IsNotExist(err) && cfg.PurgeOnError {
return nil
}
return err
}
err = s.vs.Remove(ctx, v, rmOpts...)
if IsNotExist(err) {
err = nil
} else if IsInUse(err) {
err = errdefs.Conflict(err)
} else if IsNotExist(err) && cfg.PurgeOnError {
err = nil
}
if err == nil {
s.eventLogger.LogVolumeEvent(v.Name(), "destroy", map[string]string{"driver": v.DriverName()})
}
return err
}
var acceptedPruneFilters = map[string]bool{
"label": true,
"label!": true,
}
var acceptedListFilters = map[string]bool{
"dangling": true,
"name": true,
"driver": true,
"label": true,
}
// LocalVolumesSize gets all local volumes and fetches their size on disk
// Note that this intentionally skips volumes which have mount options. Typically
// volumes with mount options are not really local even if they are using the
// local driver.
func (s *VolumesService) LocalVolumesSize(ctx context.Context) ([]*types.Volume, error) {
ls, _, err := s.vs.Find(ctx, And(ByDriver(volume.DefaultDriverName), CustomFilter(func(v volume.Volume) bool {
dv, ok := v.(volume.DetailedVolume)
return ok && len(dv.Options()) == 0
})))
if err != nil {
return nil, err
}
return s.volumesToAPI(ctx, ls, calcSize(true)), nil
}
// Prune removes (local) volumes which match the past in filter arguments.
// Note that this intentionally skips volumes with mount options as there would
// be no space reclaimed in this case.
func (s *VolumesService) Prune(ctx context.Context, filter filters.Args) (*types.VolumesPruneReport, error) {
if !atomic.CompareAndSwapInt32(&s.pruneRunning, 0, 1) {
return nil, errdefs.Conflict(errors.New("a prune operation is already running"))
}
defer atomic.StoreInt32(&s.pruneRunning, 0)
by, err := filtersToBy(filter, acceptedPruneFilters)
if err != nil {
return nil, err
}
ls, _, err := s.vs.Find(ctx, And(ByDriver(volume.DefaultDriverName), ByReferenced(false), by, CustomFilter(func(v volume.Volume) bool {
dv, ok := v.(volume.DetailedVolume)
return ok && len(dv.Options()) == 0
})))
if err != nil {
return nil, err
}
rep := &types.VolumesPruneReport{VolumesDeleted: make([]string, 0, len(ls))}
for _, v := range ls {
select {
case <-ctx.Done():
err := ctx.Err()
if err == context.Canceled {
err = nil
}
return rep, err
default:
}
vSize, err := directory.Size(ctx, v.Path())
if err != nil {
logrus.WithField("volume", v.Name()).WithError(err).Warn("could not determine size of volume")
}
if err := s.vs.Remove(ctx, v); err != nil {
logrus.WithError(err).WithField("volume", v.Name()).Warnf("Could not determine size of volume")
continue
}
rep.SpaceReclaimed += uint64(vSize)
rep.VolumesDeleted = append(rep.VolumesDeleted, v.Name())
}
return rep, nil
}
// List gets the list of volumes which match the past in filters
// If filters is nil or empty all volumes are returned.
func (s *VolumesService) List(ctx context.Context, filter filters.Args) (volumesOut []*types.Volume, warnings []string, err error) {
by, err := filtersToBy(filter, acceptedListFilters)
if err != nil {
return nil, nil, err
}
volumes, warnings, err := s.vs.Find(ctx, by)
if err != nil {
return nil, nil, err
}
return s.volumesToAPI(ctx, volumes, useCachedPath(true)), warnings, nil
}
// Shutdown shuts down the image service and dependencies
func (s *VolumesService) Shutdown() error {
return s.vs.Shutdown()
}

View file

@ -0,0 +1,66 @@
package service
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/volume"
volumedrivers "github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/local"
"github.com/docker/docker/volume/service/opts"
"github.com/docker/docker/volume/testutils"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestLocalVolumeSize(t *testing.T) {
t.Parallel()
ds := volumedrivers.NewStore(nil)
dir, err := ioutil.TempDir("", t.Name())
assert.Assert(t, err)
defer os.RemoveAll(dir)
l, err := local.New(dir, idtools.Identity{UID: os.Getuid(), GID: os.Getegid()})
assert.Assert(t, err)
assert.Assert(t, ds.Register(l, volume.DefaultDriverName))
assert.Assert(t, ds.Register(testutils.NewFakeDriver("fake"), "fake"))
service, cleanup := newTestService(t, ds)
defer cleanup()
ctx := context.Background()
v1, err := service.Create(ctx, "test1", volume.DefaultDriverName, opts.WithCreateReference("foo"))
assert.Assert(t, err)
v2, err := service.Create(ctx, "test2", volume.DefaultDriverName)
assert.Assert(t, err)
_, err = service.Create(ctx, "test3", "fake")
assert.Assert(t, err)
data := make([]byte, 1024)
err = ioutil.WriteFile(filepath.Join(v1.Mountpoint, "data"), data, 0644)
assert.Assert(t, err)
err = ioutil.WriteFile(filepath.Join(v2.Mountpoint, "data"), data[:1], 0644)
assert.Assert(t, err)
ls, err := service.LocalVolumesSize(ctx)
assert.Assert(t, err)
assert.Assert(t, is.Len(ls, 2))
for _, v := range ls {
switch v.Name {
case "test1":
assert.Assert(t, is.Equal(v.UsageData.Size, int64(len(data))))
assert.Assert(t, is.Equal(v.UsageData.RefCount, int64(1)))
case "test2":
assert.Assert(t, is.Equal(v.UsageData.Size, int64(len(data[:1]))))
assert.Assert(t, is.Equal(v.UsageData.RefCount, int64(0)))
default:
t.Fatalf("got unexpected volume: %+v", v)
}
}
}

View file

@ -0,0 +1,253 @@
package service
import (
"context"
"io/ioutil"
"os"
"testing"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/volume"
volumedrivers "github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/service/opts"
"github.com/docker/docker/volume/testutils"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestServiceCreate(t *testing.T) {
t.Parallel()
ds := volumedrivers.NewStore(nil)
assert.Assert(t, ds.Register(testutils.NewFakeDriver("d1"), "d1"))
assert.Assert(t, ds.Register(testutils.NewFakeDriver("d2"), "d2"))
ctx := context.Background()
service, cleanup := newTestService(t, ds)
defer cleanup()
_, err := service.Create(ctx, "v1", "notexist")
assert.Assert(t, errdefs.IsNotFound(err), err)
v, err := service.Create(ctx, "v1", "d1")
assert.Assert(t, err)
vCopy, err := service.Create(ctx, "v1", "d1")
assert.Assert(t, err)
assert.Assert(t, is.DeepEqual(v, vCopy))
_, err = service.Create(ctx, "v1", "d2")
assert.Check(t, IsNameConflict(err), err)
assert.Check(t, errdefs.IsConflict(err), err)
assert.Assert(t, service.Remove(ctx, "v1"))
_, err = service.Create(ctx, "v1", "d2")
assert.Assert(t, err)
_, err = service.Create(ctx, "v1", "d2")
assert.Assert(t, err)
}
func TestServiceList(t *testing.T) {
t.Parallel()
ds := volumedrivers.NewStore(nil)
assert.Assert(t, ds.Register(testutils.NewFakeDriver("d1"), "d1"))
assert.Assert(t, ds.Register(testutils.NewFakeDriver("d2"), "d2"))
service, cleanup := newTestService(t, ds)
defer cleanup()
ctx := context.Background()
_, err := service.Create(ctx, "v1", "d1")
assert.Assert(t, err)
_, err = service.Create(ctx, "v2", "d1")
assert.Assert(t, err)
_, err = service.Create(ctx, "v3", "d2")
assert.Assert(t, err)
ls, _, err := service.List(ctx, filters.NewArgs(filters.Arg("driver", "d1")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 2))
ls, _, err = service.List(ctx, filters.NewArgs(filters.Arg("driver", "d2")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 1))
ls, _, err = service.List(ctx, filters.NewArgs(filters.Arg("driver", "notexist")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 0))
ls, _, err = service.List(ctx, filters.NewArgs(filters.Arg("dangling", "true")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 3))
ls, _, err = service.List(ctx, filters.NewArgs(filters.Arg("dangling", "false")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 0))
_, err = service.Get(ctx, "v1", opts.WithGetReference("foo"))
assert.Assert(t, err)
ls, _, err = service.List(ctx, filters.NewArgs(filters.Arg("dangling", "true")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 2))
ls, _, err = service.List(ctx, filters.NewArgs(filters.Arg("dangling", "false")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 1))
ls, _, err = service.List(ctx, filters.NewArgs(filters.Arg("dangling", "false"), filters.Arg("driver", "d2")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 0))
ls, _, err = service.List(ctx, filters.NewArgs(filters.Arg("dangling", "true"), filters.Arg("driver", "d2")))
assert.Assert(t, err)
assert.Check(t, is.Len(ls, 1))
}
func TestServiceRemove(t *testing.T) {
t.Parallel()
ds := volumedrivers.NewStore(nil)
assert.Assert(t, ds.Register(testutils.NewFakeDriver("d1"), "d1"))
service, cleanup := newTestService(t, ds)
defer cleanup()
ctx := context.Background()
_, err := service.Create(ctx, "test", "d1")
assert.Assert(t, err)
assert.Assert(t, service.Remove(ctx, "test"))
assert.Assert(t, service.Remove(ctx, "test", opts.WithPurgeOnError(true)))
}
func TestServiceGet(t *testing.T) {
t.Parallel()
ds := volumedrivers.NewStore(nil)
assert.Assert(t, ds.Register(testutils.NewFakeDriver("d1"), "d1"))
service, cleanup := newTestService(t, ds)
defer cleanup()
ctx := context.Background()
v, err := service.Get(ctx, "notexist")
assert.Assert(t, IsNotExist(err))
assert.Check(t, v == nil)
created, err := service.Create(ctx, "test", "d1")
assert.Assert(t, err)
assert.Assert(t, created != nil)
v, err = service.Get(ctx, "test")
assert.Assert(t, err)
assert.Assert(t, is.DeepEqual(created, v))
v, err = service.Get(ctx, "test", opts.WithGetResolveStatus)
assert.Assert(t, err)
assert.Assert(t, is.Len(v.Status, 1), v.Status)
v, err = service.Get(ctx, "test", opts.WithGetDriver("notarealdriver"))
assert.Assert(t, errdefs.IsConflict(err), err)
v, err = service.Get(ctx, "test", opts.WithGetDriver("d1"))
assert.Assert(t, err == nil)
assert.Assert(t, is.DeepEqual(created, v))
assert.Assert(t, ds.Register(testutils.NewFakeDriver("d2"), "d2"))
v, err = service.Get(ctx, "test", opts.WithGetDriver("d2"))
assert.Assert(t, errdefs.IsConflict(err), err)
}
func TestServicePrune(t *testing.T) {
t.Parallel()
ds := volumedrivers.NewStore(nil)
assert.Assert(t, ds.Register(testutils.NewFakeDriver(volume.DefaultDriverName), volume.DefaultDriverName))
assert.Assert(t, ds.Register(testutils.NewFakeDriver("other"), "other"))
service, cleanup := newTestService(t, ds)
defer cleanup()
ctx := context.Background()
_, err := service.Create(ctx, "test", volume.DefaultDriverName)
assert.Assert(t, err)
_, err = service.Create(ctx, "test2", "other")
assert.Assert(t, err)
pr, err := service.Prune(ctx, filters.NewArgs(filters.Arg("label", "banana")))
assert.Assert(t, err)
assert.Assert(t, is.Len(pr.VolumesDeleted, 0))
pr, err = service.Prune(ctx, filters.NewArgs())
assert.Assert(t, err)
assert.Assert(t, is.Len(pr.VolumesDeleted, 1))
assert.Assert(t, is.Equal(pr.VolumesDeleted[0], "test"))
_, err = service.Get(ctx, "test")
assert.Assert(t, IsNotExist(err), err)
v, err := service.Get(ctx, "test2")
assert.Assert(t, err)
assert.Assert(t, is.Equal(v.Driver, "other"))
_, err = service.Create(ctx, "test", volume.DefaultDriverName)
assert.Assert(t, err)
pr, err = service.Prune(ctx, filters.NewArgs(filters.Arg("label!", "banana")))
assert.Assert(t, err)
assert.Assert(t, is.Len(pr.VolumesDeleted, 1))
assert.Assert(t, is.Equal(pr.VolumesDeleted[0], "test"))
v, err = service.Get(ctx, "test2")
assert.Assert(t, err)
assert.Assert(t, is.Equal(v.Driver, "other"))
_, err = service.Create(ctx, "test", volume.DefaultDriverName, opts.WithCreateLabels(map[string]string{"banana": ""}))
assert.Assert(t, err)
pr, err = service.Prune(ctx, filters.NewArgs(filters.Arg("label!", "banana")))
assert.Assert(t, err)
assert.Assert(t, is.Len(pr.VolumesDeleted, 0))
_, err = service.Create(ctx, "test3", volume.DefaultDriverName, opts.WithCreateLabels(map[string]string{"banana": "split"}))
assert.Assert(t, err)
pr, err = service.Prune(ctx, filters.NewArgs(filters.Arg("label!", "banana=split")))
assert.Assert(t, err)
assert.Assert(t, is.Len(pr.VolumesDeleted, 1))
assert.Assert(t, is.Equal(pr.VolumesDeleted[0], "test"))
pr, err = service.Prune(ctx, filters.NewArgs(filters.Arg("label", "banana=split")))
assert.Assert(t, err)
assert.Assert(t, is.Len(pr.VolumesDeleted, 1))
assert.Assert(t, is.Equal(pr.VolumesDeleted[0], "test3"))
v, err = service.Create(ctx, "test", volume.DefaultDriverName, opts.WithCreateReference(t.Name()))
assert.Assert(t, err)
pr, err = service.Prune(ctx, filters.NewArgs())
assert.Assert(t, err)
assert.Assert(t, is.Len(pr.VolumesDeleted, 0))
assert.Assert(t, service.Release(ctx, v.Name, t.Name()))
pr, err = service.Prune(ctx, filters.NewArgs())
assert.Assert(t, err)
assert.Assert(t, is.Len(pr.VolumesDeleted, 1))
assert.Assert(t, is.Equal(pr.VolumesDeleted[0], "test"))
}
func newTestService(t *testing.T, ds *volumedrivers.Store) (*VolumesService, func()) {
t.Helper()
dir, err := ioutil.TempDir("", t.Name())
assert.Assert(t, err)
store, err := NewStore(dir, ds)
assert.Assert(t, err)
s := &VolumesService{vs: store, eventLogger: dummyEventLogger{}}
return s, func() {
assert.Check(t, s.Shutdown())
assert.Check(t, os.RemoveAll(dir))
}
}
type dummyEventLogger struct{}
func (dummyEventLogger) LogVolumeEvent(_, _ string, _ map[string]string) {}

View file

@ -0,0 +1,858 @@
package service // import "github.com/docker/docker/volume/service"
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/pkg/errors"
"github.com/boltdb/bolt"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/locker"
"github.com/docker/docker/volume"
"github.com/docker/docker/volume/drivers"
volumemounts "github.com/docker/docker/volume/mounts"
"github.com/docker/docker/volume/service/opts"
"github.com/sirupsen/logrus"
)
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 {
if v.options == nil {
return nil
}
options := make(map[string]string, len(v.options))
for key, value := range v.options {
options[key] = value
}
return options
}
func (v volumeWrapper) Labels() map[string]string {
if v.labels == nil {
return nil
}
labels := make(map[string]string, len(v.labels))
for key, value := range v.labels {
labels[key] = value
}
return 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()
}
// NewStore creates a new volume store at the given path
func NewStore(rootPath string, drivers *drivers.Store) (*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),
drivers: drivers,
}
if rootPath != "" {
// initialize metadata store
volPath := filepath.Join(rootPath, volumeDataDir)
if err := os.MkdirAll(volPath, 0750); err != nil {
return nil, err
}
var err error
vs.db, err = bolt.Open(filepath.Join(volPath, "metadata.db"), 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(ctx context.Context, name string) error {
s.globalLock.Lock()
defer s.globalLock.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
default:
}
v, exists := s.names[name]
if exists {
driverName := v.DriverName()
if _, err := s.drivers.ReleaseDriver(driverName); err != nil {
logrus.WithError(err).WithField("driver", driverName).Error("Error releasing reference to volume driver")
}
}
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)
return nil
}
// 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
drivers *drivers.Store
// 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
}
func filterByDriver(names []string) filterFunc {
return func(v volume.Volume) bool {
for _, name := range names {
if name == v.DriverName() {
return true
}
}
return false
}
}
func (s *VolumeStore) byReferenced(referenced bool) filterFunc {
return func(v volume.Volume) bool {
return s.hasRef(v.Name()) == referenced
}
}
func (s *VolumeStore) filter(ctx context.Context, vols *[]volume.Volume, by By) (warnings []string, err error) {
// note that this specifically does not support the `FromList` By type.
switch f := by.(type) {
case nil:
if *vols == nil {
var ls []volume.Volume
ls, warnings, err = s.list(ctx)
if err != nil {
return warnings, err
}
*vols = ls
}
case byDriver:
if *vols != nil {
filter(vols, filterByDriver([]string(f)))
return nil, nil
}
var ls []volume.Volume
ls, warnings, err = s.list(ctx, []string(f)...)
if err != nil {
return nil, err
}
*vols = ls
case ByReferenced:
// TODO(@cpuguy83): It would be nice to optimize this by looking at the list
// of referenced volumes, however the locking strategy makes this difficult
// without either providing inconsistent data or deadlocks.
if *vols == nil {
var ls []volume.Volume
ls, warnings, err = s.list(ctx)
if err != nil {
return nil, err
}
*vols = ls
}
filter(vols, s.byReferenced(bool(f)))
case andCombinator:
for _, by := range f {
w, err := s.filter(ctx, vols, by)
if err != nil {
return warnings, err
}
warnings = append(warnings, w...)
}
case orCombinator:
for _, by := range f {
switch by.(type) {
case byDriver:
var ls []volume.Volume
w, err := s.filter(ctx, &ls, by)
if err != nil {
return warnings, err
}
warnings = append(warnings, w...)
default:
ls, w, err := s.list(ctx)
if err != nil {
return warnings, err
}
warnings = append(warnings, w...)
w, err = s.filter(ctx, &ls, by)
if err != nil {
return warnings, err
}
warnings = append(warnings, w...)
*vols = append(*vols, ls...)
}
}
unique(vols)
case CustomFilter:
if *vols == nil {
var ls []volume.Volume
ls, warnings, err = s.list(ctx)
if err != nil {
return nil, err
}
*vols = ls
}
filter(vols, filterFunc(f))
default:
return nil, errdefs.InvalidParameter(errors.Errorf("unsupported filter: %T", f))
}
return warnings, nil
}
func unique(ls *[]volume.Volume) {
names := make(map[string]bool, len(*ls))
filter(ls, func(v volume.Volume) bool {
if names[v.Name()] {
return false
}
names[v.Name()] = true
return true
})
}
// Find lists volumes filtered by the past in filter.
// 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) Find(ctx context.Context, by By) (vols []volume.Volume, warnings []string, err error) {
logrus.WithField("ByType", fmt.Sprintf("%T", by)).WithField("ByValue", fmt.Sprintf("%+v", by)).Debug("VolumeStore.Find")
switch f := by.(type) {
case nil, orCombinator, andCombinator, byDriver, ByReferenced, CustomFilter:
warnings, err = s.filter(ctx, &vols, by)
case fromList:
warnings, err = s.filter(ctx, f.ls, f.by)
default:
// Really shouldn't be possible, but makes sure that any new By's are added to this check.
err = errdefs.InvalidParameter(errors.Errorf("unsupported filter type: %T", f))
}
if err != nil {
return nil, nil, &OpErr{Err: err, Op: "list"}
}
var out []volume.Volume
for _, v := range vols {
name := normalizeVolumeName(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
}
type filterFunc func(volume.Volume) bool
func filter(vols *[]volume.Volume, fn filterFunc) {
var evict []int
for i, v := range *vols {
if !fn(v) {
evict = append(evict, i)
}
}
for n, i := range evict {
copy((*vols)[i-n:], (*vols)[i-n+1:])
(*vols)[len(*vols)-1] = nil
*vols = (*vols)[:len(*vols)-1]
}
}
// list goes through each volume driver and asks for its list of volumes.
// TODO(@cpuguy83): plumb context through
func (s *VolumeStore) list(ctx context.Context, driverNames ...string) ([]volume.Volume, []string, error) {
var (
ls = []volume.Volume{} // do not return a nil value as this affects filtering
warnings []string
)
var dls []volume.Driver
all, err := s.drivers.GetAllDrivers()
if err != nil {
return nil, nil, err
}
if len(driverNames) == 0 {
dls = all
} else {
idx := make(map[string]bool, len(driverNames))
for _, name := range driverNames {
idx[name] = true
}
for _, d := range all {
if idx[d.Name()] {
dls = append(dls, d)
}
}
}
type vols struct {
vols []volume.Volume
err error
driverName string
}
chVols := make(chan vols, len(dls))
for _, vd := range dls {
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(dls); i++ {
vs := <-chVols
if vs.err != nil {
warnings = append(warnings, vs.err.Error())
badDrivers[vs.driverName] = struct{}{}
}
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
}
// Create creates a volume with the given name and driver
// If the volume needs to be created with a reference to prevent race conditions
// with volume cleanup, make sure to use the `CreateWithReference` option.
func (s *VolumeStore) Create(ctx context.Context, name, driverName string, createOpts ...opts.CreateOption) (volume.Volume, error) {
var cfg opts.CreateConfig
for _, o := range createOpts {
o(&cfg)
}
name = normalizeVolumeName(name)
s.locks.Lock(name)
defer s.locks.Unlock(name)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
v, err := s.create(ctx, name, driverName, cfg.Options, cfg.Labels)
if err != nil {
if _, ok := err.(*OpErr); ok {
return nil, err
}
return nil, &OpErr{Err: err, Name: name, Op: "create"}
}
s.setNamed(v, cfg.Reference)
return v, nil
}
// 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(ctx context.Context, 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 := s.drivers.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(ctx, s.drivers, 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(ctx, 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(ctx context.Context, store *drivers.Store, v volume.Volume) (bool, error) {
exists, err := lookupVolume(ctx, store, 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(ctx context.Context, name, driverName string, opts, labels map[string]string) (volume.Volume, error) {
// Validate the name in a platform-specific manner
// volume name validation is specific to the host os and not on container image
// windows/lcow should have an equivalent volumename validation logic so we create a parser for current host OS
parser := volumemounts.NewParser(runtime.GOOS)
err := parser.ValidateVolumeName(name)
if err != nil {
return nil, err
}
v, err := s.checkConflict(ctx, name, driverName)
if err != nil {
return nil, err
}
if v != nil {
// there is an existing volume, if we already have this stored locally, return it.
// TODO: there could be some inconsistent details such as labels here
if vv, _ := s.getNamed(v.Name()); vv != nil {
return vv, 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(ctx, name, "")
if v != nil {
return v, nil
}
}
if driverName == "" {
driverName = volume.DefaultDriverName
}
vd, err := s.drivers.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 {
v, err = vd.Create(name, opts)
if err != nil {
if _, err := s.drivers.ReleaseDriver(driverName); err != nil {
logrus.WithError(err).WithField("driver", driverName).Error("Error releasing reference to volume driver")
}
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
}
// Get looks if a volume with the given name exists and returns it if so
func (s *VolumeStore) Get(ctx context.Context, name string, getOptions ...opts.GetOption) (volume.Volume, error) {
var cfg opts.GetConfig
for _, o := range getOptions {
o(&cfg)
}
name = normalizeVolumeName(name)
s.locks.Lock(name)
defer s.locks.Unlock(name)
v, err := s.getVolume(ctx, name, cfg.Driver)
if err != nil {
return nil, &OpErr{Err: err, Name: name, Op: "get"}
}
if cfg.Driver != "" && v.DriverName() != cfg.Driver {
return nil, &OpErr{Name: name, Op: "get", Err: errdefs.Conflict(errors.New("found volume driver does not match passed in driver"))}
}
s.setNamed(v, cfg.Reference)
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(ctx context.Context, name, driverName string) (volume.Volume, error) {
var meta volumeMetadata
meta, err := s.getMeta(name)
if err != nil {
return nil, err
}
if driverName != "" {
if meta.Driver == "" {
meta.Driver = driverName
}
if driverName != meta.Driver {
return nil, errdefs.Conflict(errors.New("provided volume driver does not match stored driver"))
}
}
if driverName == "" {
driverName = meta.Driver
}
if driverName == "" {
s.globalLock.RLock()
select {
case <-ctx.Done():
s.globalLock.RUnlock()
return nil, ctx.Err()
default:
}
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(ctx, s.drivers, meta.Driver, name)
if err != nil {
return nil, err
}
if vol == nil {
s.purge(ctx, name)
return nil, errNoSuchVolume
}
var scope string
vd, err := s.drivers.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 := s.drivers.GetAllDrivers()
if err != nil {
return nil, err
}
for _, d := range drivers {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
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``
// TODO(@cpuguy83): plumb through the context to lower level components
func lookupVolume(ctx context.Context, store *drivers.Store, driverName, volumeName string) (volume.Volume, error) {
if driverName == "" {
driverName = volume.DefaultDriverName
}
vd, err := store.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).Debug("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(ctx context.Context, v volume.Volume, rmOpts ...opts.RemoveOption) error {
var cfg opts.RemoveConfig
for _, o := range rmOpts {
o(&cfg)
}
name := v.Name()
s.locks.Lock(name)
defer s.locks.Unlock(name)
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if s.hasRef(name) {
return &OpErr{Err: errVolumeInUse, Name: name, Op: "remove", Refs: s.getRefs(name)}
}
v, err := s.getVolume(ctx, name, v.DriverName())
if err != nil {
return err
}
vd, err := s.drivers.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)
err = vd.Remove(vol)
if err != nil {
err = &OpErr{Err: err, Name: name, Op: "remove"}
}
if err == nil || cfg.PurgeOnError {
if e := s.purge(ctx, name); e != nil && err == nil {
err = e
}
}
return err
}
// Release releases the specified reference to the volume
func (s *VolumeStore) Release(ctx context.Context, name string, ref string) error {
s.locks.Lock(name)
defer s.locks.Unlock(name)
select {
case <-ctx.Done():
return ctx.Err()
default:
}
s.globalLock.Lock()
defer s.globalLock.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if s.refs[name] != nil {
delete(s.refs[name], ref)
}
return nil
}
// CountReferences gives a count of all references for a given volume.
func (s *VolumeStore) CountReferences(v volume.Volume) int {
name := normalizeVolumeName(v.Name())
s.locks.Lock(name)
defer s.locks.Unlock(name)
s.globalLock.Lock()
defer s.globalLock.Unlock()
return len(s.refs[name])
}
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()
}

View file

@ -0,0 +1,421 @@
package service // import "github.com/docker/docker/volume/service"
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"strings"
"testing"
"github.com/docker/docker/volume"
volumedrivers "github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/service/opts"
volumetestutils "github.com/docker/docker/volume/testutils"
"github.com/google/go-cmp/cmp"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestCreate(t *testing.T) {
t.Parallel()
s, cleanup := setupTest(t)
defer cleanup()
s.drivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
ctx := context.Background()
v, err := s.Create(ctx, "fake1", "fake")
if err != nil {
t.Fatal(err)
}
if v.Name() != "fake1" {
t.Fatalf("Expected fake1 volume, got %v", v)
}
if l, _, _ := s.Find(ctx, nil); len(l) != 1 {
t.Fatalf("Expected 1 volume in the store, got %v: %v", len(l), l)
}
if _, err := s.Create(ctx, "none", "none"); err == nil {
t.Fatalf("Expected unknown driver error, got nil")
}
_, err = s.Create(ctx, "fakeerror", "fake", opts.WithCreateOptions(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) {
t.Parallel()
s, cleanup := setupTest(t)
defer cleanup()
s.drivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
s.drivers.Register(volumetestutils.NewFakeDriver("noop"), "noop")
ctx := context.Background()
// doing string compare here since this error comes directly from the driver
expected := "no such volume"
var v volume.Volume = volumetestutils.NoopVolume{}
if err := s.Remove(ctx, v); err == nil || !strings.Contains(err.Error(), expected) {
t.Fatalf("Expected error %q, got %v", expected, err)
}
v, err := s.Create(ctx, "fake1", "fake", opts.WithCreateReference("fake"))
if err != nil {
t.Fatal(err)
}
if err := s.Remove(ctx, v); !IsInUse(err) {
t.Fatalf("Expected ErrVolumeInUse error, got %v", err)
}
s.Release(ctx, v.Name(), "fake")
if err := s.Remove(ctx, v); err != nil {
t.Fatal(err)
}
if l, _, _ := s.Find(ctx, nil); len(l) != 0 {
t.Fatalf("Expected 0 volumes in the store, got %v, %v", len(l), l)
}
}
func TestList(t *testing.T) {
t.Parallel()
dir, err := ioutil.TempDir("", "test-list")
assert.NilError(t, err)
defer os.RemoveAll(dir)
drivers := volumedrivers.NewStore(nil)
drivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
drivers.Register(volumetestutils.NewFakeDriver("fake2"), "fake2")
s, err := NewStore(dir, drivers)
assert.NilError(t, err)
ctx := context.Background()
if _, err := s.Create(ctx, "test", "fake"); err != nil {
t.Fatal(err)
}
if _, err := s.Create(ctx, "test2", "fake2"); err != nil {
t.Fatal(err)
}
ls, _, err := s.Find(ctx, nil)
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 = NewStore(dir, drivers)
if err != nil {
t.Fatal(err)
}
ls, _, err = s.Find(ctx, nil)
if err != nil {
t.Fatal(err)
}
if len(ls) != 2 {
t.Fatalf("expected 2 volumes, got: %d", len(ls))
}
}
func TestFindByDriver(t *testing.T) {
t.Parallel()
s, cleanup := setupTest(t)
defer cleanup()
assert.Assert(t, s.drivers.Register(volumetestutils.NewFakeDriver("fake"), "fake"))
assert.Assert(t, s.drivers.Register(volumetestutils.NewFakeDriver("noop"), "noop"))
ctx := context.Background()
_, err := s.Create(ctx, "fake1", "fake")
assert.NilError(t, err)
_, err = s.Create(ctx, "fake2", "fake")
assert.NilError(t, err)
_, err = s.Create(ctx, "fake3", "noop")
assert.NilError(t, err)
l, _, err := s.Find(ctx, ByDriver("fake"))
assert.NilError(t, err)
assert.Equal(t, len(l), 2)
l, _, err = s.Find(ctx, ByDriver("noop"))
assert.NilError(t, err)
assert.Equal(t, len(l), 1)
l, _, err = s.Find(ctx, ByDriver("nosuchdriver"))
assert.NilError(t, err)
assert.Equal(t, len(l), 0)
}
func TestFindByReferenced(t *testing.T) {
t.Parallel()
s, cleanup := setupTest(t)
defer cleanup()
s.drivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
s.drivers.Register(volumetestutils.NewFakeDriver("noop"), "noop")
ctx := context.Background()
if _, err := s.Create(ctx, "fake1", "fake", opts.WithCreateReference("volReference")); err != nil {
t.Fatal(err)
}
if _, err := s.Create(ctx, "fake2", "fake"); err != nil {
t.Fatal(err)
}
dangling, _, err := s.Find(ctx, ByReferenced(false))
assert.Assert(t, err)
assert.Assert(t, len(dangling) == 1)
assert.Check(t, dangling[0].Name() == "fake2")
used, _, err := s.Find(ctx, ByReferenced(true))
assert.Assert(t, err)
assert.Assert(t, len(used) == 1)
assert.Check(t, used[0].Name() == "fake1")
}
func TestDerefMultipleOfSameRef(t *testing.T) {
t.Parallel()
s, cleanup := setupTest(t)
defer cleanup()
s.drivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
ctx := context.Background()
v, err := s.Create(ctx, "fake1", "fake", opts.WithCreateReference("volReference"))
if err != nil {
t.Fatal(err)
}
if _, err := s.Get(ctx, "fake1", opts.WithGetDriver("fake"), opts.WithGetReference("volReference")); err != nil {
t.Fatal(err)
}
s.Release(ctx, v.Name(), "volReference")
if err := s.Remove(ctx, v); err != nil {
t.Fatal(err)
}
}
func TestCreateKeepOptsLabelsWhenExistsRemotely(t *testing.T) {
t.Parallel()
s, cleanup := setupTest(t)
defer cleanup()
vd := volumetestutils.NewFakeDriver("fake")
s.drivers.Register(vd, "fake")
// Create a volume in the driver directly
if _, err := vd.Create("foo", nil); err != nil {
t.Fatal(err)
}
ctx := context.Background()
v, err := s.Create(ctx, "foo", "fake", opts.WithCreateLabels(map[string]string{"hello": "world"}))
if err != nil {
t.Fatal(err)
}
switch dv := v.(type) {
case volume.DetailedVolume:
if dv.Labels()["hello"] != "world" {
t.Fatalf("labels don't match")
}
default:
t.Fatalf("got unexpected type: %T", v)
}
}
func TestDefererencePluginOnCreateError(t *testing.T) {
t.Parallel()
var (
l net.Listener
err error
)
for i := 32768; l == nil && i < 40000; i++ {
l, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", i))
}
if l == nil {
t.Fatalf("could not create listener: %v", err)
}
defer l.Close()
s, cleanup := setupTest(t)
defer cleanup()
d := volumetestutils.NewFakeDriver("TestDefererencePluginOnCreateError")
p, err := volumetestutils.MakeFakePlugin(d, l)
if err != nil {
t.Fatal(err)
}
pg := volumetestutils.NewFakePluginGetter(p)
s.drivers = volumedrivers.NewStore(pg)
ctx := context.Background()
// create a good volume so we have a plugin reference
_, err = s.Create(ctx, "fake1", d.Name())
if err != nil {
t.Fatal(err)
}
// Now create another one expecting an error
_, err = s.Create(ctx, "fake2", d.Name(), opts.WithCreateOptions(map[string]string{"error": "some error"}))
if err == nil || !strings.Contains(err.Error(), "some error") {
t.Fatalf("expected an error on create: %v", err)
}
// There should be only 1 plugin reference
if refs := volumetestutils.FakeRefs(p); refs != 1 {
t.Fatalf("expected 1 plugin reference, got: %d", refs)
}
}
func TestRefDerefRemove(t *testing.T) {
t.Parallel()
driverName := "test-ref-deref-remove"
s, cleanup := setupTest(t)
defer cleanup()
s.drivers.Register(volumetestutils.NewFakeDriver(driverName), driverName)
ctx := context.Background()
v, err := s.Create(ctx, "test", driverName, opts.WithCreateReference("test-ref"))
assert.NilError(t, err)
err = s.Remove(ctx, v)
assert.Assert(t, is.ErrorContains(err, ""))
assert.Equal(t, errVolumeInUse, err.(*OpErr).Err)
s.Release(ctx, v.Name(), "test-ref")
err = s.Remove(ctx, v)
assert.NilError(t, err)
}
func TestGet(t *testing.T) {
t.Parallel()
driverName := "test-get"
s, cleanup := setupTest(t)
defer cleanup()
s.drivers.Register(volumetestutils.NewFakeDriver(driverName), driverName)
ctx := context.Background()
_, err := s.Get(ctx, "not-exist")
assert.Assert(t, is.ErrorContains(err, ""))
assert.Equal(t, errNoSuchVolume, err.(*OpErr).Err)
v1, err := s.Create(ctx, "test", driverName, opts.WithCreateLabels(map[string]string{"a": "1"}))
assert.NilError(t, err)
v2, err := s.Get(ctx, "test")
assert.NilError(t, err)
assert.DeepEqual(t, v1, v2, cmpVolume)
dv := v2.(volume.DetailedVolume)
assert.Equal(t, "1", dv.Labels()["a"])
err = s.Remove(ctx, v1)
assert.NilError(t, err)
}
func TestGetWithReference(t *testing.T) {
t.Parallel()
driverName := "test-get-with-ref"
s, cleanup := setupTest(t)
defer cleanup()
s.drivers.Register(volumetestutils.NewFakeDriver(driverName), driverName)
ctx := context.Background()
_, err := s.Get(ctx, "not-exist", opts.WithGetDriver(driverName), opts.WithGetReference("test-ref"))
assert.Assert(t, is.ErrorContains(err, ""))
v1, err := s.Create(ctx, "test", driverName, opts.WithCreateLabels(map[string]string{"a": "1"}))
assert.NilError(t, err)
v2, err := s.Get(ctx, "test", opts.WithGetDriver(driverName), opts.WithGetReference("test-ref"))
assert.NilError(t, err)
assert.DeepEqual(t, v1, v2, cmpVolume)
err = s.Remove(ctx, v2)
assert.Assert(t, is.ErrorContains(err, ""))
assert.Equal(t, errVolumeInUse, err.(*OpErr).Err)
s.Release(ctx, v2.Name(), "test-ref")
err = s.Remove(ctx, v2)
assert.NilError(t, err)
}
var cmpVolume = cmp.AllowUnexported(volumetestutils.FakeVolume{}, volumeWrapper{})
func setupTest(t *testing.T) (*VolumeStore, func()) {
t.Helper()
dirName := strings.Replace(t.Name(), string(os.PathSeparator), "_", -1)
dir, err := ioutil.TempDir("", dirName)
assert.NilError(t, err)
cleanup := func() {
t.Helper()
err := os.RemoveAll(dir)
assert.Check(t, err)
}
s, err := NewStore(dir, volumedrivers.NewStore(nil))
assert.Check(t, err)
return s, func() {
s.Shutdown()
cleanup()
}
}
func TestFilterFunc(t *testing.T) {
testDriver := volumetestutils.NewFakeDriver("test")
testVolume, err := testDriver.Create("test", nil)
assert.NilError(t, err)
testVolume2, err := testDriver.Create("test2", nil)
assert.NilError(t, err)
testVolume3, err := testDriver.Create("test3", nil)
assert.NilError(t, err)
for _, test := range []struct {
vols []volume.Volume
fn filterFunc
desc string
expect []volume.Volume
}{
{desc: "test nil list", vols: nil, expect: nil, fn: func(volume.Volume) bool { return true }},
{desc: "test empty list", vols: []volume.Volume{}, expect: []volume.Volume{}, fn: func(volume.Volume) bool { return true }},
{desc: "test filter non-empty to empty", vols: []volume.Volume{testVolume}, expect: []volume.Volume{}, fn: func(volume.Volume) bool { return false }},
{desc: "test nothing to fitler non-empty list", vols: []volume.Volume{testVolume}, expect: []volume.Volume{testVolume}, fn: func(volume.Volume) bool { return true }},
{desc: "test filter some", vols: []volume.Volume{testVolume, testVolume2}, expect: []volume.Volume{testVolume}, fn: func(v volume.Volume) bool { return v.Name() == testVolume.Name() }},
{desc: "test filter middle", vols: []volume.Volume{testVolume, testVolume2, testVolume3}, expect: []volume.Volume{testVolume, testVolume3}, fn: func(v volume.Volume) bool { return v.Name() != testVolume2.Name() }},
{desc: "test filter middle and last", vols: []volume.Volume{testVolume, testVolume2, testVolume3}, expect: []volume.Volume{testVolume}, fn: func(v volume.Volume) bool { return v.Name() != testVolume2.Name() && v.Name() != testVolume3.Name() }},
{desc: "test filter first and last", vols: []volume.Volume{testVolume, testVolume2, testVolume3}, expect: []volume.Volume{testVolume2}, fn: func(v volume.Volume) bool { return v.Name() != testVolume.Name() && v.Name() != testVolume3.Name() }},
} {
t.Run(test.desc, func(t *testing.T) {
test := test
t.Parallel()
filter(&test.vols, test.fn)
assert.DeepEqual(t, test.vols, test.expect, cmpVolume)
})
}
}

View file

@ -0,0 +1,9 @@
// +build linux freebsd darwin
package service // import "github.com/docker/docker/volume/service"
// normalizeVolumeName is a platform specific function to normalize the name
// of a volume. This is a no-op on Unix-like platforms
func normalizeVolumeName(name string) string {
return name
}

View file

@ -0,0 +1,12 @@
package service // import "github.com/docker/docker/volume/service"
import "strings"
// normalizeVolumeName is a platform specific function to normalize 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 normalizeVolumeName(name string) string {
return strings.ToLower(name)
}

View file

@ -0,0 +1,230 @@
package testutils // import "github.com/docker/docker/volume/testutils"
import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/docker/docker/pkg/plugingetter"
"github.com/docker/docker/pkg/plugins"
"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 provides 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
createdAt time.Time
}
// NewFakeVolume creates a new fake volume for testing
func NewFakeVolume(name string, driverName string) volume.Volume {
return FakeVolume{name: name, driverName: driverName, createdAt: time.Now()}
}
// 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 provides low-level details about the volume
func (FakeVolume) Status() map[string]interface{} {
return map[string]interface{}{"datakey": "datavalue"}
}
// CreatedAt provides the time the volume (directory) was created at
func (f FakeVolume) CreatedAt() (time.Time, error) {
return f.createdAt, 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"
}
type fakePlugin struct {
client *plugins.Client
name string
refs int
}
// MakeFakePlugin creates a fake plugin from the passed in driver
// Note: currently only "Create" is implemented because that's all that's needed
// so far. If you need it to test something else, add it here, but probably you
// shouldn't need to use this except for very specific cases with v2 plugin handling.
func MakeFakePlugin(d volume.Driver, l net.Listener) (plugingetter.CompatPlugin, error) {
c, err := plugins.NewClient(l.Addr().Network()+"://"+l.Addr().String(), nil)
if err != nil {
return nil, err
}
mux := http.NewServeMux()
mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) {
createReq := struct {
Name string
Opts map[string]string
}{}
if err := json.NewDecoder(r.Body).Decode(&createReq); err != nil {
fmt.Fprintf(w, `{"Err": "%s"}`, err.Error())
return
}
_, err := d.Create(createReq.Name, createReq.Opts)
if err != nil {
fmt.Fprintf(w, `{"Err": "%s"}`, err.Error())
return
}
w.Write([]byte("{}"))
})
go http.Serve(l, mux)
return &fakePlugin{client: c, name: d.Name()}, nil
}
func (p *fakePlugin) Client() *plugins.Client {
return p.client
}
func (p *fakePlugin) Name() string {
return p.name
}
func (p *fakePlugin) IsV1() bool {
return false
}
func (p *fakePlugin) ScopedPath(s string) string {
return s
}
type fakePluginGetter struct {
plugins map[string]plugingetter.CompatPlugin
}
// NewFakePluginGetter returns a plugin getter for fake plugins
func NewFakePluginGetter(pls ...plugingetter.CompatPlugin) plugingetter.PluginGetter {
idx := make(map[string]plugingetter.CompatPlugin, len(pls))
for _, p := range pls {
idx[p.Name()] = p
}
return &fakePluginGetter{plugins: idx}
}
// This ignores the second argument since we only care about volume drivers here,
// there shouldn't be any other kind of plugin in here
func (g *fakePluginGetter) Get(name, _ string, mode int) (plugingetter.CompatPlugin, error) {
p, ok := g.plugins[name]
if !ok {
return nil, errors.New("not found")
}
p.(*fakePlugin).refs += mode
return p, nil
}
func (g *fakePluginGetter) GetAllByCap(capability string) ([]plugingetter.CompatPlugin, error) {
panic("GetAllByCap shouldn't be called")
}
func (g *fakePluginGetter) GetAllManagedPluginsByCap(capability string) []plugingetter.CompatPlugin {
panic("GetAllManagedPluginsByCap should not be called")
}
func (g *fakePluginGetter) Handle(capability string, callback func(string, *plugins.Client)) {
panic("Handle should not be called")
}
// FakeRefs checks ref count on a fake plugin.
func FakeRefs(p plugingetter.CompatPlugin) int {
// this should panic if something other than a `*fakePlugin` is passed in
return p.(*fakePlugin).refs
}

View file

@ -0,0 +1,69 @@
package volume // import "github.com/docker/docker/volume"
import (
"time"
)
// 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
}