Skip to content

Commit

Permalink
Initial implementation of volume plugins
Browse files Browse the repository at this point in the history
This implements support for mounting and unmounting volumes
backed by volume plugins. Support for actually retrieving
plugins requires a pull request to land in containers.conf and
then that to be vendored, and as such is not yet ready. Given
this, this code is only compile tested. However, the code for
everything past retrieving the plugin has been written - there is
support for creating, removing, mounting, and unmounting volumes,
which should allow full functionality once the c/common PR is
merged.

A major change is the signature of the MountPoint function for
volumes, which now, by necessity, returns an error. Named volumes
managed by a plugin do not have a mountpoint we control; instead,
it is managed entirely by the plugin. As such, we need to cache
the path in the DB, and calls to retrieve it now need to access
the DB (and may fail as such).

Notably absent is support for SELinux relabelling and chowning
these volumes. Given that we don't manage the mountpoint for
these volumes, I am extremely reluctant to try and modify it - we
could easily break the plugin trying to chown or relabel it.

Also, we had no less than *5* separate implementations of
inspecting a volume floating around in pkg/infra/abi and
pkg/api/handlers/libpod. And none of them used volume.Inspect(),
the only correct way of inspecting volumes. Remove them all and
consolidate to using the correct way. Compat API is likely still
doing things the wrong way, but that is an issue for another day.

Fixes containers#4304

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
  • Loading branch information
mheon committed Jan 8, 2021
1 parent 49db79e commit 76e75b9
Show file tree
Hide file tree
Showing 24 changed files with 496 additions and 247 deletions.
10 changes: 6 additions & 4 deletions docs/source/markdown/podman-volume-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ driver options can be set using the **--opt** flag.

#### **--driver**=*driver*

Specify the volume driver name (default local).
Specify the volume driver name (default **local**). Setting this to a value other than **local** Podman will attempt to create the volume using a volume plugin with the given name. Such plugins must be defined in the **volume_plugins** section of the **containers.conf**(5) configuration file.

#### **--help**

Expand All @@ -30,13 +30,14 @@ Set metadata for a volume (e.g., --label mykey=value).
#### **--opt**=*option*, **-o**

Set driver specific options.
For the default driver, `local`, this allows a volume to be configured to mount a filesystem on the host.
For the default driver, **local**, this allows a volume to be configured to mount a filesystem on the host.
For the `local` driver the following options are supported: `type`, `device`, and `o`.
The `type` option sets the type of the filesystem to be mounted, and is equivalent to the `-t` flag to **mount(8)**.
The `device` option sets the device to be mounted, and is equivalent to the `device` argument to **mount(8)**.
The `o` option sets options for the mount, and is equivalent to the `-o` flag to **mount(8)** with two exceptions.
The `o` option supports `uid` and `gid` options to set the UID and GID of the created volume that are not normally supported by **mount(8)**.
Using volume options with the `local` driver requires root privileges.
Using volume options with the **local** driver requires root privileges.
When not using the **local** driver, the given options will be passed directly to the volume plugin. In this case, supported options will be dictated by the plugin in question, not Podman.

## EXAMPLES

