Skip to content

WIP: volume protection by pinning #26846

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions cmd/podman/system/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ var (
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman system prune`,
}
force bool
force bool
includePinned bool
)

func init() {
Expand All @@ -50,6 +51,7 @@ func init() {
flags.BoolVar(&pruneOptions.External, "external", false, "Remove container data in storage not controlled by podman")
flags.BoolVar(&pruneOptions.Build, "build", false, "Remove build containers")
flags.BoolVar(&pruneOptions.Volume, "volumes", false, "Prune volumes")
flags.BoolVar(&includePinned, "include-pinned", false, "Include pinned volumes in prune operation")
filterFlagName := "filter"
flags.StringArrayVar(&filters, filterFlagName, []string{}, "Provide filter values (e.g. 'label=<key>=<value>')")
_ = pruneCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePruneFilters)
Expand Down Expand Up @@ -80,12 +82,22 @@ func prune(cmd *cobra.Command, args []string) error {
return nil
}
}

// Set the include pinned flag for volume pruning
if pruneOptions.Volume {
pruneOptions.VolumePruneOptions.IncludePinned = includePinned
}

// Remove all unused pods, containers, images, networks, and volume data.
pruneOptions.Filters, err = parse.FilterArgumentsIntoFilters(filters)
if err != nil {
return err
}

// Set the include pinned flag for volume pruning
if pruneOptions.Volume {
pruneOptions.VolumePruneOptions.IncludePinned = includePinned
}

response, err := registry.ContainerEngine().SystemPrune(context.Background(), pruneOptions)
if err != nil {
Expand Down Expand Up @@ -126,6 +138,11 @@ func prune(cmd *cobra.Command, args []string) error {
}

func createPruneWarningMessage(pruneOpts entities.SystemPruneOptions) string {
pinnedNote := ""
if pruneOpts.Volume && !pruneOpts.VolumePruneOptions.IncludePinned {
pinnedNote = " (excluding pinned volumes)"
}

if pruneOpts.All {
return `WARNING! This command removes:
- all stopped containers
Expand All @@ -137,7 +154,7 @@ func createPruneWarningMessage(pruneOpts entities.SystemPruneOptions) string {
}
return `WARNING! This command removes:
- all stopped containers
- all networks not used by at least one container%s%s
- all networks not used by at least one container%s%s` + pinnedNote + `
- all dangling images
- all dangling build cache

Expand Down
6 changes: 6 additions & 0 deletions cmd/podman/volumes/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var (
Ignore bool
UID int
GID int
Pinned bool
}{}
)

Expand Down Expand Up @@ -68,6 +69,10 @@ func init() {
gidFlagName := "gid"
flags.IntVar(&opts.GID, gidFlagName, 0, "Set the GID of the volume owner")
_ = createCommand.RegisterFlagCompletionFunc(gidFlagName, completion.AutocompleteNone)

pinnedFlagName := "pinned"
flags.BoolVar(&opts.Pinned, pinnedFlagName, false, "Mark volume as pinned (excluded from system prune by default)")
_ = createCommand.RegisterFlagCompletionFunc(pinnedFlagName, completion.AutocompleteNone)
}

func create(cmd *cobra.Command, args []string) error {
Expand All @@ -94,6 +99,7 @@ func create(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("gid") {
createOpts.GID = &opts.GID
}
createOpts.Pinned = opts.Pinned
response, err := registry.ContainerEngine().VolumeCreate(context.Background(), createOpts)
if err != nil {
return err
Expand Down
66 changes: 66 additions & 0 deletions cmd/podman/volumes/pin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package volumes

import (
"context"
"fmt"

"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)

var (
pinDescription = `Mark or unmark a volume as pinned.

Pinned volumes are excluded from system prune operations by default.`

pinCommand = &cobra.Command{
Use: "pin [options] VOLUME [VOLUME...]",
Short: "Mark or unmark volume as pinned",
Long: pinDescription,
RunE: pin,
ValidArgsFunction: common.AutocompleteVolumes,
Example: `podman volume pin myvol
podman volume pin --unpin myvol
podman volume pin vol1 vol2 vol3`,
}
)

var (
pinOptions = entities.VolumePinOptions{}
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: pinCommand,
Parent: volumeCmd,
})
flags := pinCommand.Flags()
flags.BoolVar(&pinOptions.Unpin, "unpin", false, "Remove pinning from volume")
}

func pin(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("must specify at least one volume name")
}

responses, err := registry.ContainerEngine().VolumePin(context.Background(), args, pinOptions)
if err != nil {
return err
}

for _, r := range responses {
if r.Err != nil {
fmt.Printf("Error pinning volume %s: %v\n", r.Id, r.Err)
} else {
if pinOptions.Unpin {
fmt.Printf("Volume %s is now unpinned\n", r.Id)
} else {
fmt.Printf("Volume %s is now pinned\n", r.Id)
}
}
}

return nil
}
10 changes: 5 additions & 5 deletions cmd/podman/volumes/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ var (
)

var (
rmOptions = entities.VolumeRmOptions{}
stopTimeout int
rmOptions = entities.VolumeRmOptions{}
stopTimeout int
includePinned bool
)

func init() {
Expand All @@ -45,6 +46,7 @@ func init() {
flags := rmCommand.Flags()
flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all volumes")
flags.BoolVarP(&rmOptions.Force, "force", "f", false, "Remove a volume by force, even if it is being used by a container")
flags.BoolVar(&includePinned, "include-pinned", false, "Include pinned volumes in removal operation")
timeFlagName := "time"
flags.IntVarP(&stopTimeout, timeFlagName, "t", int(containerConfig.Engine.StopTimeout), "Seconds to wait for running containers to stop before killing the container")
_ = rmCommand.RegisterFlagCompletionFunc(timeFlagName, completion.AutocompleteNone)
Expand All @@ -64,6 +66,7 @@ func rm(cmd *cobra.Command, args []string) error {
timeout := uint(stopTimeout)
rmOptions.Timeout = &timeout
}
rmOptions.IncludePinned = includePinned
responses, err := registry.ContainerEngine().VolumeRm(context.Background(), args, rmOptions)
if err != nil {
if rmOptions.Force && strings.Contains(err.Error(), define.ErrNoSuchVolume.Error()) {
Expand All @@ -76,9 +79,6 @@ func rm(cmd *cobra.Command, args []string) error {
if r.Err == nil {
fmt.Println(r.Id)
} else {
if rmOptions.Force && strings.Contains(r.Err.Error(), define.ErrNoSuchVolume.Error()) {
continue
}
setExitCode(r.Err)
errs = append(errs, r.Err)
}
Expand Down
3 changes: 3 additions & 0 deletions libpod/define/volume_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ type InspectVolumeData struct {
StorageID string `json:"StorageID,omitempty"`
// LockNumber is the number of the volume's Libpod lock.
LockNumber uint32
// Pinned indicates that this volume should be excluded from
// system prune operations by default.
Pinned bool `json:"Pinned,omitempty"`
}

type VolumeReload struct {
Expand Down
14 changes: 14 additions & 0 deletions libpod/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -1753,6 +1753,20 @@ func withSetAnon() VolumeCreateOption {
}
}

// WithVolumePinned sets the pinned flag for the volume.
// Pinned volumes are excluded from system prune operations by default.
func WithVolumePinned() VolumeCreateOption {
return func(volume *Volume) error {
if volume.valid {
return define.ErrVolumeFinalized
}

volume.state.Pinned = true

return nil
}
}

// WithVolumeDriverTimeout sets the volume creation timeout period.
// Only usable if a non-local volume driver is in use.
func WithVolumeDriverTimeout(timeout uint) VolumeCreateOption {
Expand Down
52 changes: 52 additions & 0 deletions libpod/runtime_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,55 @@ func (r *Runtime) PruneVolumes(ctx context.Context, filterFuncs []VolumeFilter)
}
return preports, nil
}

// PruneVolumesWithOptions removes unused volumes from the system with options
func (r *Runtime) PruneVolumesWithOptions(ctx context.Context, filterFuncs []VolumeFilter, includePinned bool) ([]*reports.PruneReport, error) {
preports := make([]*reports.PruneReport, 0)
vols, err := r.Volumes(filterFuncs...)
if err != nil {
return nil, err
}

for _, vol := range vols {
// Skip pinned volumes unless explicitly requested
if vol.Pinned() && !includePinned {
continue
}

report := new(reports.PruneReport)
volSize, err := vol.Size()
if err != nil {
volSize = 0
}
report.Size = volSize
report.Id = vol.Name()
var timeout *uint
if err := r.RemoveVolume(ctx, vol, false, timeout); err != nil {
if !errors.Is(err, define.ErrVolumeBeingUsed) && !errors.Is(err, define.ErrVolumeRemoved) {
report.Err = err
} else {
// We didn't remove the volume for some reason
continue
}
} else {
vol.newVolumeEvent(events.Prune)
}
preports = append(preports, report)
}
return preports, nil
}

// SetVolumePinned sets the pinned status of a volume by name.
// Pinned volumes are excluded from system prune operations by default.
func (r *Runtime) SetVolumePinned(volumeName string, pinned bool) error {
if !r.valid {
return define.ErrRuntimeStopped
}

vol, err := r.state.Volume(volumeName)
if err != nil {
return err
}

return vol.SetPinned(pinned)
}
35 changes: 35 additions & 0 deletions libpod/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ type VolumeState struct {
UIDChowned int `json:"uidChowned,omitempty"`
// GIDChowned is the GID the volume was chowned to.
GIDChowned int `json:"gidChowned,omitempty"`
// Pinned indicates that this volume should be excluded from
// system prune operations by default
Pinned bool `json:"pinned,omitempty"`
}

// Name retrieves the volume's name
Expand Down Expand Up @@ -283,6 +286,38 @@ func (v *Volume) UsesVolumeDriver() bool {
return v.config.Driver != define.VolumeDriverLocal && v.config.Driver != ""
}

// Pinned returns whether this volume is marked as pinned.
// Pinned volumes are excluded from system prune operations by default.
func (v *Volume) Pinned() bool {
v.lock.Lock()
defer v.lock.Unlock()

if err := v.update(); err != nil {
return false
}

return v.state.Pinned
}

// SetPinned sets the pinned status of the volume.
// Pinned volumes are excluded from system prune operations by default.
func (v *Volume) SetPinned(pinned bool) error {
if !v.valid {
return define.ErrVolumeRemoved
}

v.lock.Lock()
defer v.lock.Unlock()

if err := v.update(); err != nil {
return err
}

v.state.Pinned = pinned

return v.save()
}

func (v *Volume) Mount() (string, error) {
v.lock.Lock()
defer v.lock.Unlock()
Expand Down
2 changes: 2 additions & 0 deletions libpod/volume_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,7 @@ func (v *Volume) Inspect() (*define.InspectVolumeData, error) {
data.Timeout = v.runtime.config.Engine.VolumePluginTimeout
}

data.Pinned = v.state.Pinned

return data, nil
}
24 changes: 21 additions & 3 deletions pkg/api/handlers/libpod/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) {
volumeOptions = append(volumeOptions, libpod.WithVolumeGID(*input.GID), libpod.WithVolumeNoChown())
}

if input.Pinned {
volumeOptions = append(volumeOptions, libpod.WithVolumePinned())
}

vol, err := runtime.NewVolume(r.Context(), volumeOptions...)
if err != nil {
utils.InternalServerError(w, err)
Expand Down Expand Up @@ -163,7 +167,13 @@ func pruneVolumesHelper(r *http.Request) ([]*reports.PruneReport, error) {
filterFuncs = append(filterFuncs, filterFunc)
}

reports, err := runtime.PruneVolumes(r.Context(), filterFuncs)
// Check for includePinned parameter
includePinned := false
if includeParam := r.URL.Query().Get("includePinned"); includeParam == "true" {
includePinned = true
}

reports, err := runtime.PruneVolumesWithOptions(r.Context(), filterFuncs, includePinned)
if err != nil {
return nil, err
}
Expand All @@ -176,8 +186,9 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) {
decoder = r.Context().Value(api.DecoderKey).(*schema.Decoder)
)
query := struct {
Force bool `schema:"force"`
Timeout *uint `schema:"timeout"`
Force bool `schema:"force"`
Timeout *uint `schema:"timeout"`
IncludePinned bool `schema:"includePinned"`
}{
// override any golang type defaults
}
Expand All @@ -193,6 +204,13 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) {
utils.VolumeNotFound(w, name, err)
return
}
// Check if volume is pinned and --include-pinned flag is not set
if vol.Pinned() && !query.IncludePinned {
utils.Error(w, http.StatusBadRequest,
fmt.Errorf("volume %s is pinned and cannot be removed without includePinned=true parameter", vol.Name()))
return
}

if err := runtime.RemoveVolume(r.Context(), vol, query.Force, query.Timeout); err != nil {
if errors.Is(err, define.ErrVolumeBeingUsed) {
utils.Error(w, http.StatusConflict, err)
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 @@ -122,4 +122,5 @@ type ContainerEngine interface { //nolint:interfacebloat
VolumeReload(ctx context.Context) (*VolumeReloadReport, error)
VolumeExport(ctx context.Context, nameOrID string, options VolumeExportOptions) error
VolumeImport(ctx context.Context, nameOrID string, options VolumeImportOptions) error
VolumePin(ctx context.Context, namesOrIds []string, opts VolumePinOptions) ([]*VolumePinReport, error)
}
Loading