summaryrefslogtreecommitdiff
path: root/libpod
diff options
context:
space:
mode:
Diffstat (limited to 'libpod')
-rw-r--r--libpod/boltdb_state.go378
-rw-r--r--libpod/boltdb_state_internal.go128
-rw-r--r--libpod/common_test.go5
-rw-r--r--libpod/container_inspect.go16
-rw-r--r--libpod/errors.go14
-rw-r--r--libpod/in_memory_state.go168
-rw-r--r--libpod/options.go80
-rw-r--r--libpod/runtime.go4
-rw-r--r--libpod/runtime_ctr.go27
-rw-r--r--libpod/runtime_volume.go107
-rw-r--r--libpod/runtime_volume_linux.go132
-rw-r--r--libpod/state.go23
-rw-r--r--libpod/volume.go63
-rw-r--r--libpod/volume_internal.go29
14 files changed, 1156 insertions, 18 deletions
diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go
index cb661d4e9..b154d8bda 100644
--- a/libpod/boltdb_state.go
+++ b/libpod/boltdb_state.go
@@ -94,6 +94,12 @@ func NewBoltState(path string, runtime *Runtime) (State, error) {
if _, err := tx.CreateBucketIfNotExists(allPodsBkt); err != nil {
return errors.Wrapf(err, "error creating all pods bucket")
}
+ if _, err := tx.CreateBucketIfNotExists(volBkt); err != nil {
+ return errors.Wrapf(err, "error creating volume bucket")
+ }
+ if _, err := tx.CreateBucketIfNotExists(allVolsBkt); err != nil {
+ return errors.Wrapf(err, "error creating all volumes bucket")
+ }
if _, err := tx.CreateBucketIfNotExists(runtimeConfigBkt); err != nil {
return errors.Wrapf(err, "error creating runtime-config bucket")
}
@@ -1150,6 +1156,378 @@ func (s *BoltState) PodContainers(pod *Pod) ([]*Container, error) {
return ctrs, nil
}
+// AddVolume adds the given volume to the state. It also adds ctrDepID to
+// the sub bucket holding the container dependencies that this volume has
+func (s *BoltState) AddVolume(volume *Volume) error {
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ if !volume.valid {
+ return ErrVolumeRemoved
+ }
+
+ volName := []byte(volume.Name())
+
+ volConfigJSON, err := json.Marshal(volume.config)
+ if err != nil {
+ return errors.Wrapf(err, "error marshalling volume %s config to JSON", volume.Name())
+ }
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.Update(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ allVolsBkt, err := getAllVolsBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ // Check if we already have a volume with the given name
+ volExists := allVolsBkt.Get(volName)
+ if volExists != nil {
+ return errors.Wrapf(ErrVolumeExists, "name %s is in use", volume.Name())
+ }
+
+ // We are good to add the volume
+ // Make a bucket for it
+ newVol, err := volBkt.CreateBucket(volName)
+ if err != nil {
+ return errors.Wrapf(err, "error creating bucket for volume %s", volume.Name())
+ }
+
+ // Make a subbucket for the containers using the volume. Dependent container IDs will be addedremoved to
+ // this bucket in addcontainer/removeContainer
+ if _, err := newVol.CreateBucket(volDependenciesBkt); err != nil {
+ return errors.Wrapf(err, "error creating bucket for containers using volume %s", volume.Name())
+ }
+
+ if err := newVol.Put(configKey, volConfigJSON); err != nil {
+ return errors.Wrapf(err, "error storing volume %s configuration in DB", volume.Name())
+ }
+
+ if err := allVolsBkt.Put(volName, volName); err != nil {
+ return errors.Wrapf(err, "error storing volume %s in all volumes bucket in DB", volume.Name())
+ }
+
+ return nil
+ })
+ return err
+}
+
+// RemoveVolCtrDep updates the container dependencies sub bucket of the given volume.
+// It deletes it from the bucket when found.
+// This is important when force removing a volume and we want to get rid of the dependencies.
+func (s *BoltState) RemoveVolCtrDep(volume *Volume, ctrID string) error {
+ if ctrID == "" {
+ return nil
+ }
+
+ if !s.valid {
+ return ErrDBBadConfig
+ }
+
+ if !volume.valid {
+ return ErrVolumeRemoved
+ }
+
+ volName := []byte(volume.Name())
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.Update(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ volDB := volBkt.Bucket(volName)
+ if volDB == nil {
+ volume.valid = false
+ return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in database", volume.Name())
+ }
+
+ // Make a subbucket for the containers using the volume
+ ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
+ depCtrID := []byte(ctrID)
+ if depExists := ctrDepsBkt.Get(depCtrID); depExists != nil {
+ if err := ctrDepsBkt.Delete(depCtrID); err != nil {
+ return errors.Wrapf(err, "error deleting container dependencies %q for volume %s in ctrDependencies bucket in DB", ctrID, volume.Name())
+ }
+ }
+
+ return nil
+ })
+ return err
+}
+
+// RemoveVolume removes the given volume from the state
+func (s *BoltState) RemoveVolume(volume *Volume) error {
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ if !volume.valid {
+ return ErrVolumeRemoved
+ }
+
+ volName := []byte(volume.Name())
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.Update(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ allVolsBkt, err := getAllVolsBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ // Check if the volume exists
+ volDB := volBkt.Bucket(volName)
+ if volDB == nil {
+ volume.valid = false
+ return errors.Wrapf(ErrNoSuchVolume, "volume %s does not exist in DB", volume.Name())
+ }
+
+ // Check if volume is not being used by any container
+ // This should never be nil
+ // But if it is, we can assume that no containers are using
+ // the volume.
+ volCtrsBkt := volDB.Bucket(volDependenciesBkt)
+ if volCtrsBkt != nil {
+ var deps []string
+ err = volCtrsBkt.ForEach(func(id, value []byte) error {
+ deps = append(deps, string(id))
+ return nil
+ })
+ if err != nil {
+ return errors.Wrapf(err, "error getting list of dependencies from dependencies bucket for volumes %q", volume.Name())
+ }
+ if len(deps) > 0 {
+ return errors.Wrapf(ErrVolumeBeingUsed, "volume %s is being used by container(s) %s", volume.Name(), strings.Join(deps, ","))
+ }
+ }
+
+ // volume is ready for removal
+ // Let's kick it out
+ if err := allVolsBkt.Delete(volName); err != nil {
+ return errors.Wrapf(err, "error removing volume %s from all volumes bucket in DB", volume.Name())
+ }
+ if err := volBkt.DeleteBucket(volName); err != nil {
+ return errors.Wrapf(err, "error removing volume %s from DB", volume.Name())
+ }
+
+ return nil
+ })
+ return err
+}
+
+// AllVolumes returns all volumes present in the state
+func (s *BoltState) AllVolumes() ([]*Volume, error) {
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ volumes := []*Volume{}
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return nil, err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.View(func(tx *bolt.Tx) error {
+ allVolsBucket, err := getAllVolsBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ volBucket, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+ err = allVolsBucket.ForEach(func(id, name []byte) error {
+ volExists := volBucket.Bucket(id)
+ // This check can be removed if performance becomes an
+ // issue, but much less helpful errors will be produced
+ if volExists == nil {
+ return errors.Wrapf(ErrInternal, "inconsistency in state - volume %s is in all volumes bucket but volume not found", string(id))
+ }
+
+ volume := new(Volume)
+ volume.config = new(VolumeConfig)
+
+ if err := s.getVolumeFromDB(id, volume, volBucket); err != nil {
+ if errors.Cause(err) != ErrNSMismatch {
+ logrus.Errorf("Error retrieving volume %s from the database: %v", string(id), err)
+ }
+ } else {
+ volumes = append(volumes, volume)
+ }
+
+ return nil
+ })
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return volumes, nil
+}
+
+// Volume retrieves a volume from full name
+func (s *BoltState) Volume(name string) (*Volume, error) {
+ if name == "" {
+ return nil, ErrEmptyID
+ }
+
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ volName := []byte(name)
+
+ volume := new(Volume)
+ volume.config = new(VolumeConfig)
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return nil, err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.View(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ return s.getVolumeFromDB(volName, volume, volBkt)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return volume, nil
+}
+
+// HasVolume returns true if the given volume exists in the state, otherwise it returns false
+func (s *BoltState) HasVolume(name string) (bool, error) {
+ if name == "" {
+ return false, ErrEmptyID
+ }
+
+ if !s.valid {
+ return false, ErrDBClosed
+ }
+
+ volName := []byte(name)
+
+ exists := false
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return false, err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.View(func(tx *bolt.Tx) error {
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ volDB := volBkt.Bucket(volName)
+ if volDB != nil {
+ exists = true
+ }
+
+ return nil
+ })
+ if err != nil {
+ return false, err
+ }
+
+ return exists, nil
+}
+
+// VolumeInUse checks if any container is using the volume
+// It returns a slice of the IDs of the containers using the given
+// volume. If the slice is empty, no containers use the given volume
+func (s *BoltState) VolumeInUse(volume *Volume) ([]string, error) {
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ if !volume.valid {
+ return nil, ErrVolumeRemoved
+ }
+
+ depCtrs := []string{}
+
+ db, err := s.getDBCon()
+ if err != nil {
+ return nil, err
+ }
+ defer s.closeDBCon(db)
+
+ err = db.View(func(tx *bolt.Tx) error {
+ volBucket, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
+ volDB := volBucket.Bucket([]byte(volume.Name()))
+ if volDB == nil {
+ volume.valid = false
+ return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in DB", volume.Name())
+ }
+
+ dependsBkt := volDB.Bucket(volDependenciesBkt)
+ if dependsBkt == nil {
+ return errors.Wrapf(ErrInternal, "volume %s has no dependencies bucket", volume.Name())
+ }
+
+ // Iterate through and add dependencies
+ err = dependsBkt.ForEach(func(id, value []byte) error {
+ depCtrs = append(depCtrs, string(id))
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return depCtrs, nil
+}
+
// AddPod adds the given pod to the state.
func (s *BoltState) AddPod(pod *Pod) error {
if !s.valid {
diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go
index 3f00657ea..0970f4d41 100644
--- a/libpod/boltdb_state_internal.go
+++ b/libpod/boltdb_state_internal.go
@@ -21,15 +21,18 @@ const (
allCtrsName = "all-ctrs"
podName = "pod"
allPodsName = "allPods"
+ volName = "vol"
+ allVolsName = "allVolumes"
runtimeConfigName = "runtime-config"
- configName = "config"
- stateName = "state"
- dependenciesName = "dependencies"
- netNSName = "netns"
- containersName = "containers"
- podIDName = "pod-id"
- namespaceName = "namespace"
+ configName = "config"
+ stateName = "state"
+ dependenciesName = "dependencies"
+ volCtrDependencies = "vol-dependencies"
+ netNSName = "netns"
+ containersName = "containers"
+ podIDName = "pod-id"
+ namespaceName = "namespace"
staticDirName = "static-dir"
tmpDirName = "tmp-dir"
@@ -47,15 +50,18 @@ var (
allCtrsBkt = []byte(allCtrsName)
podBkt = []byte(podName)
allPodsBkt = []byte(allPodsName)
+ volBkt = []byte(volName)
+ allVolsBkt = []byte(allVolsName)
runtimeConfigBkt = []byte(runtimeConfigName)
- configKey = []byte(configName)
- stateKey = []byte(stateName)
- dependenciesBkt = []byte(dependenciesName)
- netNSKey = []byte(netNSName)
- containersBkt = []byte(containersName)
- podIDKey = []byte(podIDName)
- namespaceKey = []byte(namespaceName)
+ configKey = []byte(configName)
+ stateKey = []byte(stateName)
+ dependenciesBkt = []byte(dependenciesName)
+ volDependenciesBkt = []byte(volCtrDependencies)
+ netNSKey = []byte(netNSName)
+ containersBkt = []byte(containersName)
+ podIDKey = []byte(podIDName)
+ namespaceKey = []byte(namespaceName)
staticDirKey = []byte(staticDirName)
tmpDirKey = []byte(tmpDirName)
@@ -234,6 +240,22 @@ func getAllPodsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
return bkt, nil
}
+func getVolBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
+ bkt := tx.Bucket(volBkt)
+ if bkt == nil {
+ return nil, errors.Wrapf(ErrDBBadConfig, "volumes bucket not found in DB")
+ }
+ return bkt, nil
+}
+
+func getAllVolsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
+ bkt := tx.Bucket(allVolsBkt)
+ if bkt == nil {
+ return nil, errors.Wrapf(ErrDBBadConfig, "all volumes bucket not found in DB")
+ }
+ return bkt, nil
+}
+
func getRuntimeConfigBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(runtimeConfigBkt)
if bkt == nil {
@@ -315,6 +337,35 @@ func (s *BoltState) getPodFromDB(id []byte, pod *Pod, podBkt *bolt.Bucket) error
return nil
}
+func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bucket) error {
+ volDB := volBkt.Bucket(name)
+ if volDB == nil {
+ return errors.Wrapf(ErrNoSuchVolume, "volume with name %s not found", string(name))
+ }
+
+ volConfigBytes := volDB.Get(configKey)
+ if volConfigBytes == nil {
+ return errors.Wrapf(ErrInternal, "volume %s is missing configuration key in DB", string(name))
+ }
+
+ if err := json.Unmarshal(volConfigBytes, volume.config); err != nil {
+ return errors.Wrapf(err, "error unmarshalling volume %s config from DB", string(name))
+ }
+
+ // Get the lock
+ lockPath := filepath.Join(s.runtime.lockDir, string(name))
+ lock, err := storage.GetLockfile(lockPath)
+ if err != nil {
+ return errors.Wrapf(err, "error retrieving lockfile for volume %s", string(name))
+ }
+ volume.lock = lock
+
+ volume.runtime = s.runtime
+ volume.valid = true
+
+ return nil
+}
+
// Add a container to the DB
// If pod is not nil, the container is added to the pod as well
func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
@@ -376,6 +427,11 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
return err
}
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
// If a pod was given, check if it exists
var podDB *bolt.Bucket
var podCtrs *bolt.Bucket
@@ -508,6 +564,25 @@ func (s *BoltState) addContainer(ctr *Container, pod *Pod) error {
}
}
+ // Add container to volume dependencies bucket if container is using a named volume
+ for _, vol := range ctr.config.Spec.Mounts {
+ if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
+ volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
+
+ volDB := volBkt.Bucket([]byte(volName))
+ if volDB == nil {
+ return errors.Wrapf(ErrNoSuchVolume, "no volume with name %s found in database", volName)
+ }
+
+ ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
+ if depExists := ctrDepsBkt.Get(ctrID); depExists == nil {
+ if err := ctrDepsBkt.Put(ctrID, ctrID); err != nil {
+ return errors.Wrapf(err, "error storing container dependencies %q for volume %s in ctrDependencies bucket in DB", ctr.ID(), volName)
+ }
+ }
+ }
+ }
+
return nil
})
return err
@@ -545,6 +620,11 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error
return err
}
+ volBkt, err := getVolBucket(tx)
+ if err != nil {
+ return err
+ }
+
// Does the pod exist?
var podDB *bolt.Bucket
if pod != nil {
@@ -663,5 +743,25 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error
}
}
+ // Remove container from volume dependencies bucket if container is using a named volume
+ for _, vol := range ctr.config.Spec.Mounts {
+ if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
+ volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
+
+ volDB := volBkt.Bucket([]byte(volName))
+ if volDB == nil {
+ // Let's assume the volume was already deleted and continue to remove the container
+ continue
+ }
+
+ ctrDepsBkt := volDB.Bucket(volDependenciesBkt)
+ if depExists := ctrDepsBkt.Get(ctrID); depExists != nil {
+ if err := ctrDepsBkt.Delete(ctrID); err != nil {
+ return errors.Wrapf(err, "error deleting container dependencies %q for volume %s in ctrDependencies bucket in DB", ctr.ID(), volName)
+ }
+ }
+ }
+ }
+
return nil
}
diff --git a/libpod/common_test.go b/libpod/common_test.go
index b7fee2764..81c8f1920 100644
--- a/libpod/common_test.go
+++ b/libpod/common_test.go
@@ -74,6 +74,11 @@ func getTestContainer(id, name, locksDir string) (*Container, error) {
"/test/file.test": "/test2/file2.test",
},
},
+ runtime: &Runtime{
+ config: &RuntimeConfig{
+ VolumePath: "/does/not/exist/tmp/volumes",
+ },
+ },
valid: true,
}
diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go
index 9b07198bc..06a0c9f32 100644
--- a/libpod/container_inspect.go
+++ b/libpod/container_inspect.go
@@ -1,8 +1,11 @@
package libpod
import (
+ "strings"
+
"github.com/containers/libpod/pkg/inspect"
"github.com/cri-o/ocicni/pkg/ocicni"
+ specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
@@ -48,6 +51,17 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
hostnamePath = getPath
}
+ var mounts []specs.Mount
+ for i, mnt := range spec.Mounts {
+ mounts = append(mounts, mnt)
+ // We only want to show the name of the named volume in the inspect
+ // output, so split the path and get the name out of it.
+ if strings.Contains(mnt.Source, c.runtime.config.VolumePath) {
+ split := strings.Split(mnt.Source[len(c.runtime.config.VolumePath)+1:], "/")
+ mounts[i].Source = split[0]
+ }
+ }
+
data := &inspect.ContainerInspectData{
ID: config.ID,
Created: config.CreatedTime,
@@ -85,7 +99,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data)
AppArmorProfile: spec.Process.ApparmorProfile,
ExecIDs: execIDs,
GraphDriver: driverData,
- Mounts: spec.Mounts,
+ Mounts: mounts,
Dependencies: c.Dependencies(),
NetworkSettings: &inspect.NetworkSettings{
Bridge: "", // TODO
diff --git a/libpod/errors.go b/libpod/errors.go
index 75b4928da..d6614141c 100644
--- a/libpod/errors.go
+++ b/libpod/errors.go
@@ -11,18 +11,24 @@ var (
ErrNoSuchPod = errors.New("no such pod")
// ErrNoSuchImage indicates the requested image does not exist
ErrNoSuchImage = errors.New("no such image")
+ // ErrNoSuchVolume indicates the requested volume does not exist
+ ErrNoSuchVolume = errors.New("no such volume")
// ErrCtrExists indicates a container with the same name or ID already
// exists
ErrCtrExists = errors.New("container already exists")
// ErrPodExists indicates a pod with the same name or ID already exists
ErrPodExists = errors.New("pod already exists")
- // ErrImageExists indicated an image with the same ID already exists
+ // ErrImageExists indicates an image with the same ID already exists
ErrImageExists = errors.New("image already exists")
+ // ErrVolumeExists indicates a volume with the same name already exists
+ ErrVolumeExists = errors.New("volume already exists")
// ErrCtrStateInvalid indicates a container is in an improper state for
// the requested operation
ErrCtrStateInvalid = errors.New("container state improper")
+ // ErrVolumeBeingUsed indicates that a volume is being used by at least one container
+ ErrVolumeBeingUsed = errors.New("volume is being used")
// ErrRuntimeFinalized indicates that the runtime has already been
// created and cannot be modified
@@ -33,6 +39,9 @@ var (
// ErrPodFinalized indicates that the pod has already been created and
// cannot be modified
ErrPodFinalized = errors.New("pod has been finalized")
+ // ErrVolumeFinalized indicates that the volume has already been created and
+ // cannot be modified
+ ErrVolumeFinalized = errors.New("volume has been finalized")
// ErrInvalidArg indicates that an invalid argument was passed
ErrInvalidArg = errors.New("invalid argument")
@@ -55,6 +64,9 @@ var (
// ErrPodRemoved indicates that the pod has already been removed and no
// further operations can be performed on it
ErrPodRemoved = errors.New("pod has already been removed")
+ // ErrVolumeRemoved indicates that the volume has already been removed and
+ // no further operations can be performed on it
+ ErrVolumeRemoved = errors.New("volume has already been removed")
// ErrDBClosed indicates that the connection to the state database has
// already been closed
diff --git a/libpod/in_memory_state.go b/libpod/in_memory_state.go
index 77eba0cc6..314799309 100644
--- a/libpod/in_memory_state.go
+++ b/libpod/in_memory_state.go
@@ -18,8 +18,10 @@ type InMemoryState struct {
pods map[string]*Pod
// Maps container ID to container struct.
containers map[string]*Container
+ volumes map[string]*Volume
// Maps container ID to a list of IDs of dependencies.
- ctrDepends map[string][]string
+ ctrDepends map[string][]string
+ volumeDepends map[string][]string
// Maps pod ID to a map of container ID to container struct.
podContainers map[string]map[string]*Container
// Global name registry - ensures name uniqueness and performs lookups.
@@ -46,8 +48,10 @@ func NewInMemoryState() (State, error) {
state.pods = make(map[string]*Pod)
state.containers = make(map[string]*Container)
+ state.volumes = make(map[string]*Volume)
state.ctrDepends = make(map[string][]string)
+ state.volumeDepends = make(map[string][]string)
state.podContainers = make(map[string]map[string]*Container)
@@ -244,6 +248,14 @@ func (s *InMemoryState) AddContainer(ctr *Container) error {
s.addCtrToDependsMap(ctr.ID(), depCtr)
}
+ // Add container to volume dependencies
+ for _, vol := range ctr.config.Spec.Mounts {
+ if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
+ volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
+ s.addCtrToVolDependsMap(ctr.ID(), volName)
+ }
+ }
+
return nil
}
@@ -294,6 +306,14 @@ func (s *InMemoryState) RemoveContainer(ctr *Container) error {
s.removeCtrFromDependsMap(ctr.ID(), depCtr)
}
+ // Remove container from volume dependencies
+ for _, vol := range ctr.config.Spec.Mounts {
+ if strings.Contains(vol.Source, ctr.runtime.config.VolumePath) {
+ volName := strings.Split(vol.Source[len(ctr.runtime.config.VolumePath)+1:], "/")[0]
+ s.removeCtrFromVolDependsMap(ctr.ID(), volName)
+ }
+ }
+
return nil
}
@@ -358,6 +378,114 @@ func (s *InMemoryState) ContainerInUse(ctr *Container) ([]string, error) {
return arr, nil
}
+// Volume retrieves a volume from its full name
+func (s *InMemoryState) Volume(name string) (*Volume, error) {
+ if name == "" {
+ return nil, ErrEmptyID
+ }
+
+ vol, ok := s.volumes[name]
+ if !ok {
+ return nil, errors.Wrapf(ErrNoSuchCtr, "no volume with name %s found", name)
+ }
+
+ return vol, nil
+}
+
+// HasVolume checks if a volume with the given name is present in the state
+func (s *InMemoryState) HasVolume(name string) (bool, error) {
+ if name == "" {
+ return false, ErrEmptyID
+ }
+
+ _, ok := s.volumes[name]
+ if !ok {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+// AddVolume adds a volume to the state
+func (s *InMemoryState) AddVolume(volume *Volume) error {
+ if !volume.valid {
+ return errors.Wrapf(ErrVolumeRemoved, "volume with name %s is not valid", volume.Name())
+ }
+
+ if _, ok := s.volumes[volume.Name()]; ok {
+ return errors.Wrapf(ErrVolumeExists, "volume with name %s already exists in state", volume.Name())
+ }
+
+ s.volumes[volume.Name()] = volume
+
+ return nil
+}
+
+// RemoveVolume removes a volume from the state
+func (s *InMemoryState) RemoveVolume(volume *Volume) error {
+ // Ensure we don't remove a volume which containers depend on
+ deps, ok := s.volumeDepends[volume.Name()]
+ if ok && len(deps) != 0 {
+ depsStr := strings.Join(deps, ", ")
+ return errors.Wrapf(ErrVolumeExists, "the following containers depend on volume %s: %s", volume.Name(), depsStr)
+ }
+
+ if _, ok := s.volumes[volume.Name()]; !ok {
+ volume.valid = false
+ return errors.Wrapf(ErrVolumeRemoved, "no volume exists in state with name %s", volume.Name())
+ }
+
+ delete(s.volumes, volume.Name())
+
+ return nil
+}
+
+// RemoveVolCtrDep updates the container dependencies of the volume
+func (s *InMemoryState) RemoveVolCtrDep(volume *Volume, ctrID string) error {
+ if !volume.valid {
+ return errors.Wrapf(ErrVolumeRemoved, "volume with name %s is not valid", volume.Name())
+ }
+
+ if _, ok := s.volumes[volume.Name()]; !ok {
+ return errors.Wrapf(ErrNoSuchVolume, "volume with name %s doesn't exists in state", volume.Name())
+ }
+
+ // Remove container that is using this volume
+ s.removeCtrFromVolDependsMap(ctrID, volume.Name())
+
+ return nil
+}
+
+// VolumeInUse checks if the given volume is being used by at least one container
+func (s *InMemoryState) VolumeInUse(volume *Volume) ([]string, error) {
+ if !volume.valid {
+ return nil, ErrVolumeRemoved
+ }
+
+ // If the volume does not exist, return error
+ if _, ok := s.volumes[volume.Name()]; !ok {
+ volume.valid = false
+ return nil, errors.Wrapf(ErrNoSuchVolume, "volume with name %s not found in state", volume.Name())
+ }
+
+ arr, ok := s.volumeDepends[volume.Name()]
+ if !ok {
+ return []string{}, nil
+ }
+
+ return arr, nil
+}
+
+// AllVolumes returns all volumes that exist in the state
+func (s *InMemoryState) AllVolumes() ([]*Volume, error) {
+ allVols := make([]*Volume, 0, len(s.volumes))
+ for _, v := range s.volumes {
+ allVols = append(allVols, v)
+ }
+
+ return allVols, nil
+}
+
// AllContainers retrieves all containers from the state
func (s *InMemoryState) AllContainers() ([]*Container, error) {
ctrs := make([]*Container, 0, len(s.containers))
@@ -945,6 +1073,44 @@ func (s *InMemoryState) removeCtrFromDependsMap(ctrID, dependsID string) {
}
}
+// Add a container to the dependency mappings for the volume
+func (s *InMemoryState) addCtrToVolDependsMap(depCtrID, volName string) {
+ if volName != "" {
+ arr, ok := s.volumeDepends[volName]
+ if !ok {
+ // Do not have a mapping for that volume yet
+ s.volumeDepends[volName] = []string{depCtrID}
+ } else {
+ // Have a mapping for the volume
+ arr = append(arr, depCtrID)
+ s.volumeDepends[volName] = arr
+ }
+ }
+}
+
+// Remove a container from the dependency mappings for the volume
+func (s *InMemoryState) removeCtrFromVolDependsMap(depCtrID, volName string) {
+ if volName != "" {
+ arr, ok := s.volumeDepends[volName]
+ if !ok {
+ // Internal state seems inconsistent
+ // But the dependency is definitely gone
+ // So just return
+ return
+ }
+
+ newArr := make([]string, 0, len(arr))
+
+ for _, id := range arr {
+ if id != depCtrID {
+ newArr = append(newArr, id)
+ }
+ }
+
+ s.volumeDepends[volName] = newArr
+ }
+}
+
// Check if we can access a pod or container, or if that is blocked by
// namespaces.
func (s *InMemoryState) checkNSMatch(id, ns string) error {
diff --git a/libpod/options.go b/libpod/options.go
index 3e43d73f0..352e6a506 100644
--- a/libpod/options.go
+++ b/libpod/options.go
@@ -327,6 +327,22 @@ func WithNamespace(ns string) RuntimeOption {
}
}
+// WithVolumePath sets the path under which all named volumes
+// should be created.
+// The path changes based on whethe rthe user is running as root
+// or not.
+func WithVolumePath(volPath string) RuntimeOption {
+ return func(rt *Runtime) error {
+ if rt.valid {
+ return ErrRuntimeFinalized
+ }
+
+ rt.config.VolumePath = volPath
+
+ return nil
+ }
+}
+
// WithDefaultInfraImage sets the infra image for libpod.
// An infra image is used for inter-container kernel
// namespace sharing within a pod. Typically, an infra
@@ -1125,6 +1141,70 @@ func withIsInfra() CtrCreateOption {
}
}
+// Volume Creation Options
+
+// WithVolumeName sets the name of the volume.
+func WithVolumeName(name string) VolumeCreateOption {
+ return func(volume *Volume) error {
+ if volume.valid {
+ return ErrVolumeFinalized
+ }
+
+ // Check the name against a regex
+ if !nameRegex.MatchString(name) {
+ return errors.Wrapf(ErrInvalidArg, "name must match regex [a-zA-Z0-9_-]+")
+ }
+ volume.config.Name = name
+
+ return nil
+ }
+}
+
+// WithVolumeLabels sets the labels of the volume.
+func WithVolumeLabels(labels map[string]string) VolumeCreateOption {
+ return func(volume *Volume) error {
+ if volume.valid {
+ return ErrVolumeFinalized
+ }
+
+ volume.config.Labels = make(map[string]string)
+ for key, value := range labels {
+ volume.config.Labels[key] = value
+ }
+
+ return nil
+ }
+}
+
+// WithVolumeDriver sets the driver of the volume.
+func WithVolumeDriver(driver string) VolumeCreateOption {
+ return func(volume *Volume) error {
+ if volume.valid {
+ return ErrVolumeFinalized
+ }
+
+ volume.config.Driver = driver
+
+ return nil
+ }
+}
+
+// WithVolumeOptions sets the options of the volume.
+func WithVolumeOptions(options map[string]string) VolumeCreateOption {
+ return func(volume *Volume) error {
+ if volume.valid {
+ return ErrVolumeFinalized
+ }
+
+ volume.config.Options = make(map[string]string)
+ for key, value := range options {
+ volume.config.Options[key] = value
+ }
+
+ return nil
+ }
+}
+
// Pod Creation Options
// WithPodName sets the name of the pod.
diff --git a/libpod/runtime.go b/libpod/runtime.go
index 6b6a8cc2d..82473aae9 100644
--- a/libpod/runtime.go
+++ b/libpod/runtime.go
@@ -92,6 +92,7 @@ type RuntimeConfig struct {
// Not included in on-disk config, use the dedicated containers/storage
// configuration file instead
StorageConfig storage.StoreOptions `toml:"-"`
+ VolumePath string `toml:"volume_path"`
// ImageDefaultTransport is the default transport method used to fetch
// images
ImageDefaultTransport string `toml:"image_default_transport"`
@@ -278,12 +279,13 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
if rootless.IsRootless() {
// If we're rootless, override the default storage config
- storageConf, err := util.GetDefaultStoreOptions()
+ storageConf, volumePath, err := util.GetDefaultStoreOptions()
if err != nil {
return nil, errors.Wrapf(err, "error retrieving rootless storage config")
}
runtime.config.StorageConfig = storageConf
runtime.config.StaticDir = filepath.Join(storageConf.GraphRoot, "libpod")
+ runtime.config.VolumePath = volumePath
}
configPath := ConfigPath
diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go
index 09d0ec042..ba8eaacbe 100644
--- a/libpod/runtime_ctr.go
+++ b/libpod/runtime_ctr.go
@@ -154,6 +154,24 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
}
}()
+ // Go through the volume mounts and check for named volumes
+ // If the named volme already exists continue, otherwise create
+ // the storage for the named volume.
+ for i, vol := range ctr.config.Spec.Mounts {
+ if vol.Source[0] != '/' && isNamedVolume(vol.Source) {
+ volInfo, err := r.state.Volume(vol.Source)
+ if err != nil {
+ newVol, err := r.newVolume(ctx, WithVolumeName(vol.Source))
+ if err != nil {
+ logrus.Errorf("error creating named volume %q: %v", vol.Source, err)
+ }
+ ctr.config.Spec.Mounts[i].Source = newVol.MountPoint()
+ continue
+ }
+ ctr.config.Spec.Mounts[i].Source = volInfo.MountPoint()
+ }
+ }
+
if ctr.config.LogPath == "" {
ctr.config.LogPath = filepath.Join(ctr.config.StaticDir, "ctr.log")
}
@@ -170,6 +188,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
}
ctr.config.Mounts = append(ctr.config.Mounts, ctr.config.ShmDir)
}
+
// Add the container to the state
// TODO: May be worth looking into recovering from name/ID collisions here
if ctr.config.Pod != "" {
@@ -474,3 +493,11 @@ func (r *Runtime) GetLatestContainer() (*Container, error) {
}
return ctrs[lastCreatedIndex], nil
}
+
+// Check if volName is a named volume and not one of the default mounts we add to containers
+func isNamedVolume(volName string) bool {
+ if volName != "proc" && volName != "tmpfs" && volName != "devpts" && volName != "shm" && volName != "mqueue" && volName != "sysfs" && volName != "cgroup" {
+ return true
+ }
+ return false
+}
diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go
new file mode 100644
index 000000000..3921758ee
--- /dev/null
+++ b/libpod/runtime_volume.go
@@ -0,0 +1,107 @@
+package libpod
+
+import (
+ "context"
+)
+
+// Contains the public Runtime API for volumes
+
+// A VolumeCreateOption is a functional option which alters the Volume created by
+// NewVolume
+type VolumeCreateOption func(*Volume) error
+
+// VolumeFilter is a function to determine whether a volume is included in command
+// output. Volumes to be outputted are tested using the function. a true return will
+// include the volume, a false return will exclude it.
+type VolumeFilter func(*Volume) bool
+
+// RemoveVolume removes a volumes
+func (r *Runtime) RemoveVolume(ctx context.Context, v *Volume, force, prune bool) error {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ if !r.valid {
+ return ErrRuntimeStopped
+ }
+
+ if !v.valid {
+ if ok, _ := r.state.HasVolume(v.Name()); !ok {
+ // Volume probably already removed
+ // Or was never in the runtime to begin with
+ return nil
+ }
+ }
+
+ v.lock.Lock()
+ defer v.lock.Unlock()
+
+ return r.removeVolume(ctx, v, force, prune)
+}
+
+// GetVolume retrieves a volume by its name
+func (r *Runtime) GetVolume(name string) (*Volume, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if !r.valid {
+ return nil, ErrRuntimeStopped
+ }
+
+ return r.state.Volume(name)
+}
+
+// HasVolume checks to see if a volume with the given name exists
+func (r *Runtime) HasVolume(name string) (bool, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if !r.valid {
+ return false, ErrRuntimeStopped
+ }
+
+ return r.state.HasVolume(name)
+}
+
+// Volumes retrieves all volumes
+// Filters can be provided which will determine which volumes are included in the
+// output. Multiple filters are handled by ANDing their output, so only volumes
+// matching all filters are returned
+func (r *Runtime) Volumes(filters ...VolumeFilter) ([]*Volume, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if !r.valid {
+ return nil, ErrRuntimeStopped
+ }
+
+ vols, err := r.state.AllVolumes()
+ if err != nil {
+ return nil, err
+ }
+
+ volsFiltered := make([]*Volume, 0, len(vols))
+ for _, vol := range vols {
+ include := true
+ for _, filter := range filters {
+ include = include && filter(vol)
+ }
+
+ if include {
+ volsFiltered = append(volsFiltered, vol)
+ }
+ }
+
+ return volsFiltered, nil
+}
+
+// GetAllVolumes retrieves all the volumes
+func (r *Runtime) GetAllVolumes() ([]*Volume, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if !r.valid {
+ return nil, ErrRuntimeStopped
+ }
+
+ return r.state.AllVolumes()
+}
diff --git a/libpod/runtime_volume_linux.go b/libpod/runtime_volume_linux.go
new file mode 100644
index 000000000..5cc0938f0
--- /dev/null
+++ b/libpod/runtime_volume_linux.go
@@ -0,0 +1,132 @@
+// +build linux
+
+package libpod
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/containers/storage"
+ "github.com/containers/storage/pkg/stringid"
+ "github.com/opencontainers/selinux/go-selinux/label"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+// NewVolume creates a new empty volume
+func (r *Runtime) NewVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ if !r.valid {
+ return nil, ErrRuntimeStopped
+ }
+ return r.newVolume(ctx, options...)
+}
+
+// newVolume creates a new empty volume
+func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) {
+ volume, err := newVolume(r)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error creating volume")
+ }
+
+ for _, option := range options {
+ if err := option(volume); err != nil {
+ return nil, errors.Wrapf(err, "error running volume create option")
+ }
+ }
+
+ if volume.config.Name == "" {
+ volume.config.Name = stringid.GenerateNonCryptoID()
+ }
+ // TODO: support for other volume drivers
+ if volume.config.Driver == "" {
+ volume.config.Driver = "local"
+ }
+ // TODO: determine when the scope is global and set it to that
+ if volume.config.Scope == "" {
+ volume.config.Scope = "local"
+ }
+
+ // Create the mountpoint of this volume
+ fullVolPath := filepath.Join(r.config.VolumePath, volume.config.Name, "_data")
+ if err := os.MkdirAll(fullVolPath, 0755); err != nil {
+ return nil, errors.Wrapf(err, "error creating volume directory %q", fullVolPath)
+ }
+ _, mountLabel, err := label.InitLabels([]string{})
+ if err != nil {
+ return nil, errors.Wrapf(err, "error getting default mountlabels")
+ }
+ if err := label.ReleaseLabel(mountLabel); err != nil {
+ return nil, errors.Wrapf(err, "error releasing label %q", mountLabel)
+ }
+ if err := label.Relabel(fullVolPath, mountLabel, true); err != nil {
+ return nil, errors.Wrapf(err, "error setting selinux label to %q", fullVolPath)
+ }
+ volume.config.MountPoint = fullVolPath
+
+ // Path our lock file will reside at
+ lockPath := filepath.Join(r.lockDir, volume.config.Name)
+ // Grab a lockfile at the given path
+ lock, err := storage.GetLockfile(lockPath)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error creating lockfile for new volume")
+ }
+ volume.lock = lock
+
+ volume.valid = true
+
+ // Add the volume to state
+ if err := r.state.AddVolume(volume); err != nil {
+ return nil, errors.Wrapf(err, "error adding volume to state")
+ }
+
+ return volume, nil
+}
+
+// removeVolume removes the specified volume from state as well tears down its mountpoint and storage
+func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force, prune bool) error {
+ if !v.valid {
+ return ErrNoSuchVolume
+ }
+
+ deps, err := r.state.VolumeInUse(v)
+ if err != nil {
+ return err
+ }
+ if len(deps) != 0 {
+ if prune {
+ return ErrVolumeBeingUsed
+ }
+ depsStr := strings.Join(deps, ", ")
+ if !force {
+ return errors.Wrapf(ErrVolumeBeingUsed, "volume %s is being used by the following container(s): %s", v.Name(), depsStr)
+ }
+ // If using force, log the warning that the volume is being used by at least one container
+ logrus.Warnf("volume %s is being used by the following container(s): %s", v.Name(), depsStr)
+ // Remove the container dependencies so we can go ahead and delete the volume
+ for _, dep := range deps {
+ if err := r.state.RemoveVolCtrDep(v, dep); err != nil {
+ return errors.Wrapf(err, "unable to remove container dependency %q from volume %q while trying to delete volume by force", dep, v.Name())
+ }
+ }
+ }
+
+ // Delete the mountpoint path of the volume, that is delete the volume from /var/lib/containers/storage/volumes
+ if err := v.teardownStorage(); err != nil {
+ return errors.Wrapf(err, "error cleaning up volume storage for %q", v.Name())
+ }
+
+ // Remove the volume from the state
+ if err := r.state.RemoveVolume(v); err != nil {
+ return errors.Wrapf(err, "error removing volume %s", v.Name())
+ }
+
+ // Set volume as invalid so it can no longer be used
+ v.valid = false
+
+ return nil
+}
diff --git a/libpod/state.go b/libpod/state.go
index 06c2003d8..88d89f673 100644
--- a/libpod/state.go
+++ b/libpod/state.go
@@ -153,4 +153,27 @@ type State interface {
// If a namespace has been set, only pods in that namespace will be
// returned.
AllPods() ([]*Pod, error)
+
+ // Volume accepts full name of volume
+ // If the volume doesn't exist, an error will be returned
+ Volume(volName string) (*Volume, error)
+ // HasVolume returns true if volName exists in the state,
+ // otherwise it returns false
+ HasVolume(volName string) (bool, error)
+ // VolumeInUse goes through the container dependencies of a volume
+ // and checks if the volume is being used by any container. If it is
+ // a slice of container IDs using the volume is returned
+ VolumeInUse(volume *Volume) ([]string, error)
+ // AddVolume adds the specified volume to state. The volume's name
+ // must be unique within the list of existing volumes
+ AddVolume(volume *Volume) error
+ // RemoveVolCtrDep updates the list of container dependencies that the
+ // volume has. It either deletes the dependent container ID from
+ // the sub-bucket
+ RemoveVolCtrDep(volume *Volume, ctrID string) error
+ // RemoveVolume removes the specified volume.
+ // Only volumes that have no container dependencies can be removed
+ RemoveVolume(volume *Volume) error
+ // AllVolumes returns all the volumes available in the state
+ AllVolumes() ([]*Volume, error)
}
diff --git a/libpod/volume.go b/libpod/volume.go
new file mode 100644
index 000000000..b732e8aa7
--- /dev/null
+++ b/libpod/volume.go
@@ -0,0 +1,63 @@
+package libpod
+
+import "github.com/containers/storage"
+
+// Volume is the type used to create named volumes
+// TODO: all volumes should be created using this and the Volume API
+type Volume struct {
+ config *VolumeConfig
+
+ valid bool
+ runtime *Runtime
+ lock storage.Locker
+}
+
+// VolumeConfig holds the volume's config information
+//easyjson:json
+type VolumeConfig struct {
+ Name string `json:"name"`
+ Labels map[string]string `json:"labels"`
+ MountPoint string `json:"mountPoint"`
+ Driver string `json:"driver"`
+ Options map[string]string `json:"options"`
+ Scope string `json:"scope"`
+}
+
+// Name retrieves the volume's name
+func (v *Volume) Name() string {
+ return v.config.Name
+}
+
+// Labels returns the volume's labels
+func (v *Volume) Labels() map[string]string {
+ labels := make(map[string]string)
+ for key, value := range v.config.Labels {
+ labels[key] = value
+ }
+ return labels
+}
+
+// MountPoint returns the volume's mountpoint on the host
+func (v *Volume) MountPoint() string {
+ return v.config.MountPoint
+}
+
+// Driver returns the volume's driver
+func (v *Volume) Driver() string {
+ return v.config.Driver
+}
+
+// Options return the volume's options
+func (v *Volume) Options() map[string]string {
+ options := make(map[string]string)
+ for key, value := range v.config.Options {
+ options[key] = value
+ }
+
+ return options
+}
+
+// Scope returns the scope of the volume
+func (v *Volume) Scope() string {
+ return v.config.Scope
+}
diff --git a/libpod/volume_internal.go b/libpod/volume_internal.go
new file mode 100644
index 000000000..800e6d106
--- /dev/null
+++ b/libpod/volume_internal.go
@@ -0,0 +1,29 @@
+package libpod
+
+import (
+ "os"
+ "path/filepath"
+)
+
+// VolumePath is the path under which all volumes that are created using the
+// local driver will be created
+// const VolumePath = "/var/lib/containers/storage/volumes"
+
+// Creates a new volume
+func newVolume(runtime *Runtime) (*Volume, error) {
+ volume := new(Volume)
+ volume.config = new(VolumeConfig)
+ volume.runtime = runtime
+ volume.config.Labels = make(map[string]string)
+ volume.config.Options = make(map[string]string)
+
+ return volume, nil
+}
+
+// teardownStorage deletes the volume from volumePath
+func (v *Volume) teardownStorage() error {
+ if !v.valid {
+ return ErrNoSuchVolume
+ }
+ return os.RemoveAll(filepath.Join(v.runtime.config.VolumePath, v.Name()))
+}