Expand All @@ -53,7 +54,8 @@ $ podman volume create --label foo=bar myvol
```

## SEE ALSO
podman-volume(1), mount(8)
**podman-volume**(1), **mount**(8), **containers.conf**(5)

## HISTORY
January 2020, updated with information on volume plugins by Matthew Heon <mheon@redhat.com>
November 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>
1 change: 1 addition & 0 deletions libpod/boltdb_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ func (s *BoltState) Refresh() error {

// Reset mount count to 0
oldState.MountCount = 0
oldState.MountPoint = ""

newState, err := json.Marshal(oldState)
if err != nil {
Expand Down
15 changes: 15 additions & 0 deletions libpod/boltdb_state_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,21 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu
}
}

// Retrieve volume driver
if volume.UsesVolumeDriver() {
plugin, err := s.runtime.getVolumePlugin(volume.config.Driver)
if err != nil {
// We want to fail gracefully here, to ensure that we
// can still remove volumes even if their plugin is
// missing. Otherwise, we end up with volumes that
// cannot even be retrieved from the database and will
// cause things like `volume ls` to fail.
logrus.Errorf("Volume %s uses volume plugin %s, but it cannot be accessed - some functionality may not be available: %v", volume.Name(), volume.config.Driver, err)
} else {
volume.plugin = plugin
}
}

// Get the lock
lock, err := s.runtime.lockManager.RetrieveLock(volume.config.LockID)
if err != nil {
Expand Down
7 changes: 6 additions & 1 deletion libpod/container_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,12 @@ func (c *Container) getInspectMounts(namedVolumes []*ContainerNamedVolume, image
return nil, errors.Wrapf(err, "error looking up volume %s in container %s config", volume.Name, c.ID())
}
mountStruct.Driver = volFromDB.Driver()
mountStruct.Source = volFromDB.MountPoint()

mountPoint, err := volFromDB.MountPoint()
if err != nil {
return nil, err
}
mountStruct.Source = mountPoint

parseMountOptionsForInspect(volume.Options, &mountStruct)

Expand Down
23 changes: 20 additions & 3 deletions libpod/container_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -1535,8 +1535,18 @@ func (c *Container) mountNamedVolume(v *ContainerNamedVolume, mountpoint string)
return nil, err
}

// HACK HACK HACK - copy up into a volume driver is 100% broken
// right now.
if vol.UsesVolumeDriver() {
logrus.Infof("Not copying up into volume %s as it uses a volume driver", vol.Name())
return vol, nil
}

// If the volume is not empty, we should not copy up.
volMount := vol.MountPoint()
volMount, err := vol.MountPoint()
if err != nil {
return nil, err
}
contents, err := ioutil.ReadDir(volMount)
if err != nil {
return nil, errors.Wrapf(err, "error listing contents of volume %s mountpoint when copying up from container %s", vol.Name(), c.ID())
Expand Down Expand Up @@ -1574,7 +1584,11 @@ func (c *Container) chownVolume(volumeName string) error {
return err
}

if vol.state.NeedsChown {
// TODO: For now, I've disabled chowning volumes owned by non-Podman
// drivers. This may be safe, but it's really going to be a case-by-case
// thing, I think - safest to leave disabled now and reenable later if
// there is a demand.
if vol.state.NeedsChown && !vol.UsesVolumeDriver() {
vol.state.NeedsChown = false

uid := int(c.config.Spec.Process.User.UID)
Expand All @@ -1601,7 +1615,10 @@ func (c *Container) chownVolume(volumeName string) error {
return err
}

mountPoint := vol.MountPoint()
mountPoint, err := vol.MountPoint()
if err != nil {
return err
}

if err := os.Lchown(mountPoint, uid, gid); err != nil {
return err
Expand Down
39 changes: 28 additions & 11 deletions libpod/container_internal_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,10 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
if err != nil {
return nil, errors.Wrapf(err, "error retrieving volume %s to add to container %s", namedVol.Name, c.ID())
}
mountPoint := volume.MountPoint()
mountPoint, err := volume.MountPoint()
if err != nil {
return nil, err
}
volMount := spec.Mount{
Type: "bind",
Source: mountPoint,
Expand Down Expand Up @@ -901,7 +904,15 @@ func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error {
return err
}

input, err := archive.TarWithOptions(volume.MountPoint(), &archive.TarOptions{
mp, err := volume.MountPoint()
if err != nil {
return err
}
if mp == "" {
return errors.Wrapf(define.ErrInternal, "volume %s is not mounted, cannot export", volume.Name())
}

input, err := archive.TarWithOptions(mp, &archive.TarOptions{
Compression: archive.Uncompressed,
IncludeSourceDir: true,
})
Expand Down Expand Up @@ -956,10 +967,10 @@ func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error {

func (c *Container) checkpointRestoreSupported() error {
if !criu.CheckForCriu() {
return errors.Errorf("Checkpoint/Restore requires at least CRIU %d", criu.MinCriuVersion)
return errors.Errorf("checkpoint/restore requires at least CRIU %d", criu.MinCriuVersion)
}
if !c.ociRuntime.SupportsCheckpoint() {
return errors.Errorf("Configured runtime does not support checkpoint/restore")
return errors.Errorf("configured runtime does not support checkpoint/restore")
}
return nil
}
Expand Down Expand Up @@ -991,7 +1002,7 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
}

if c.AutoRemove() && options.TargetFile == "" {
return errors.Errorf("Cannot checkpoint containers that have been started with '--rm' unless '--export' is used")
return errors.Errorf("cannot checkpoint containers that have been started with '--rm' unless '--export' is used")
}

if err := c.checkpointRestoreLabelLog("dump.log"); err != nil {
Expand Down Expand Up @@ -1068,13 +1079,13 @@ func (c *Container) importCheckpoint(input string) error {
}
err = archive.Untar(archiveFile, c.bundlePath(), options)
if err != nil {
return errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", input)
return errors.Wrapf(err, "unpacking of checkpoint archive %s failed", input)
}

// Make sure the newly created config.json exists on disk
g := generate.Generator{Config: c.config.Spec}
if err = c.saveSpec(g.Config); err != nil {
return errors.Wrap(err, "Saving imported container specification for restore failed")
return errors.Wrap(err, "saving imported container specification for restore failed")
}

return nil
Expand All @@ -1098,7 +1109,7 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
// Let's try to stat() CRIU's inventory file. If it does not exist, it makes
// no sense to try a restore. This is a minimal check if a checkpoint exist.
if _, err := os.Stat(filepath.Join(c.CheckpointPath(), "inventory.img")); os.IsNotExist(err) {
return errors.Wrapf(err, "A complete checkpoint for this container cannot be found, cannot restore")
return errors.Wrapf(err, "a complete checkpoint for this container cannot be found, cannot restore")
}

if err := c.checkpointRestoreLabelLog("restore.log"); err != nil {
Expand Down Expand Up @@ -1254,16 +1265,22 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti

volumeFile, err := os.Open(volumeFilePath)
if err != nil {
return errors.Wrapf(err, "Failed to open volume file %s", volumeFilePath)
return errors.Wrapf(err, "failed to open volume file %s", volumeFilePath)
}
defer volumeFile.Close()

volume, err := c.runtime.GetVolume(v.Name)
if err != nil {
return errors.Wrapf(err, "Failed to retrieve volume %s", v.Name)
return errors.Wrapf(err, "failed to retrieve volume %s", v.Name)
}

mountPoint := volume.MountPoint()
mountPoint, err := volume.MountPoint()
if err != nil {
return err
}
if mountPoint == "" {
return errors.Wrapf(err, "unable to import volume %s as it is not mounted", volume.Name())
}
if err := archive.UntarUncompressed(volumeFile, mountPoint, nil); err != nil {
return errors.Wrapf(err, "Failed to extract volume %s to %s", volumeFilePath, mountPoint)
}
Expand Down
4 changes: 4 additions & 0 deletions libpod/define/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ var (
// aliases.
ErrNoAliases = errors.New("no aliases for container")

// ErrMissingPlugin indicates that the requested operation requires a
// plugin that is not present on the system or in the configuration.
ErrMissingPlugin = errors.New("required plugin missing")

// ErrCtrExists indicates a container with the same name or ID already
// exists
ErrCtrExists = errors.New("container already exists")
Expand Down
47 changes: 47 additions & 0 deletions libpod/define/volume_inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package define

import (
"time"
)

// InspectVolumeData is the output of Inspect() on a volume. It is matched to
// the format of 'docker volume inspect'.
type InspectVolumeData struct {
// Name is the name of the volume.
Name string `json:"Name"`
// Driver is the driver used to create the volume.
// If set to "local" or "", the Local driver (Podman built-in code) is
// used to service the volume; otherwise, a volume plugin with the given
// name is used to mount and manage the volume.
Driver string `json:"Driver"`
// Mountpoint is the path on the host where the volume is mounted.
Mountpoint string `json:"Mountpoint"`
// CreatedAt is the date and time the volume was created at. This is not
// stored for older Libpod volumes; if so, it will be omitted.
CreatedAt time.Time `json:"CreatedAt,omitempty"`
// Status is used to return information on the volume's current state,
// if the volume was created using a volume plugin (uses a Driver that
// is not the local driver).
// Status is provided to us by an external program, so no guarantees are
// made about its format or contents. Further, it is an optional field,
// so it may not be set even in cases where a volume plugin is in use.
Status map[string]interface{} `json:"Status,omitempty"`
// Labels includes the volume's configured labels, key:value pairs that
// can be passed during volume creation to provide information for third
// party tools.
Labels map[string]string `json:"Labels"`
// Scope is unused and provided solely for Docker compatibility. It is
// unconditionally set to "local".
Scope string `json:"Scope"`
// Options is a set of options that were used when creating the volume.
// It is presently not used.
Options map[string]string `json:"Options"`
// UID is the UID that the volume was created with.
UID int `json:"UID,omitempty"`
// GID is the GID that the volume was created with.
GID int `json:"GID,omitempty"`
// Anonymous indicates that the volume was created as an anonymous
// volume for a specific container, and will be be removed when any
// container using it is removed.
Anonymous bool `json:"Anonymous,omitempty"`
}
11 changes: 0 additions & 11 deletions libpod/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -1549,17 +1549,6 @@ func WithVolumeDriver(driver string) VolumeCreateOption {
return define.ErrVolumeFinalized
}

// Uncomment when volume plugins are ready for use.
// if driver != define.VolumeDriverLocal {
// if _, err := plugin.GetVolumePlugin(driver); err != nil {
// return err
// }
// }

if driver != define.VolumeDriverLocal {
return define.ErrNotImplemented
}

volume.config.Driver = driver
return nil
}
Expand Down
Loading

0 comments on commit 76e75b9

Please sign in to comment.