9af963ab |
// Package local provides the default implementation for volumes. It
// is used to mount data volume containers and directories local to
// the host server. |
81fa9feb |
package local
import ( |
b05b2370 |
"encoding/json" |
81fa9feb |
"fmt"
"io/ioutil"
"os"
"path/filepath" |
246d1eb5 |
"reflect" |
668fa8af |
"strings" |
81fa9feb |
"sync"
|
2a5e85e2 |
"github.com/pkg/errors"
|
dba271a4 |
"github.com/docker/docker/api" |
442b4562 |
"github.com/docker/docker/pkg/idtools" |
b05b2370 |
"github.com/docker/docker/pkg/mount" |
81fa9feb |
"github.com/docker/docker/volume" |
1009e6a4 |
"github.com/sirupsen/logrus" |
81fa9feb |
)
|
bd9814f0 |
// VolumeDataPathName is the name of the directory where the volume data is stored. |
927b334e |
// It uses a very distinctive name to avoid collisions migrating data between |
bd9814f0 |
// Docker versions.
const (
VolumeDataPathName = "_data"
volumesPathName = "volumes"
)
|
d6d60287 |
var (
// ErrNotFound is the typed error returned when the requested volume name can't be found |
a793564b |
ErrNotFound = fmt.Errorf("volume not found") |
927b334e |
// volumeNameRegex ensures the name assigned for the volume is valid. |
d6d60287 |
// 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. |
dba271a4 |
volumeNameRegex = api.RestrictedNamePattern |
d6d60287 |
) |
bd9814f0 |
|
a793564b |
type validationError struct {
error
}
func (validationError) IsValidationError() bool {
return true
}
|
b05b2370 |
type activeMount struct {
count uint64
mounted bool
}
|
9af963ab |
// 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. |
09cd96c5 |
func New(scope string, rootIDs idtools.IDPair) (*Root, error) { |
bd9814f0 |
rootDirectory := filepath.Join(scope, volumesPathName)
|
09cd96c5 |
if err := idtools.MkdirAllAndChown(rootDirectory, 0700, rootIDs); err != nil { |
81fa9feb |
return nil, err
} |
bd9814f0 |
|
81fa9feb |
r := &Root{ |
bd9814f0 |
scope: scope, |
81fa9feb |
path: rootDirectory, |
9af963ab |
volumes: make(map[string]*localVolume), |
09cd96c5 |
rootIDs: rootIDs, |
81fa9feb |
} |
bd9814f0 |
|
81fa9feb |
dirs, err := ioutil.ReadDir(rootDirectory)
if err != nil {
return nil, err
} |
bd9814f0 |
|
b05b2370 |
mountInfos, err := mount.GetMounts()
if err != nil {
logrus.Debugf("error looking up mounts for local volume cleanup: %v", err)
}
|
81fa9feb |
for _, d := range dirs { |
fc214b44 |
if !d.IsDir() {
continue
}
|
81fa9feb |
name := filepath.Base(d.Name()) |
b05b2370 |
v := &localVolume{ |
81fa9feb |
driverName: r.Name(),
name: name, |
bd9814f0 |
path: r.DataPath(name), |
81fa9feb |
} |
b05b2370 |
r.volumes[name] = v |
c560dd96 |
optsFilePath := filepath.Join(rootDirectory, name, "opts.json")
if b, err := ioutil.ReadFile(optsFilePath); err == nil { |
246d1eb5 |
opts := optsConfig{}
if err := json.Unmarshal(b, &opts); err != nil { |
2a5e85e2 |
return nil, errors.Wrapf(err, "error while unmarshaling volume options for volume: %s", name) |
b05b2370 |
} |
c7075bd1 |
// Make sure this isn't an empty optsConfig.
// This could be empty due to buggy behavior in older versions of Docker. |
246d1eb5 |
if !reflect.DeepEqual(opts, optsConfig{}) {
v.opts = &opts
} |
b05b2370 |
// unmount anything that may still be mounted (for example, from an unclean shutdown)
for _, info := range mountInfos {
if info.Mountpoint == v.path {
mount.Unmount(v.path)
break
}
}
} |
81fa9feb |
} |
b3b7eb27 |
|
81fa9feb |
return r, nil
}
|
9af963ab |
// 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. |
81fa9feb |
type Root struct {
m sync.Mutex |
bd9814f0 |
scope string |
81fa9feb |
path string |
9af963ab |
volumes map[string]*localVolume |
09cd96c5 |
rootIDs idtools.IDPair |
81fa9feb |
}
|
b3b7eb27 |
// List lists all the volumes |
d3eca445 |
func (r *Root) List() ([]volume.Volume, error) { |
b3b7eb27 |
var ls []volume.Volume |
3536c09c |
r.m.Lock() |
b3b7eb27 |
for _, v := range r.volumes {
ls = append(ls, v)
} |
3536c09c |
r.m.Unlock() |
d3eca445 |
return ls, nil |
b3b7eb27 |
}
|
9af963ab |
// DataPath returns the constructed path of this volume. |
bd9814f0 |
func (r *Root) DataPath(volumeName string) string {
return filepath.Join(r.path, volumeName, VolumeDataPathName)
}
|
9af963ab |
// Name returns the name of Root, defined in the volume package in the DefaultDriverName constant. |
81fa9feb |
func (r *Root) Name() string { |
9af963ab |
return volume.DefaultDriverName |
81fa9feb |
}
|
9af963ab |
// Create creates a new volume.Volume with the provided name, creating
// the underlying directory tree required for this volume in the
// process. |
b05b2370 |
func (r *Root) Create(name string, opts map[string]string) (volume.Volume, error) { |
d6d60287 |
if err := r.validateName(name); err != nil {
return nil, err
}
|
81fa9feb |
r.m.Lock()
defer r.m.Unlock() |
bd9814f0 |
|
81fa9feb |
v, exists := r.volumes[name] |
b3b7eb27 |
if exists {
return v, nil
}
path := r.DataPath(name) |
09cd96c5 |
if err := idtools.MkdirAllAndChown(path, 0755, r.rootIDs); err != nil { |
b3b7eb27 |
if os.IsExist(err) {
return nil, fmt.Errorf("volume already exists under %s", filepath.Dir(path)) |
81fa9feb |
} |
2a5e85e2 |
return nil, errors.Wrapf(err, "error while creating volume path '%s'", path) |
81fa9feb |
} |
b05b2370 |
var err error
defer func() {
if err != nil {
os.RemoveAll(filepath.Dir(path))
}
}()
|
b3b7eb27 |
v = &localVolume{
driverName: r.Name(),
name: name,
path: path,
} |
b05b2370 |
|
246d1eb5 |
if len(opts) != 0 { |
b05b2370 |
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 { |
2a5e85e2 |
return nil, errors.Wrap(err, "error while persisting volume options") |
b05b2370 |
}
}
|
b3b7eb27 |
r.volumes[name] = v |
81fa9feb |
return v, nil
}
|
9af963ab |
// 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. |
81fa9feb |
func (r *Root) Remove(v volume.Volume) error {
r.m.Lock()
defer r.m.Unlock() |
8d27417b |
|
9af963ab |
lv, ok := v.(*localVolume) |
81fa9feb |
if !ok { |
fc214b44 |
return fmt.Errorf("unknown volume type %T", v) |
81fa9feb |
} |
bd9814f0 |
|
db3576f8 |
if lv.active.count > 0 {
return fmt.Errorf("volume has active mounts")
}
if err := lv.unmount(); err != nil {
return err
}
|
b3b7eb27 |
realPath, err := filepath.EvalSymlinks(lv.path)
if err != nil { |
8d27417b |
if !os.IsNotExist(err) {
return err
}
realPath = filepath.Dir(lv.path) |
b3b7eb27 |
} |
8d27417b |
|
b3b7eb27 |
if !r.scopedPath(realPath) { |
8d27417b |
return fmt.Errorf("Unable to remove a directory of out the Docker root %s: %s", r.scope, realPath) |
b3b7eb27 |
} |
bd9814f0 |
|
8d27417b |
if err := removePath(realPath); err != nil { |
b3b7eb27 |
return err |
81fa9feb |
} |
b3b7eb27 |
delete(r.volumes, lv.name) |
8d27417b |
return removePath(filepath.Dir(lv.path))
}
func removePath(path string) error {
if err := os.RemoveAll(path); err != nil {
if os.IsNotExist(err) {
return nil
} |
2a5e85e2 |
return errors.Wrapf(err, "error removing volume path '%s'", path) |
8d27417b |
}
return nil |
b3b7eb27 |
}
// 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 |
81fa9feb |
}
|
2f40b1b2 |
// Scope returns the local volume scope
func (r *Root) Scope() string {
return volume.LocalScope
}
|
d6d60287 |
func (r *Root) validateName(name string) error { |
8d5a6150 |
if len(name) == 1 {
return validationError{fmt.Errorf("volume name is too short, names should be at least two alphanumeric characters")}
} |
d6d60287 |
if !volumeNameRegex.MatchString(name) { |
eee0cfa4 |
return validationError{fmt.Errorf("%q includes invalid characters for a local volume name, only %q are allowed. If you intended to pass a host directory, use absolute path", name, api.RestrictedNameChars)} |
d6d60287 |
}
return nil
}
|
9af963ab |
// localVolume implements the Volume interface from the volume package and
// represents the volumes created by Root.
type localVolume struct { |
d2312608 |
m sync.Mutex |
81fa9feb |
// 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 |
b05b2370 |
// opts is the parsed list of options used to create the volume
opts *optsConfig
// active refcounts the active mounts
active activeMount |
81fa9feb |
}
|
9af963ab |
// Name returns the name of the given Volume.
func (v *localVolume) Name() string { |
81fa9feb |
return v.name
}
|
9af963ab |
// DriverName returns the driver that created the given Volume.
func (v *localVolume) DriverName() string { |
81fa9feb |
return v.driverName
}
|
9af963ab |
// Path returns the data location.
func (v *localVolume) Path() string { |
81fa9feb |
return v.path
}
|
9af963ab |
// Mount implements the localVolume interface, returning the data location. |
db3576f8 |
// If there are any provided mount options, the resources will be mounted at this point |
2b6bc294 |
func (v *localVolume) Mount(id string) (string, error) { |
b05b2370 |
v.m.Lock()
defer v.m.Unlock()
if v.opts != nil {
if !v.active.mounted {
if err := v.mount(); err != nil {
return "", err
}
v.active.mounted = true
}
v.active.count++
} |
81fa9feb |
return v.path, nil
}
|
db3576f8 |
// Unmount dereferences the id, and if it is the last reference will unmount any resources
// that were previously mounted. |
2b6bc294 |
func (v *localVolume) Unmount(id string) error { |
b05b2370 |
v.m.Lock()
defer v.m.Unlock() |
db3576f8 |
// 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. |
b05b2370 |
if v.opts != nil {
v.active.count-- |
db3576f8 |
}
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 { |
2a5e85e2 |
return errors.Wrapf(err, "error while unmounting volume path '%s'", v.path) |
b05b2370 |
}
} |
db3576f8 |
v.active.mounted = false |
b05b2370 |
}
return nil
}
func validateOpts(opts map[string]string) error {
for opt := range opts {
if !validOpts[opt] {
return validationError{fmt.Errorf("invalid option key: %q", opt)}
}
} |
81fa9feb |
return nil
} |
36a1c56c |
func (v *localVolume) Status() map[string]interface{} {
return nil
} |
668fa8af |
// 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 ""
} |