Skip to content

Commit

Permalink
add podman volume reload to sync volume plugins
Browse files Browse the repository at this point in the history
Libpod requires that all volumes are stored in the libpod db. Because
volume plugins can be created outside of podman, it will not show all
available plugins. This podman volume reload command allows users to
sync the libpod db with their external volume plugins. All new volumes
from the plugin are also created in the libpod db and when a volume from
the db no longer exists it will be removed if possible.

There are some problems:
- naming conflicts, in this case we only use the first volume we found.
  This is not deterministic.
- race conditions, we have no control over the volume plugins. It is
  possible that the volumes changed while we run this command.

Fixes containers#14207

Signed-off-by: Paul Holzinger <[email protected]>
  • Loading branch information
Luap99 committed Jun 23, 2022
1 parent 6e8953a commit 2fab7d1
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 13 deletions.
52 changes: 52 additions & 0 deletions cmd/podman/volumes/reload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package volumes

import (
"fmt"

"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/utils"
"github.com/containers/podman/v4/cmd/podman/validate"
"github.com/spf13/cobra"
)

var (
reloadDescription = `Check all configured volume plugins and update the libpod database with all available volumes.
Existing volumes are also removed from the database when they are no longer present in the plugin.`
reloadCommand = &cobra.Command{
Use: "reload",
Args: validate.NoArgs,
Short: "reload all volumes from volume plugins",
Long: reloadDescription,
RunE: reload,
ValidArgsFunction: completion.AutocompleteNone,
}
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: reloadCommand,
Parent: volumeCmd,
})
}

func reload(cmd *cobra.Command, args []string) error {
report, err := registry.ContainerEngine().VolumeReload(registry.Context())
if err != nil {
return err
}
printReload("Added", report.Added)
printReload("Removed", report.Removed)
errs := (utils.OutputErrors)(report.Errors)
return errs.PrintErrors()
}

func printReload(typ string, values []string) {
if len(values) > 0 {
fmt.Println(typ + ":")
for _, name := range values {
fmt.Println(name)
}
}
}
29 changes: 29 additions & 0 deletions docs/source/markdown/podman-volume-reload.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
% podman-volume-reload(1)

## NAME
podman\-volume\-reload - Reload all volumes from volumes plugins

## SYNOPSIS
**podman volume reload**

## DESCRIPTION

**podman volume reload** checks all configured volume plugins and updates the libpod database with all available volumes.
Existing volumes are also removed from the database when they are no longer present in the plugin.

This command it is best effort and cannot guarantee a perfect state because plugins can be modified from the outside at any time.

Note: This command is not supported with podman-remote.

## EXAMPLES

```
$ podman volume reload
Added:
vol6
Removed:
t3
```

## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**
1 change: 1 addition & 0 deletions docs/source/markdown/podman-volume.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ podman volume is a set of subcommands that manage volumes.
| ls | [podman-volume-ls(1)](podman-volume-ls.1.md) | List all the available volumes. |
| mount | [podman-volume-mount(1)](podman-volume-mount.1.md) | Mount a volume filesystem. |
| prune | [podman-volume-prune(1)](podman-volume-prune.1.md) | Remove all unused volumes. |
| reload | [podman-volume-reload(1)](podman-volume-reload.1.md) | Reload all volumes from volumes plugins. |
| rm | [podman-volume-rm(1)](podman-volume-rm.1.md) | Remove one or more volumes. |
| unmount | [podman-volume-unmount(1)](podman-volume-unmount.1.md) | Unmount a volume. |

Expand Down
6 changes: 6 additions & 0 deletions libpod/define/volume_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ type InspectVolumeData struct {
// UID/GID.
NeedsChown bool `json:"NeedsChown,omitempty"`
}

