Merge pull request #3931 from mheon/volumes_with_options
Add support for mounting volumes with local driver and options
This commit is contained in:
commit
0d8a22496e
|
|
@ -39,6 +39,8 @@ $ podman volume create myvol
|
||||||
$ podman volume create
|
$ podman volume create
|
||||||
|
|
||||||
$ podman volume create --label foo=bar myvol
|
$ podman volume create --label foo=bar myvol
|
||||||
|
|
||||||
|
$ podman volume create --opt device=tmpfs --opt type=tmpfs --opt o=nodev,noexec myvol
|
||||||
```
|
```
|
||||||
|
|
||||||
## SEE ALSO
|
## SEE ALSO
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,16 @@ func (s *BoltState) Refresh() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allVolsBucket, err := getAllVolsBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
volBucket, err := getVolBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Iterate through all IDs. Check if they are containers.
|
// Iterate through all IDs. Check if they are containers.
|
||||||
// If they are, unmarshal their state, and then clear
|
// If they are, unmarshal their state, and then clear
|
||||||
// PID, mountpoint, and state for all of them
|
// PID, mountpoint, and state for all of them
|
||||||
|
|
@ -235,6 +245,44 @@ func (s *BoltState) Refresh() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now refresh volumes
|
||||||
|
err = allVolsBucket.ForEach(func(id, name []byte) error {
|
||||||
|
dbVol := volBucket.Bucket(id)
|
||||||
|
if dbVol == nil {
|
||||||
|
return errors.Wrapf(define.ErrInternal, "inconsistency in state - volume %s is in all volumes bucket but volume not found", string(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the state
|
||||||
|
volStateBytes := dbVol.Get(stateKey)
|
||||||
|
if volStateBytes == nil {
|
||||||
|
// If the volume doesn't have a state, nothing to do
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
oldState := new(VolumeState)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(volStateBytes, oldState); err != nil {
|
||||||
|
return errors.Wrapf(err, "error unmarshalling state for volume %s", string(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mount count to 0
|
||||||
|
oldState.MountCount = 0
|
||||||
|
|
||||||
|
newState, err := json.Marshal(oldState)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error marshalling state for volume %s", string(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbVol.Put(stateKey, newState); err != nil {
|
||||||
|
return errors.Wrapf(err, "error storing new state for volume %s", string(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
|
|
@ -1352,6 +1400,15 @@ func (s *BoltState) AddVolume(volume *Volume) error {
|
||||||
return errors.Wrapf(err, "error marshalling volume %s config to JSON", volume.Name())
|
return errors.Wrapf(err, "error marshalling volume %s config to JSON", volume.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Volume state is allowed to not exist
|
||||||
|
var volStateJSON []byte
|
||||||
|
if volume.state != nil {
|
||||||
|
volStateJSON, err = json.Marshal(volume.state)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error marshalling volume %s state to JSON", volume.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
db, err := s.getDBCon()
|
db, err := s.getDBCon()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -1392,6 +1449,12 @@ func (s *BoltState) AddVolume(volume *Volume) error {
|
||||||
return errors.Wrapf(err, "error storing volume %s configuration in DB", volume.Name())
|
return errors.Wrapf(err, "error storing volume %s configuration in DB", volume.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if volStateJSON != nil {
|
||||||
|
if err := newVol.Put(stateKey, volStateJSON); err != nil {
|
||||||
|
return errors.Wrapf(err, "error storing volume %s state in DB", volume.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := allVolsBkt.Put(volName, volName); err != nil {
|
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 errors.Wrapf(err, "error storing volume %s in all volumes bucket in DB", volume.Name())
|
||||||
}
|
}
|
||||||
|
|
@ -1483,6 +1546,103 @@ func (s *BoltState) RemoveVolume(volume *Volume) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateVolume updates the volume's state from the database.
|
||||||
|
func (s *BoltState) UpdateVolume(volume *Volume) error {
|
||||||
|
if !s.valid {
|
||||||
|
return define.ErrDBClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !volume.valid {
|
||||||
|
return define.ErrVolumeRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := new(VolumeState)
|
||||||
|
volumeName := []byte(volume.Name())
|
||||||
|
|
||||||
|
db, err := s.getDBCon()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.deferredCloseDBCon(db)
|
||||||
|
|
||||||
|
err = db.View(func(tx *bolt.Tx) error {
|
||||||
|
volBucket, err := getVolBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
volToUpdate := volBucket.Bucket(volumeName)
|
||||||
|
if volToUpdate == nil {
|
||||||
|
volume.valid = false
|
||||||
|
return errors.Wrapf(define.ErrNoSuchVolume, "no volume with name %s found in database", volume.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
stateBytes := volToUpdate.Get(stateKey)
|
||||||
|
if stateBytes == nil {
|
||||||
|
// Having no state is valid.
|
||||||
|
// Return nil, use the empty state.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(stateBytes, newState); err != nil {
|
||||||
|
return errors.Wrapf(err, "error unmarshalling volume %s state", volume.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
volume.state = newState
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveVolume saves the volume's state to the database.
|
||||||
|
func (s *BoltState) SaveVolume(volume *Volume) error {
|
||||||
|
if !s.valid {
|
||||||
|
return define.ErrDBClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !volume.valid {
|
||||||
|
return define.ErrVolumeRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeName := []byte(volume.Name())
|
||||||
|
|
||||||
|
var newStateJSON []byte
|
||||||
|
if volume.state != nil {
|
||||||
|
stateJSON, err := json.Marshal(volume.state)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error marshalling volume %s state to JSON", volume.Name())
|
||||||
|
}
|
||||||
|
newStateJSON = stateJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := s.getDBCon()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.deferredCloseDBCon(db)
|
||||||
|
|
||||||
|
err = db.Update(func(tx *bolt.Tx) error {
|
||||||
|
volBucket, err := getVolBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
volToUpdate := volBucket.Bucket(volumeName)
|
||||||
|
if volToUpdate == nil {
|
||||||
|
volume.valid = false
|
||||||
|
return errors.Wrapf(define.ErrNoSuchVolume, "no volume with name %s found in database", volume.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return volToUpdate.Put(stateKey, newStateJSON)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// AllVolumes returns all volumes present in the state
|
// AllVolumes returns all volumes present in the state
|
||||||
func (s *BoltState) AllVolumes() ([]*Volume, error) {
|
func (s *BoltState) AllVolumes() ([]*Volume, error) {
|
||||||
if !s.valid {
|
if !s.valid {
|
||||||
|
|
@ -1517,6 +1677,7 @@ func (s *BoltState) AllVolumes() ([]*Volume, error) {
|
||||||
|
|
||||||
volume := new(Volume)
|
volume := new(Volume)
|
||||||
volume.config = new(VolumeConfig)
|
volume.config = new(VolumeConfig)
|
||||||
|
volume.state = new(VolumeState)
|
||||||
|
|
||||||
if err := s.getVolumeFromDB(id, volume, volBucket); err != nil {
|
if err := s.getVolumeFromDB(id, volume, volBucket); err != nil {
|
||||||
if errors.Cause(err) != define.ErrNSMismatch {
|
if errors.Cause(err) != define.ErrNSMismatch {
|
||||||
|
|
@ -1551,6 +1712,7 @@ func (s *BoltState) Volume(name string) (*Volume, error) {
|
||||||
|
|
||||||
volume := new(Volume)
|
volume := new(Volume)
|
||||||
volume.config = new(VolumeConfig)
|
volume.config = new(VolumeConfig)
|
||||||
|
volume.state = new(VolumeState)
|
||||||
|
|
||||||
db, err := s.getDBCon()
|
db, err := s.getDBCon()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,14 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu
|
||||||
return errors.Wrapf(err, "error unmarshalling volume %s config from DB", string(name))
|
return errors.Wrapf(err, "error unmarshalling volume %s config from DB", string(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Volume state is allowed to be nil for legacy compatibility
|
||||||
|
volStateBytes := volDB.Get(stateKey)
|
||||||
|
if volStateBytes != nil {
|
||||||
|
if err := json.Unmarshal(volStateBytes, volume.state); err != nil {
|
||||||
|
return errors.Wrapf(err, "error unmarshalling volume %s state from DB", string(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the lock
|
// Get the lock
|
||||||
lock, err := s.runtime.lockManager.RetrieveLock(volume.config.LockID)
|
lock, err := s.runtime.lockManager.RetrieveLock(volume.config.LockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1205,7 +1205,7 @@ func (c *Container) restartWithTimeout(ctx context.Context, timeout uint) (err e
|
||||||
// TODO: Add ability to override mount label so we can use this for Mount() too
|
// TODO: Add ability to override mount label so we can use this for Mount() too
|
||||||
// TODO: Can we use this for export? Copying SHM into the export might not be
|
// TODO: Can we use this for export? Copying SHM into the export might not be
|
||||||
// good
|
// good
|
||||||
func (c *Container) mountStorage() (string, error) {
|
func (c *Container) mountStorage() (_ string, Err error) {
|
||||||
var err error
|
var err error
|
||||||
// Container already mounted, nothing to do
|
// Container already mounted, nothing to do
|
||||||
if c.state.Mounted {
|
if c.state.Mounted {
|
||||||
|
|
@ -1225,6 +1225,40 @@ func (c *Container) mountStorage() (string, error) {
|
||||||
if err := os.Chown(c.config.ShmDir, c.RootUID(), c.RootGID()); err != nil {
|
if err := os.Chown(c.config.ShmDir, c.RootUID(), c.RootGID()); err != nil {
|
||||||
return "", errors.Wrapf(err, "failed to chown %s", c.config.ShmDir)
|
return "", errors.Wrapf(err, "failed to chown %s", c.config.ShmDir)
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
if err := c.unmountSHM(c.config.ShmDir); err != nil {
|
||||||
|
logrus.Errorf("Error unmounting SHM for container %s after mount error: %v", c.ID(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a mount of all named volumes
|
||||||
|
for _, v := range c.config.NamedVolumes {
|
||||||
|
vol, err := c.runtime.state.Volume(v.Name)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "error retrieving named volume %s for container %s", v.Name, c.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
if vol.needsMount() {
|
||||||
|
vol.lock.Lock()
|
||||||
|
if err := vol.mount(); err != nil {
|
||||||
|
vol.lock.Unlock()
|
||||||
|
return "", errors.Wrapf(err, "error mounting volume %s for container %s", vol.Name(), c.ID())
|
||||||
|
}
|
||||||
|
vol.lock.Unlock()
|
||||||
|
defer func() {
|
||||||
|
if Err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vol.lock.Lock()
|
||||||
|
if err := vol.unmount(false); err != nil {
|
||||||
|
logrus.Errorf("Error unmounting volume %s after error mounting container %s: %v", vol.Name(), c.ID(), err)
|
||||||
|
}
|
||||||
|
vol.lock.Unlock()
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: generalize this mount code so it will mount every mount in ctr.config.Mounts
|
// TODO: generalize this mount code so it will mount every mount in ctr.config.Mounts
|
||||||
|
|
@ -1247,14 +1281,19 @@ func (c *Container) cleanupStorage() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cleanupErr error
|
||||||
|
|
||||||
for _, containerMount := range c.config.Mounts {
|
for _, containerMount := range c.config.Mounts {
|
||||||
if err := c.unmountSHM(containerMount); err != nil {
|
if err := c.unmountSHM(containerMount); err != nil {
|
||||||
return err
|
if cleanupErr != nil {
|
||||||
|
logrus.Errorf("Error unmounting container %s: %v", c.ID(), cleanupErr)
|
||||||
|
}
|
||||||
|
cleanupErr = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.config.Rootfs != "" {
|
if c.config.Rootfs != "" {
|
||||||
return nil
|
return cleanupErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.unmount(false); err != nil {
|
if err := c.unmount(false); err != nil {
|
||||||
|
|
@ -1264,19 +1303,52 @@ func (c *Container) cleanupStorage() error {
|
||||||
// state
|
// state
|
||||||
if errors.Cause(err) == storage.ErrNotAContainer || errors.Cause(err) == storage.ErrContainerUnknown {
|
if errors.Cause(err) == storage.ErrNotAContainer || errors.Cause(err) == storage.ErrContainerUnknown {
|
||||||
logrus.Errorf("Storage for container %s has been removed", c.ID())
|
logrus.Errorf("Storage for container %s has been removed", c.ID())
|
||||||
return nil
|
} else {
|
||||||
|
if cleanupErr != nil {
|
||||||
|
logrus.Errorf("Error cleaning up container %s storage: %v", c.ID(), cleanupErr)
|
||||||
|
}
|
||||||
|
cleanupErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request an unmount of all named volumes
|
||||||
|
for _, v := range c.config.NamedVolumes {
|
||||||
|
vol, err := c.runtime.state.Volume(v.Name)
|
||||||
|
if err != nil {
|
||||||
|
if cleanupErr != nil {
|
||||||
|
logrus.Errorf("Error unmounting container %s: %v", c.ID(), cleanupErr)
|
||||||
|
}
|
||||||
|
cleanupErr = errors.Wrapf(err, "error retrieving named volume %s for container %s", v.Name, c.ID())
|
||||||
|
|
||||||
|
// We need to try and unmount every volume, so continue
|
||||||
|
// if they fail.
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
if vol.needsMount() {
|
||||||
|
vol.lock.Lock()
|
||||||
|
if err := vol.unmount(false); err != nil {
|
||||||
|
if cleanupErr != nil {
|
||||||
|
logrus.Errorf("Error unmounting container %s: %v", c.ID(), cleanupErr)
|
||||||
|
}
|
||||||
|
cleanupErr = errors.Wrapf(err, "error unmounting volume %s for container %s", vol.Name(), c.ID())
|
||||||
|
}
|
||||||
|
vol.lock.Unlock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.state.Mountpoint = ""
|
c.state.Mountpoint = ""
|
||||||
c.state.Mounted = false
|
c.state.Mounted = false
|
||||||
|
|
||||||
if c.valid {
|
if c.valid {
|
||||||
return c.save()
|
if err := c.save(); err != nil {
|
||||||
|
if cleanupErr != nil {
|
||||||
|
logrus.Errorf("Error unmounting container %s: %v", c.ID(), cleanupErr)
|
||||||
|
}
|
||||||
|
cleanupErr = err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return cleanupErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmount the a container and free its resources
|
// Unmount the a container and free its resources
|
||||||
|
|
|
||||||
|
|
@ -49,19 +49,19 @@ func (c *Container) mountSHM(shmOptions string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) unmountSHM(mount string) error {
|
func (c *Container) unmountSHM(mount string) error {
|
||||||
if err := unix.Unmount(mount, unix.MNT_DETACH); err != nil {
|
if err := unix.Unmount(mount, 0); err != nil {
|
||||||
if err != syscall.EINVAL {
|
if err != syscall.EINVAL {
|
||||||
logrus.Warnf("container %s failed to unmount %s : %v", c.ID(), mount, err)
|
return errors.Wrapf(err, "error unmounting container %s SHM mount %s", c.ID(), mount)
|
||||||
} else {
|
|
||||||
logrus.Debugf("container %s failed to unmount %s : %v", c.ID(), mount, err)
|
|
||||||
}
|
}
|
||||||
|
// If it's just an EINVAL, debug logs only
|
||||||
|
logrus.Debugf("container %s failed to unmount %s : %v", c.ID(), mount, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare mounts the container and sets up other required resources like net
|
// prepare mounts the container and sets up other required resources like net
|
||||||
// namespaces
|
// namespaces
|
||||||
func (c *Container) prepare() (err error) {
|
func (c *Container) prepare() (Err error) {
|
||||||
var (
|
var (
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
netNS ns.NetNS
|
netNS ns.NetNS
|
||||||
|
|
@ -108,31 +108,42 @@ func (c *Container) prepare() (err error) {
|
||||||
logrus.Debugf("Created root filesystem for container %s at %s", c.ID(), c.state.Mountpoint)
|
logrus.Debugf("Created root filesystem for container %s at %s", c.ID(), c.state.Mountpoint)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
if err2 := c.cleanupNetwork(); err2 != nil {
|
|
||||||
logrus.Errorf("Error cleaning up container %s network: %v", c.ID(), err2)
|
|
||||||
}
|
|
||||||
if err2 := c.cleanupStorage(); err2 != nil {
|
|
||||||
logrus.Errorf("Error cleaning up container %s storage: %v", c.ID(), err2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
var createErr error
|
||||||
if createNetNSErr != nil {
|
if createNetNSErr != nil {
|
||||||
if mountStorageErr != nil {
|
createErr = createNetNSErr
|
||||||
logrus.Error(createNetNSErr)
|
|
||||||
return mountStorageErr
|
|
||||||
}
|
|
||||||
return createNetNSErr
|
|
||||||
}
|
}
|
||||||
if mountStorageErr != nil {
|
if mountStorageErr != nil {
|
||||||
return mountStorageErr
|
logrus.Errorf("Error preparing container %s: %v", c.ID(), createErr)
|
||||||
|
createErr = mountStorageErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the container
|
// Only trigger storage cleanup if mountStorage was successful.
|
||||||
|
// Otherwise, we may mess up mount counters.
|
||||||
|
if createNetNSErr != nil && mountStorageErr == nil {
|
||||||
|
if err := c.cleanupStorage(); err != nil {
|
||||||
|
// createErr is guaranteed non-nil, so print
|
||||||
|
// unconditionally
|
||||||
|
logrus.Errorf("Error preparing container %s: %v", c.ID(), createErr)
|
||||||
|
createErr = errors.Wrapf(err, "error unmounting storage for container %s after network create failure", c.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's OK to unconditionally trigger network cleanup. If the network
|
||||||
|
// isn't ready it will do nothing.
|
||||||
|
if createErr != nil {
|
||||||
|
if err := c.cleanupNetwork(); err != nil {
|
||||||
|
logrus.Errorf("Error preparing container %s: %v", c.ID(), createErr)
|
||||||
|
createErr = errors.Wrapf(err, "error cleaning up container %s network after setup failure", c.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if createErr != nil {
|
||||||
|
return createErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes to container state
|
||||||
return c.save()
|
return c.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,7 @@ type InfoData struct {
|
||||||
Type string
|
Type string
|
||||||
Data map[string]interface{}
|
Data map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VolumeDriverLocal is the "local" volume driver. It is managed by libpod
|
||||||
|
// itself.
|
||||||
|
const VolumeDriverLocal = "local"
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,36 @@ func (s *InMemoryState) RemoveVolume(volume *Volume) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateVolume updates a volume from the database.
|
||||||
|
// For the in-memory state, this is a no-op.
|
||||||
|
func (s *InMemoryState) UpdateVolume(volume *Volume) error {
|
||||||
|
if !volume.valid {
|
||||||
|
return define.ErrVolumeRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := s.volumes[volume.Name()]; !ok {
|
||||||
|
volume.valid = false
|
||||||
|
return errors.Wrapf(define.ErrNoSuchVolume, "volume with name %q not found in state", volume.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveVolume saves a volume's state to the database.
|
||||||
|
// For the in-memory state, this is a no-op.
|
||||||
|
func (s *InMemoryState) SaveVolume(volume *Volume) error {
|
||||||
|
if !volume.valid {
|
||||||
|
return define.ErrVolumeRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := s.volumes[volume.Name()]; !ok {
|
||||||
|
volume.valid = false
|
||||||
|
return errors.Wrapf(define.ErrNoSuchVolume, "volume with name %q not found in state", volume.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// VolumeInUse checks if the given volume is being used by at least one container
|
// VolumeInUse checks if the given volume is being used by at least one container
|
||||||
func (s *InMemoryState) VolumeInUse(volume *Volume) ([]string, error) {
|
func (s *InMemoryState) VolumeInUse(volume *Volume) ([]string, error) {
|
||||||
if !volume.valid {
|
if !volume.valid {
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,23 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
|
||||||
volume.config.Name = stringid.GenerateNonCryptoID()
|
volume.config.Name = stringid.GenerateNonCryptoID()
|
||||||
}
|
}
|
||||||
if volume.config.Driver == "" {
|
if volume.config.Driver == "" {
|
||||||
volume.config.Driver = "local"
|
volume.config.Driver = define.VolumeDriverLocal
|
||||||
}
|
}
|
||||||
volume.config.CreatedTime = time.Now()
|
volume.config.CreatedTime = time.Now()
|
||||||
|
|
||||||
|
if volume.config.Driver == define.VolumeDriverLocal {
|
||||||
|
logrus.Debugf("Validating options for local driver")
|
||||||
|
// Validate options
|
||||||
|
for key := range volume.config.Options {
|
||||||
|
switch key {
|
||||||
|
case "device", "o", "type":
|
||||||
|
// Do nothing, valid keys
|
||||||
|
default:
|
||||||
|
return nil, errors.Wrapf(define.ErrInvalidArg, "invalid mount option %s for driver 'local'", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the mountpoint of this volume
|
// Create the mountpoint of this volume
|
||||||
volPathRoot := filepath.Join(r.config.VolumePath, volume.config.Name)
|
volPathRoot := filepath.Join(r.config.VolumePath, volume.config.Name)
|
||||||
if err := os.MkdirAll(volPathRoot, 0700); err != nil {
|
if err := os.MkdirAll(volPathRoot, 0700); err != nil {
|
||||||
|
|
@ -102,6 +115,11 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool) error
|
||||||
return define.ErrVolumeRemoved
|
return define.ErrVolumeRemoved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update volume status to pick up a potential removal from state
|
||||||
|
if err := v.update(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
deps, err := r.state.VolumeInUse(v)
|
deps, err := r.state.VolumeInUse(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -137,6 +155,11 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the volume is still mounted - force unmount it
|
||||||
|
if err := v.unmount(true); err != nil {
|
||||||
|
return errors.Wrapf(err, "error unmounting volume %s", v.Name())
|
||||||
|
}
|
||||||
|
|
||||||
// Set volume as invalid so it can no longer be used
|
// Set volume as invalid so it can no longer be used
|
||||||
v.valid = false
|
v.valid = false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,10 @@ type State interface {
|
||||||
// RemoveVolume removes the specified volume.
|
// RemoveVolume removes the specified volume.
|
||||||
// Only volumes that have no container dependencies can be removed
|
// Only volumes that have no container dependencies can be removed
|
||||||
RemoveVolume(volume *Volume) error
|
RemoveVolume(volume *Volume) error
|
||||||
|
// UpdateVolume updates the volume's state from the database.
|
||||||
|
UpdateVolume(volume *Volume) error
|
||||||
|
// SaveVolume saves a volume's state to the database.
|
||||||
|
SaveVolume(volume *Volume) error
|
||||||
// AllVolumes returns all the volumes available in the state
|
// AllVolumes returns all the volumes available in the state
|
||||||
AllVolumes() ([]*Volume, error)
|
AllVolumes() ([]*Volume, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,20 @@ import (
|
||||||
"github.com/containers/libpod/libpod/lock"
|
"github.com/containers/libpod/libpod/lock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Volume is the type used to create named volumes
|
// Volume is a libpod named volume.
|
||||||
// TODO: all volumes should be created using this and the Volume API
|
// Named volumes may be shared by multiple containers, and may be created using
|
||||||
|
// more complex options than normal bind mounts. They may be backed by a mounted
|
||||||
|
// filesystem on the host.
|
||||||
type Volume struct {
|
type Volume struct {
|
||||||
config *VolumeConfig
|
config *VolumeConfig
|
||||||
|
state *VolumeState
|
||||||
|
|
||||||
valid bool
|
valid bool
|
||||||
runtime *Runtime
|
runtime *Runtime
|
||||||
lock lock.Locker
|
lock lock.Locker
|
||||||
}
|
}
|
||||||
|
|
||||||
// VolumeConfig holds the volume's config information
|
// VolumeConfig holds the volume's immutable configuration.
|
||||||
type VolumeConfig struct {
|
type VolumeConfig struct {
|
||||||
// Name of the volume.
|
// Name of the volume.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
@ -26,7 +29,7 @@ type VolumeConfig struct {
|
||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
// The volume driver. Empty string or local does not activate a volume
|
// The volume driver. Empty string or local does not activate a volume
|
||||||
// driver, all other volumes will.
|
// driver, all other volumes will.
|
||||||
Driver string `json:"driver"`
|
Driver string `json:"volumeDriver"`
|
||||||
// The location the volume is mounted at.
|
// The location the volume is mounted at.
|
||||||
MountPoint string `json:"mountPoint"`
|
MountPoint string `json:"mountPoint"`
|
||||||
// Time the volume was created.
|
// Time the volume was created.
|
||||||
|
|
@ -34,7 +37,7 @@ type VolumeConfig struct {
|
||||||
// Options to pass to the volume driver. For the local driver, this is
|
// Options to pass to the volume driver. For the local driver, this is
|
||||||
// a list of mount options. For other drivers, they are passed to the
|
// a list of mount options. For other drivers, they are passed to the
|
||||||
// volume driver handling the volume.
|
// volume driver handling the volume.
|
||||||
Options map[string]string `json:"options"`
|
Options map[string]string `json:"volumeOptions,omitempty"`
|
||||||
// Whether this volume was created for a specific container and will be
|
// Whether this volume was created for a specific container and will be
|
||||||
// removed with it.
|
// removed with it.
|
||||||
IsCtrSpecific bool `json:"ctrSpecific"`
|
IsCtrSpecific bool `json:"ctrSpecific"`
|
||||||
|
|
@ -44,6 +47,18 @@ type VolumeConfig struct {
|
||||||
GID int `json:"gid"`
|
GID int `json:"gid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VolumeState holds the volume's mutable state.
|
||||||
|
// Volumes are not guaranteed to have a state. Only volumes using the Local
|
||||||
|
// driver that have mount options set will create a state.
|
||||||
|
type VolumeState struct {
|
||||||
|
// MountCount is the number of times this volume has been requested to
|
||||||
|
// be mounted.
|
||||||
|
// It is incremented on mount() and decremented on unmount().
|
||||||
|
// On incrementing from 0, the volume will be mounted on the host.
|
||||||
|
// On decrementing to 0, the volume will be unmounted on the host.
|
||||||
|
MountCount uint `json:"mountCount"`
|
||||||
|
}
|
||||||
|
|
||||||
// Name retrieves the volume's name
|
// Name retrieves the volume's name
|
||||||
func (v *Volume) Name() string {
|
func (v *Volume) Name() string {
|
||||||
return v.config.Name
|
return v.config.Name
|
||||||
|
|
@ -78,10 +93,9 @@ func (v *Volume) MountPoint() string {
|
||||||
// Options return the volume's options
|
// Options return the volume's options
|
||||||
func (v *Volume) Options() map[string]string {
|
func (v *Volume) Options() map[string]string {
|
||||||
options := make(map[string]string)
|
options := make(map[string]string)
|
||||||
for key, value := range v.config.Options {
|
for k, v := range v.config.Options {
|
||||||
options[key] = value
|
options[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package libpod
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/containers/libpod/libpod/define"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Creates a new volume
|
// Creates a new volume
|
||||||
|
|
@ -20,3 +22,25 @@ func newVolume(runtime *Runtime) (*Volume, error) {
|
||||||
func (v *Volume) teardownStorage() error {
|
func (v *Volume) teardownStorage() error {
|
||||||
return os.RemoveAll(filepath.Join(v.runtime.config.VolumePath, v.Name()))
|
return os.RemoveAll(filepath.Join(v.runtime.config.VolumePath, v.Name()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Volumes with options set, or a filesystem type, or a device to mount need to
|
||||||
|
// be mounted and unmounted.
|
||||||
|
func (v *Volume) needsMount() bool {
|
||||||
|
return len(v.config.Options) > 0 && v.config.Driver == define.VolumeDriverLocal
|
||||||
|
}
|
||||||
|
|
||||||
|
// update() updates the volume state from the DB.
|
||||||
|
func (v *Volume) update() error {
|
||||||
|
if err := v.runtime.state.UpdateVolume(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !v.valid {
|
||||||
|
return define.ErrVolumeRemoved
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save() saves the volume state to the DB
|
||||||
|
func (v *Volume) save() error {
|
||||||
|
return v.runtime.state.SaveVolume(v)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package libpod
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mount mounts the volume if necessary.
|
||||||
|
// A mount is necessary if a volume has any options set.
|
||||||
|
// If a mount is necessary, v.state.MountCount will be incremented.
|
||||||
|
// If it was 0 when the increment occurred, the volume will be mounted on the
|
||||||
|
// host. Otherwise, we assume it is already mounted.
|
||||||
|
// Must be done while the volume is locked.
|
||||||
|
// Is a no-op on volumes that do not require a mount (as defined by
|
||||||
|
// volumeNeedsMount())
|
||||||
|
func (v *Volume) mount() error {
|
||||||
|
if !v.needsMount() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the volume from the DB to get an accurate mount counter.
|
||||||
|
if err := v.update(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the count is non-zero, the volume is already mounted.
|
||||||
|
// Nothing to do.
|
||||||
|
if v.state.MountCount > 0 {
|
||||||
|
v.state.MountCount = v.state.MountCount + 1
|
||||||
|
logrus.Debugf("Volume %s mount count now at %d", v.Name(), v.state.MountCount)
|
||||||
|
return v.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
volDevice := v.config.Options["device"]
|
||||||
|
volType := v.config.Options["type"]
|
||||||
|
volOptions := v.config.Options["o"]
|
||||||
|
|
||||||
|
// Some filesystems (tmpfs) don't have a device, but we still need to
|
||||||
|
// give the kernel something.
|
||||||
|
if volDevice == "" && volType != "" {
|
||||||
|
volDevice = volType
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to use the actual mount command.
|
||||||
|
// Convincing unix.Mount to use the same semantics as the mount command
|
||||||
|
// itself seems prohibitively difficult.
|
||||||
|
// TODO: might want to cache this path in the runtime?
|
||||||
|
mountPath, err := exec.LookPath("mount")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error locating 'mount' binary")
|
||||||
|
}
|
||||||
|
mountArgs := []string{}
|
||||||
|
if volOptions != "" {
|
||||||
|
mountArgs = append(mountArgs, "-o", volOptions)
|
||||||
|
}
|
||||||
|
if volType != "" {
|
||||||
|
mountArgs = append(mountArgs, "-t", volType)
|
||||||
|
}
|
||||||
|
mountArgs = append(mountArgs, volDevice, v.config.MountPoint)
|
||||||
|
mountCmd := exec.Command(mountPath, mountArgs...)
|
||||||
|
|
||||||
|
errPipe, err := mountCmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "error getting stderr pipe for mount")
|
||||||
|
}
|
||||||
|
if err := mountCmd.Start(); err != nil {
|
||||||
|
out, err2 := ioutil.ReadAll(errPipe)
|
||||||
|
if err2 != nil {
|
||||||
|
return errors.Wrapf(err2, "error reading mount STDERR")
|
||||||
|
}
|
||||||
|
return errors.Wrapf(errors.New(string(out)), "error mounting volume %s", v.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Mounted volume %s", v.Name())
|
||||||
|
|
||||||
|
// Increment the mount counter
|
||||||
|
v.state.MountCount = v.state.MountCount + 1
|
||||||
|
logrus.Debugf("Volume %s mount count now at %d", v.Name(), v.state.MountCount)
|
||||||
|
return v.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmount unmounts the volume if necessary.
|
||||||
|
// Unmounting a volume that is not mounted is a no-op.
|
||||||
|
// Unmounting a volume that does not require a mount is a no-op.
|
||||||
|
// The volume must be locked for this to occur.
|
||||||
|
// The mount counter will be decremented if non-zero. If the counter reaches 0,
|
||||||
|
// the volume will really be unmounted, as no further containers are using the
|
||||||
|
// volume.
|
||||||
|
// If force is set, the volume will be unmounted regardless of mount counter.
|
||||||
|
func (v *Volume) unmount(force bool) error {
|
||||||
|
if !v.needsMount() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the volume from the DB to get an accurate mount counter.
|
||||||
|
if err := v.update(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.state.MountCount == 0 {
|
||||||
|
logrus.Debugf("Volume %s already unmounted", v.Name())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
v.state.MountCount = v.state.MountCount - 1
|
||||||
|
} else {
|
||||||
|
v.state.MountCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Volume %s mount count now at %d", v.Name(), v.state.MountCount)
|
||||||
|
|
||||||
|
if v.state.MountCount == 0 {
|
||||||
|
// Unmount the volume
|
||||||
|
if err := unix.Unmount(v.config.MountPoint, unix.MNT_DETACH); err != nil {
|
||||||
|
return errors.Wrapf(err, "error unmounting volume %s", v.Name())
|
||||||
|
}
|
||||||
|
logrus.Debugf("Unmounted volume %s", v.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.save()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package libpod
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/containers/libpod/libpod/define"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *Volume) mount() error {
|
||||||
|
return define.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Volume) unmount(force bool) error {
|
||||||
|
return define.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
@ -185,7 +185,7 @@ func (r *LocalRuntime) CreateVolume(ctx context.Context, c *cliconfig.VolumeCrea
|
||||||
options = append(options, libpod.WithVolumeLabels(labels))
|
options = append(options, libpod.WithVolumeLabels(labels))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(options) != 0 {
|
if len(opts) != 0 {
|
||||||
options = append(options, libpod.WithVolumeOptions(opts))
|
options = append(options, libpod.WithVolumeOptions(opts))
|
||||||
}
|
}
|
||||||
newVolume, err := r.NewVolume(ctx, options...)
|
newVolume, err := r.NewVolume(ctx, options...)
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,14 @@ package integration
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
. "github.com/containers/libpod/test/utils"
|
. "github.com/containers/libpod/test/utils"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/onsi/gomega/gexec"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("Podman run with volumes", func() {
|
var _ = Describe("Podman run with volumes", func() {
|
||||||
|
|
@ -190,4 +193,60 @@ var _ = Describe("Podman run with volumes", func() {
|
||||||
session.WaitWithDefaultTimeout()
|
session.WaitWithDefaultTimeout()
|
||||||
Expect(session.ExitCode()).To(Not(Equal(0)))
|
Expect(session.ExitCode()).To(Not(Equal(0)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("podman run with tmpfs named volume mounts and unmounts", func() {
|
||||||
|
SkipIfRootless()
|
||||||
|
volName := "testvol"
|
||||||
|
mkVolume := podmanTest.Podman([]string{"volume", "create", "--opt", "type=tmpfs", "--opt", "device=tmpfs", "--opt", "o=nodev", "testvol"})
|
||||||
|
mkVolume.WaitWithDefaultTimeout()
|
||||||
|
Expect(mkVolume.ExitCode()).To(Equal(0))
|
||||||
|
|
||||||
|
// Volume not mounted on create
|
||||||
|
mountCmd1, err := gexec.Start(exec.Command("mount"), GinkgoWriter, GinkgoWriter)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
mountCmd1.Wait(90)
|
||||||
|
Expect(mountCmd1.ExitCode()).To(Equal(0))
|
||||||
|
os.Stdout.Sync()
|
||||||
|
os.Stderr.Sync()
|
||||||
|
mountOut1 := strings.Join(strings.Fields(fmt.Sprintf("%s", mountCmd1.Out.Contents())), " ")
|
||||||
|
fmt.Printf("Output: %s", mountOut1)
|
||||||
|
Expect(strings.Contains(mountOut1, volName)).To(BeFalse())
|
||||||
|
|
||||||
|
ctrName := "testctr"
|
||||||
|
podmanSession := podmanTest.Podman([]string{"run", "-d", "--name", ctrName, "-v", fmt.Sprintf("%s:/testvol", volName), ALPINE, "top"})
|
||||||
|
podmanSession.WaitWithDefaultTimeout()
|
||||||
|
Expect(podmanSession.ExitCode()).To(Equal(0))
|
||||||
|
|
||||||
|
// Volume now mounted as container is running
|
||||||
|
mountCmd2, err := gexec.Start(exec.Command("mount"), GinkgoWriter, GinkgoWriter)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
mountCmd2.Wait(90)
|
||||||
|
Expect(mountCmd2.ExitCode()).To(Equal(0))
|
||||||
|
os.Stdout.Sync()
|
||||||
|
os.Stderr.Sync()
|
||||||
|
mountOut2 := strings.Join(strings.Fields(fmt.Sprintf("%s", mountCmd2.Out.Contents())), " ")
|
||||||
|
fmt.Printf("Output: %s", mountOut2)
|
||||||
|
Expect(strings.Contains(mountOut2, volName)).To(BeTrue())
|
||||||
|
|
||||||
|
// Stop the container to unmount
|
||||||
|
podmanStopSession := podmanTest.Podman([]string{"stop", "--timeout", "0", ctrName})
|
||||||
|
podmanStopSession.WaitWithDefaultTimeout()
|
||||||
|
Expect(podmanStopSession.ExitCode()).To(Equal(0))
|
||||||
|
|
||||||
|
// We have to force cleanup so the unmount happens
|
||||||
|
podmanCleanupSession := podmanTest.Podman([]string{"container", "cleanup", ctrName})
|
||||||
|
podmanCleanupSession.WaitWithDefaultTimeout()
|
||||||
|
Expect(podmanCleanupSession.ExitCode()).To(Equal(0))
|
||||||
|
|
||||||
|
// Ensure volume is unmounted
|
||||||
|
mountCmd3, err := gexec.Start(exec.Command("mount"), GinkgoWriter, GinkgoWriter)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
mountCmd3.Wait(90)
|
||||||
|
Expect(mountCmd3.ExitCode()).To(Equal(0))
|
||||||
|
os.Stdout.Sync()
|
||||||
|
os.Stderr.Sync()
|
||||||
|
mountOut3 := strings.Join(strings.Fields(fmt.Sprintf("%s", mountCmd3.Out.Contents())), " ")
|
||||||
|
fmt.Printf("Output: %s", mountOut3)
|
||||||
|
Expect(strings.Contains(mountOut3, volName)).To(BeFalse())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,10 @@ var _ = Describe("Podman volume create", func() {
|
||||||
Expect(match).To(BeTrue())
|
Expect(match).To(BeTrue())
|
||||||
Expect(len(check.OutputToStringArray())).To(Equal(1))
|
Expect(len(check.OutputToStringArray())).To(Equal(1))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("podman create volume with bad volume option", func() {
|
||||||
|
session := podmanTest.Podman([]string{"volume", "create", "--opt", "badOpt=bad"})
|
||||||
|
session.WaitWithDefaultTimeout()
|
||||||
|
Expect(session.ExitCode()).To(Not(Equal(0)))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue