Skip to content

Commit

Permalink
Add ability for volumes with options to mount/umount
Browse files Browse the repository at this point in the history
When volume options and the local volume driver are specified,
the volume is intended to be mounted using the 'mount' command.
Supported options will be used to volume the volume before the
first container using it starts, and unmount the volume after the
last container using it dies.

This should work for any local filesystem, though at present I've
only tested with tmpfs and btrfs.

Signed-off-by: Matthew Heon <[email protected]>
  • Loading branch information
mheon committed Sep 5, 2019
1 parent 5a8a71e commit a760e32
Show file tree
Hide file tree
Showing 14 changed files with 412 additions and 36 deletions.
2 changes: 2 additions & 0 deletions docs/podman-volume-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ $ podman volume create myvol
$ podman volume create
$ podman volume create --label foo=bar myvol
$ podman volume create --opt device=tmpfs --opt type=tmpfs --opt o=nodev,noexec myvol
```

## SEE ALSO
Expand Down
49 changes: 49 additions & 0 deletions libpod/boltdb_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ func (s *BoltState) Refresh() error {
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.
// If they are, unmarshal their state, and then clear
// PID, mountpoint, and state for all of them
Expand Down Expand Up @@ -235,6 +245,44 @@ func (s *BoltState) Refresh() error {

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
Expand Down Expand Up @@ -1630,6 +1678,7 @@ func (s *BoltState) AllVolumes() ([]*Volume, error) {

volume := new(Volume)
volume.config = new(VolumeConfig)
volume.state = new(VolumeState)

if err := s.getVolumeFromDB(id, volume, volBucket); err != nil {
if errors.Cause(err) != define.ErrNSMismatch {
Expand Down
2 changes: 1 addition & 1 deletion libpod/boltdb_state_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu
return errors.Wrapf(err, "error unmarshalling volume %s config from DB", string(name))
}

// Volume state is allowed to be nil for legacy compatability
// 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 {
Expand Down
73 changes: 70 additions & 3 deletions libpod/container_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: Can we use this for export? Copying SHM into the export might not be
// good
func (c *Container) mountStorage() (string, error) {
func (c *Container) mountStorage() (_ string, Err error) {
var err error
// Container already mounted, nothing to do
if c.state.Mounted {
Expand All @@ -1225,6 +1225,40 @@ func (c *Container) mountStorage() (string, error) {
if err := os.Chown(c.config.ShmDir, c.RootUID(), c.RootGID()); err != nil {
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()
defer vol.lock.Unlock()
if err := vol.unmount(false); err != nil {
logrus.Errorf("Error unmounting volume %s after error mounting container %s: %v", vol.Name(), c.ID(), err)
}
}()
}
}

// TODO: generalize this mount code so it will mount every mount in ctr.config.Mounts
Expand Down Expand Up @@ -1270,13 +1304,46 @@ func (c *Container) cleanupStorage() error {
return err
}

var cleanupErr error

// 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
}

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.Mounted = false

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
Expand Down
49 changes: 30 additions & 19 deletions libpod/container_internal_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (c *Container) unmountSHM(mount string) error {

// prepare mounts the container and sets up other required resources like net
// namespaces
func (c *Container) prepare() (err error) {
func (c *Container) prepare() (Err error) {
var (
wg sync.WaitGroup
netNS ns.NetNS
Expand Down Expand Up @@ -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)
}()

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()

var createErr error
if createNetNSErr != nil {
if mountStorageErr != nil {
logrus.Error(createNetNSErr)
return mountStorageErr
}
return createNetNSErr
createErr = createNetNSErr
}
if mountStorageErr != nil {
return mountStorageErr
logrus.Errorf("Error preparing container %s: %v", c.ID(), createErr)
createErr = mountStorageErr
}

// 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 the container
// Save changes to container state
return c.save()
}

Expand Down
23 changes: 23 additions & 0 deletions libpod/runtime_volume_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
}
volume.config.CreatedTime = time.Now()

if volume.config.Driver == "local" {
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
volPathRoot := filepath.Join(r.config.VolumePath, volume.config.Name)
if err := os.MkdirAll(volPathRoot, 0700); err != nil {
Expand Down Expand Up @@ -102,6 +115,11 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool) error
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)
if err != nil {
return err
Expand Down Expand Up @@ -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
v.valid = false

Expand Down
13 changes: 2 additions & 11 deletions libpod/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,6 @@ type VolumeConfig struct {
// a list of mount options. For other drivers, they are passed to the
// volume driver handling the volume.
Options map[string]string `json:"volumeOptions,omitempty"`
// Type is the type of the volume. This is only used with the local
// driver. It the the filesystem that we will attempt to mount - nfs,
// tmpfs, etc.
Type string `json:"type,omitempty"`
// Device is the device of the volume. This is only used with the local
// driver, and only with some filesystem types (e.g., not required by
// tmpfs). It is the device to mount.
Device string `json:"device,omitempty"`
// Whether this volume was created for a specific container and will be
// removed with it.
IsCtrSpecific bool `json:"ctrSpecific"`
Expand Down Expand Up @@ -101,10 +93,9 @@ func (v *Volume) MountPoint() string {
// 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
for k, v := range v.config.Options {
options[k] = v
}

return options
}

Expand Down
24 changes: 24 additions & 0 deletions libpod/volume_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package libpod
import (
"os"
"path/filepath"

"github.com/containers/libpod/libpod/define"
)

// Creates a new volume
Expand All @@ -20,3 +22,25 @@ func newVolume(runtime *Runtime) (*Volume, error) {
func (v *Volume) teardownStorage() error {
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 == "local"
}

// 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)
}
Loading

0 comments on commit a760e32

Please sign in to comment.