type VolumeReload struct {
Added []string
Removed []string
Errors []error
}
6 changes: 3 additions & 3 deletions libpod/runtime_ctr.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai
volOptions = append(volOptions, parsedOptions...)
}
}
newVol, err := r.newVolume(volOptions...)
newVol, err := r.newVolume(false, volOptions...)
if err != nil {
return nil, errors.Wrapf(err, "error creating named volume %q", vol.Name)
}
Expand Down Expand Up @@ -805,7 +805,7 @@ func (r *Runtime) removeContainer(ctx context.Context, c *Container, force, remo
if !volume.Anonymous() {
continue
}
if err := runtime.removeVolume(ctx, volume, false, timeout); err != nil && errors.Cause(err) != define.ErrNoSuchVolume {
if err := runtime.removeVolume(ctx, volume, false, timeout, false); err != nil && errors.Cause(err) != define.ErrNoSuchVolume {
if errors.Cause(err) == define.ErrVolumeBeingUsed {
// Ignore error, since podman will report original error
volumesFrom, _ := c.volumesFrom()
Expand Down Expand Up @@ -963,7 +963,7 @@ func (r *Runtime) evictContainer(ctx context.Context, idOrName string, removeVol
if !volume.Anonymous() {
continue
}
if err := r.removeVolume(ctx, volume, false, timeout); err != nil && err != define.ErrNoSuchVolume && err != define.ErrVolumeBeingUsed {
if err := r.removeVolume(ctx, volume, false, timeout, false); err != nil && err != define.ErrNoSuchVolume && err != define.ErrVolumeBeingUsed {
logrus.Errorf("Cleaning up volume (%s): %v", v, err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion libpod/runtime_pod_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool,
if !volume.Anonymous() {
continue
}
if err := r.removeVolume(ctx, volume, false, timeout); err != nil {
if err := r.removeVolume(ctx, volume, false, timeout, false); err != nil {
if errors.Cause(err) == define.ErrNoSuchVolume || errors.Cause(err) == define.ErrVolumeRemoved {
continue
}
Expand Down
2 changes: 1 addition & 1 deletion libpod/runtime_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (r *Runtime) RemoveVolume(ctx context.Context, v *Volume, force bool, timeo
return nil
}
}
return r.removeVolume(ctx, v, force, timeout)
return r.removeVolume(ctx, v, force, timeout, false)
}

// GetVolume retrieves a volume given its full name.
Expand Down
98 changes: 91 additions & 7 deletions libpod/runtime_volume_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package libpod

import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
Expand All @@ -25,11 +26,13 @@ func (r *Runtime) NewVolume(ctx context.Context, options ...VolumeCreateOption)
if !r.valid {
return nil, define.ErrRuntimeStopped
}
return r.newVolume(options...)
return r.newVolume(false, options...)
}

// newVolume creates a new empty volume
func (r *Runtime) newVolume(options ...VolumeCreateOption) (_ *Volume, deferredErr error) {
// newVolume creates a new empty volume with the given options.
// The createPluginVolume can be set to true to make it not create the volume in the volume plugin,
// this is required for the UpdateVolumePlugins() function. If you are not sure set this to false.
func (r *Runtime) newVolume(noCreatePluginVolume bool, options ...VolumeCreateOption) (_ *Volume, deferredErr error) {
volume := newVolume(r)
for _, option := range options {
if err := option(volume); err != nil {
Expand Down Expand Up @@ -83,7 +86,7 @@ func (r *Runtime) newVolume(options ...VolumeCreateOption) (_ *Volume, deferredE

// Now we get conditional: we either need to make the volume in the
// volume plugin, or on disk if not using a plugin.
if volume.plugin != nil {
if volume.plugin != nil && !noCreatePluginVolume {
// We can't chown, or relabel, or similar the path the volume is
// using, because it's not managed by us.
// TODO: reevaluate this once we actually have volume plugins in
Expand Down Expand Up @@ -164,6 +167,85 @@ func (r *Runtime) newVolume(options ...VolumeCreateOption) (_ *Volume, deferredE
return volume, nil
}

// UpdateVolumePlugins reads all volumes from all configured volume plugins and
// imports them into the libpod db. It also checks if existing libpod volumes
// are removed in the plugin, in this case we try to remove it from libpod.
// On errors we continue and try to do as much as possible. all errors are
// returned as array in the returned struct.
// This function has many race conditions, it is best effort but cannot guarantee
// a perfect state since plugins can be modified from the outside at any time.
func (r *Runtime) UpdateVolumePlugins(ctx context.Context) *define.VolumeReload {
var (
added []string
removed []string
errs []error
allPluginVolumes = map[string]struct{}{}
)

for driverName, socket := range r.config.Engine.VolumePlugins {
driver, err := volplugin.GetVolumePlugin(driverName, socket)
if err != nil {
errs = append(errs, err)
continue
}
vols, err := driver.ListVolumes()
if err != nil {
errs = append(errs, fmt.Errorf("failed to read volumes from plugin %q: %w", driverName, err))
continue
}
for _, vol := range vols {
allPluginVolumes[vol.Name] = struct{}{}
if _, err := r.newVolume(true, WithVolumeName(vol.Name), WithVolumeDriver(driverName)); err != nil {
// If the volume exists this is not an error, just ignore it and log. It is very likely
// that the volume from the plugin was already in our db.
if !errors.Is(err, define.ErrVolumeExists) {
errs = append(errs, err)
continue
}
logrus.Infof("Volume %q already exists: %v", vol.Name, err)
continue
}
added = append(added, vol.Name)
}
}

libpodVolumes, err := r.state.AllVolumes()
if err != nil {
errs = append(errs, fmt.Errorf("cannot delete dangling plugin volumes: failed to read libpod volumes: %w", err))
}
for _, vol := range libpodVolumes {
if vol.UsesVolumeDriver() {
if _, ok := allPluginVolumes[vol.Name()]; !ok {
// The volume is no longer in the plugin, lets remove it from the libpod db.
if err := r.removeVolume(ctx, vol, false, nil, true); err != nil {
if errors.Is(err, define.ErrVolumeBeingUsed) {
// Volume is still used by at least one container. This is very bad,
// the plugin no longer has this but we still need it.
errs = append(errs, fmt.Errorf("volume was removed from the plugin %q but containers still require it: %w", vol.config.Driver, err))
continue
}
if errors.Is(err, define.ErrNoSuchVolume) || errors.Is(err, define.ErrVolumeRemoved) || errors.Is(err, define.ErrMissingPlugin) {
// Volume was already removed, no problem just ignore it and continue.
continue
}

// some other error
errs = append(errs, err)
continue
}
// Volume was successfully removed
removed = append(removed, vol.Name())
}
}
}

return &define.VolumeReload{
Added: added,
Removed: removed,
Errors: errs,
}
}

// makeVolumeInPluginIfNotExist makes a volume in the given volume plugin if it
// does not already exist.
func makeVolumeInPluginIfNotExist(name string, options map[string]string, plugin *volplugin.VolumePlugin) error {
Expand Down Expand Up @@ -197,8 +279,10 @@ func makeVolumeInPluginIfNotExist(name string, options map[string]string, plugin
return 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 bool, timeout *uint) error {
// removeVolume removes the specified volume from state as well tears down its mountpoint and storage.
// ignoreVolumePlugin is used to only remove the volume from the db and not the plugin,
// this is required when the volume was already removed from the plugin, i.e. in UpdateVolumePlugins().
func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeout *uint, ignoreVolumePlugin bool) error {
if !v.valid {
if ok, _ := r.state.HasVolume(v.Name()); !ok {
return nil
Expand Down Expand Up @@ -263,7 +347,7 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo
var removalErr error

// If we use a volume plugin, we need to remove from the plugin.
if v.UsesVolumeDriver() {
if v.UsesVolumeDriver() && !ignoreVolumePlugin {
canRemove := true

// Do we have a volume driver?
Expand Down
1 change: 1 addition & 0 deletions pkg/domain/entities/engine_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,5 @@ type ContainerEngine interface {
VolumePrune(ctx context.Context, options VolumePruneOptions) ([]*reports.PruneReport, error)
VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error)
VolumeUnmount(ctx context.Context, namesOrIds []string) ([]*VolumeUnmountReport, error)
VolumeReload(ctx context.Context) (*VolumeReloadReport, error)
}
5 changes: 5 additions & 0 deletions pkg/domain/entities/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ type VolumeListReport struct {
VolumeConfigResponse
}

// VolumeReloadReport describes the response from reload volume plugins
type VolumeReloadReport struct {
define.VolumeReload
}

/*
* Docker API compatibility types
*/
Expand Down
5 changes: 5 additions & 0 deletions pkg/domain/infra/abi/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,8 @@ func (ic *ContainerEngine) VolumeUnmount(ctx context.Context, nameOrIDs []string

return reports, nil
}

func (ic *ContainerEngine) VolumeReload(ctx context.Context) (*entities.VolumeReloadReport, error) {
report := ic.Libpod.UpdateVolumePlugins(ctx)
return &entities.VolumeReloadReport{VolumeReload: *report}, nil
}
4 changes: 4 additions & 0 deletions pkg/domain/infra/tunnel/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,7 @@ func (ic *ContainerEngine) VolumeMount(ctx context.Context, nameOrIDs []string)
func (ic *ContainerEngine) VolumeUnmount(ctx context.Context, nameOrIDs []string) ([]*entities.VolumeUnmountReport, error) {
return nil, errors.New("unmounting volumes is not supported for remote clients")
}

func (ic *ContainerEngine) VolumeReload(ctx context.Context) (*entities.VolumeReloadReport, error) {
return nil, errors.New("volume reload is not supported for remote clients")
}
2 changes: 1 addition & 1 deletion test/e2e/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var (
healthcheck = "quay.io/libpod/alpine_healthcheck:latest"
ImageCacheDir = "/tmp/podman/imagecachedir"
fedoraToolbox = "registry.fedoraproject.org/fedora-toolbox:36"
volumeTest = "quay.io/libpod/volume-plugin-test-img:latest"
volumeTest = "quay.io/libpod/volume-plugin-test-img:20220623"

// This image has seccomp profiles that blocks all syscalls.
// The intention behind blocking all syscalls is to prevent
Expand Down
Loading

0 comments on commit 2fab7d1

Please sign in to comment.