diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index daecfbe5e26..7b6505d3ff1 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -1933,6 +1933,7 @@ func AutocompleteVolumeFilters(cmd *cobra.Command, _ []string, toComplete string "label=": nil, "name=": func(s string) ([]string, cobra.ShellCompDirective) { return getVolumes(cmd, s) }, "opt=": nil, + "pinned=": getBoolCompletion, "scope=": local, "since=": getImg, "until=": nil, diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go index f5d44eb2598..a6c58a65f6c 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -36,7 +36,8 @@ var ( ValidArgsFunction: completion.AutocompleteNone, Example: `podman system prune`, } - force bool + force bool + includePinned bool ) func init() { @@ -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==')") _ = pruneCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePruneFilters) @@ -81,6 +83,11 @@ func prune(_ *cobra.Command, _ []string) error { } } + // 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 { @@ -137,7 +144,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 (optionally including pinned volumes) - all dangling images - all dangling build cache diff --git a/cmd/podman/system/reset.go b/cmd/podman/system/reset.go index c302c0c6f3a..dae31e821b8 100644 --- a/cmd/podman/system/reset.go +++ b/cmd/podman/system/reset.go @@ -11,6 +11,7 @@ import ( "github.com/containers/buildah/pkg/volumes" "github.com/containers/podman/v6/cmd/podman/registry" "github.com/containers/podman/v6/cmd/podman/validate" + "github.com/containers/podman/v6/pkg/domain/entities" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/completion" @@ -19,7 +20,7 @@ import ( var ( systemResetDescription = `Reset podman storage back to default state - All containers will be stopped and removed, and all images, volumes, networks and container content will be removed. + All containers will be stopped and removed, and all images, volumes (excluding pinned volumes), networks and container content will be removed. This command does not restart podman.service and podman.socket systemd units. You may need to manually restart it after running this command. ` systemResetCommand = &cobra.Command{ @@ -32,7 +33,8 @@ var ( ValidArgsFunction: completion.AutocompleteNone, } - forceFlag bool + forceFlag bool + resetIncludePinned bool ) func init() { @@ -42,6 +44,7 @@ func init() { }) flags := systemResetCommand.Flags() flags.BoolVarP(&forceFlag, "force", "f", false, "Do not prompt for confirmation") + flags.BoolVar(&resetIncludePinned, "include-pinned", false, "Include pinned volumes in reset operation") } func reset(_ *cobra.Command, _ []string) { @@ -53,14 +56,19 @@ func reset(_ *cobra.Command, _ []string) { // Prompt for confirmation if --force is not set if !forceFlag { reader := bufio.NewReader(os.Stdin) - fmt.Println(`WARNING! This will remove: + volumeMsg := " - all volumes (excluding pinned volumes)" + if resetIncludePinned { + volumeMsg = " - all volumes" + } + fmt.Printf(`WARNING! This will remove: - all containers - all pods - all images - all networks - all build cache - all machines - - all volumes`) +%s +`, volumeMsg) info, _ := registry.ContainerEngine().Info(registry.Context()) // lets not hard fail in case of an error @@ -93,7 +101,10 @@ func reset(_ *cobra.Command, _ []string) { } // ContainerEngine() is unusable and shut down after this. - if err := registry.ContainerEngine().Reset(registry.Context()); err != nil { + resetOptions := entities.SystemResetOptions{ + IncludePinned: resetIncludePinned, + } + if err := registry.ContainerEngine().Reset(registry.Context(), resetOptions); err != nil { logrus.Error(err) } diff --git a/cmd/podman/volumes/create.go b/cmd/podman/volumes/create.go index 78c09048949..d10b31a0f6a 100644 --- a/cmd/podman/volumes/create.go +++ b/cmd/podman/volumes/create.go @@ -36,6 +36,7 @@ var ( Ignore bool UID int GID int + Pinned bool }{} ) @@ -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 { @@ -92,6 +97,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 diff --git a/cmd/podman/volumes/pin.go b/cmd/podman/volumes/pin.go new file mode 100644 index 00000000000..3568d4f184e --- /dev/null +++ b/cmd/podman/volumes/pin.go @@ -0,0 +1,66 @@ +package volumes + +import ( + "context" + "fmt" + + "github.com/containers/podman/v6/cmd/podman/common" + "github.com/containers/podman/v6/cmd/podman/registry" + "github.com/containers/podman/v6/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + pinDescription = `Mark or unmark a volume as pinned. + +Pinned volumes are excluded from system prune and system reset operations.` + + 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(_ *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 +} diff --git a/cmd/podman/volumes/prune.go b/cmd/podman/volumes/prune.go index 6cf388af4ed..298d30c0c5d 100644 --- a/cmd/podman/volumes/prune.go +++ b/cmd/podman/volumes/prune.go @@ -30,7 +30,8 @@ var ( RunE: prune, ValidArgsFunction: completion.AutocompleteNone, } - filter = []string{} + filter = []string{} + pruneOptions = entities.VolumePruneOptions{} ) func init() { @@ -44,6 +45,7 @@ func init() { flags.StringArrayVar(&filter, filterFlagName, []string{}, "Provide filter values (e.g. 'label==')") _ = pruneCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteVolumeFilters) flags.BoolP("force", "f", false, "Do not prompt for confirmation") + flags.BoolVar(&pruneOptions.IncludePinned, "include-pinned", false, "Do not prune pinned volumes") } func prune(cmd *cobra.Command, _ []string) error { diff --git a/cmd/podman/volumes/rm.go b/cmd/podman/volumes/rm.go index e4da2b02a34..208cf3a1dcf 100644 --- a/cmd/podman/volumes/rm.go +++ b/cmd/podman/volumes/rm.go @@ -33,8 +33,9 @@ var ( ) var ( - rmOptions = entities.VolumeRmOptions{} - stopTimeout int + rmOptions = entities.VolumeRmOptions{} + stopTimeout int + includePinned bool ) func init() { @@ -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) @@ -62,6 +64,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()) { @@ -74,9 +77,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) } diff --git a/docs/source/markdown/podman-system-prune.1.md b/docs/source/markdown/podman-system-prune.1.md index 7386f1f1d25..adb66fd042d 100644 --- a/docs/source/markdown/podman-system-prune.1.md +++ b/docs/source/markdown/podman-system-prune.1.md @@ -11,7 +11,7 @@ podman\-system\-prune - Remove all unused pods, containers, images, networks, an Use the **--all** option to delete all unused images. Unused images are dangling images as well as any image that does not have any containers based on it. -By default, volumes are not removed to prevent important data from being deleted if there is currently no container using the volume. Use the **--volumes** flag when running the command to prune volumes as well. +By default, volumes are not removed to prevent important data from being deleted if there is currently no container using the volume. Use the **--volumes** flag when running the command to prune volumes as well. By default, pinned volumes are excluded from pruning even when **--volumes** is specified. Use the **--include-pinned** flag to include pinned volumes in the prune operation. By default, build containers are not removed to prevent interference with builds in progress. Use the **--build** flag when running the command to remove build containers as well. @@ -59,6 +59,10 @@ Do not prompt for confirmation Print usage statement +#### **--include-pinned** + +Include pinned volumes in the prune operation when **--volumes** is specified. By default, pinned volumes are excluded from pruning to protect important data. This flag only has an effect when **--volumes** is also used. + #### **--volumes** Prune volumes currently unused by any container diff --git a/docs/source/markdown/podman-system-reset.1.md b/docs/source/markdown/podman-system-reset.1.md index 537a15d67c4..530ec2460e4 100644 --- a/docs/source/markdown/podman-system-reset.1.md +++ b/docs/source/markdown/podman-system-reset.1.md @@ -11,6 +11,8 @@ podman\-system\-reset - Reset storage back to initial state It also removes the configured graphRoot and runRoot directories. Make sure these are not set to some important directory. +By default, pinned volumes are excluded from the reset operation to protect important data. Use the **--include-pinned** flag to include pinned volumes in the reset. + This command must be run **before** changing any of the following fields in the `containers.conf` or `storage.conf` files: `driver`, `static_dir`, `tmp_dir` or `volume_path`. @@ -30,6 +32,10 @@ Do not prompt for confirmation Print usage statement +#### **--include-pinned** + +Include pinned volumes in the reset operation. By default, pinned volumes are excluded from the reset to protect important data. + ## EXAMPLES Reset all storage back to a clean initialized state. diff --git a/docs/source/markdown/podman-volume-create.1.md b/docs/source/markdown/podman-volume-create.1.md index 0ad874ee413..0b240dc68d7 100644 --- a/docs/source/markdown/podman-volume-create.1.md +++ b/docs/source/markdown/podman-volume-create.1.md @@ -73,6 +73,10 @@ This option is mandatory when using the **image** driver. When not using the **local** and **image** drivers, the given options are passed directly to the volume plugin. In this case, supported options are dictated by the plugin in question, not Podman. +#### **--pinned** + +Mark the volume as pinned. Pinned volumes are excluded from **podman system prune** and **podman system reset** operations by default, protecting them from accidental deletion during cleanup operations. The pinned status can be changed later using **podman volume pin**. + #### **--uid**=*uid* Set the UID that the volume will be created as. Differently than `--opt o=uid=*uid*`, the specified value is not passed to the mount operation. The specified UID will own the volume's mount point directory and affects the volume chown operation. @@ -137,6 +141,11 @@ Create volume overriding the owner UID and GID. # podman volume create --uid 1000 --gid 1000 myvol ``` +Create a pinned volume that is protected from system prune operations. +``` +$ podman volume create --pinned datavol +``` + Create image named volume using the specified local image in containers/storage. ``` # podman volume create --driver image --opt image=fedora:latest fedoraVol @@ -214,7 +223,7 @@ If performance is the priority, please check out the more performant [goofys](ht ## SEE ALSO -**[podman(1)](podman.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[podman-volume(1)](podman-volume.1.md)**, **mount(8)**, **xfs_quota(8)**, **xfs_quota(8)**, **projects(5)**, **projid(5)** +**[podman(1)](podman.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-pin(1)](podman-volume-pin.1.md)**, **mount(8)**, **xfs_quota(8)**, **xfs_quota(8)**, **projects(5)**, **projid(5)** ## HISTORY January 2020, updated with information on volume plugins by Matthew Heon diff --git a/docs/source/markdown/podman-volume-inspect.1.md b/docs/source/markdown/podman-volume-inspect.1.md index ed1a109d057..a0f89a3475e 100644 --- a/docs/source/markdown/podman-volume-inspect.1.md +++ b/docs/source/markdown/podman-volume-inspect.1.md @@ -44,6 +44,7 @@ Valid placeholders for the Go template are listed below: | .Status ... | Status of the volume | | .StorageID | StorageID of the volume | | .Timeout | Timeout of the volume | +| .Pinned | Whether the volume is pinned | | .UID | UID the volume was created with | #### **--help** diff --git a/docs/source/markdown/podman-volume-ls.1.md.in b/docs/source/markdown/podman-volume-ls.1.md.in index 64e84b518ec..707cd898880 100644 --- a/docs/source/markdown/podman-volume-ls.1.md.in +++ b/docs/source/markdown/podman-volume-ls.1.md.in @@ -30,6 +30,7 @@ Volumes can be filtered by the following attributes: | label | [Key] or [Key=Value] Label assigned to a volume | | name | [Name] Volume name (accepts regex) | | opt | Matches a storage driver options | +| pinned | [Bool] Matches volumes based on their pinned status (true/false) | | scope | Filters volume by scope | | after/since | Filter by volumes created after the given VOLUME (name or tag) | | until | Only remove volumes created before given timestamp | @@ -59,6 +60,7 @@ Valid placeholders for the Go template are listed below: | .Status ... | Status of the volume | | .StorageID | StorageID of the volume | | .Timeout | Timeout of the volume | +| .Pinned | Whether the volume is pinned | | .UID | UID of volume | | .VolumeConfigResponse ... | Don't use | @@ -99,6 +101,16 @@ List volumes with the label key=value. $ podman volume ls --filter label=key=value ``` +List all pinned volumes. +``` +$ podman volume ls --filter pinned=true +``` + +List all non-pinned volumes. +``` +$ podman volume ls --filter pinned=false +``` + ## SEE ALSO **[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)** diff --git a/docs/source/markdown/podman-volume-pin.1.md b/docs/source/markdown/podman-volume-pin.1.md new file mode 100644 index 00000000000..ee5bafde2b7 --- /dev/null +++ b/docs/source/markdown/podman-volume-pin.1.md @@ -0,0 +1,53 @@ +% podman-volume-pin 1 + +## NAME +podman\-volume\-pin - Mark or unmark volumes as pinned + +## SYNOPSIS +**podman volume pin** [*options*] *volume* [*volume* ...] + +## DESCRIPTION + +Mark or unmark one or more volumes as pinned. Pinned volumes are excluded from **podman system prune** and **podman system reset** operations by default, protecting them from accidental deletion. + +This is useful for volumes containing important persistent data that should be preserved during cleanup operations. + +By default, **podman volume pin** marks volumes as pinned. Use the **--unpin** option to remove the pinned status from volumes. + +## OPTIONS + +#### **--help** + +Print usage statement. + +#### **--unpin** + +Remove the pinned status from the specified volumes instead of pinning them. + +## EXAMPLES + +Mark a volume as pinned. +``` +$ podman volume pin myvol +Volume myvol is now pinned +``` + +Mark multiple volumes as pinned. +``` +$ podman volume pin vol1 vol2 vol3 +Volume vol1 is now pinned +Volume vol2 is now pinned +Volume vol3 is now pinned +``` + +Remove the pinned status from a volume. +``` +$ podman volume pin --unpin myvol +Volume myvol is now unpinned +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-create(1)](podman-volume-create.1.md)**, **[podman-volume-prune(1)](podman-volume-prune.1.md)**, **[podman-volume-rm(1)](podman-volume-rm.1.md)**, **[podman-system-prune(1)](podman-system-prune.1.md)**, **[podman-system-reset(1)](podman-system-reset.1.md)** + +## HISTORY +October 2025, Originally compiled by TobWen diff --git a/docs/source/markdown/podman-volume-prune.1.md b/docs/source/markdown/podman-volume-prune.1.md index 3dd585f3d22..95c9bcfabe0 100644 --- a/docs/source/markdown/podman-volume-prune.1.md +++ b/docs/source/markdown/podman-volume-prune.1.md @@ -12,6 +12,8 @@ Removes unused volumes. By default all unused volumes are removed, the **--filte be used to filter specific volumes. Users are prompted to confirm the removal of all the unused volumes. To bypass the confirmation, use the **--force** flag. +By default, pinned volumes are excluded from pruning to protect important data. Use the **--include-pinned** flag to include pinned volumes in the prune operation. + ## OPTIONS @@ -46,6 +48,10 @@ Do not prompt for confirmation. Print usage statement +#### **--include-pinned** + +Include pinned volumes in the prune operation. By default, pinned volumes are excluded from pruning to protect important data. + ## EXAMPLES @@ -66,7 +72,7 @@ $ podman volume prune --filter label=mylabel=mylabelvalue ``` ## SEE ALSO -**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)** +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-pin(1)](podman-volume-pin.1.md)** ## HISTORY November 2018, Originally compiled by Urvashi Mohnani diff --git a/docs/source/markdown/podman-volume-rm.1.md b/docs/source/markdown/podman-volume-rm.1.md index 5e9cceaf1a4..fbdc0f42e89 100644 --- a/docs/source/markdown/podman-volume-rm.1.md +++ b/docs/source/markdown/podman-volume-rm.1.md @@ -13,6 +13,8 @@ If a volume is being used by a container, an error is returned unless the **--fo flag is being used. To remove all volumes, use the **--all** flag. Volumes can be removed individually by providing their full name or a unique partial name. +By default, pinned volumes are excluded from removal operations to protect important data. Use the **--include-pinned** flag to allow removal of pinned volumes. + ## OPTIONS #### **--all**, **-a** @@ -28,6 +30,10 @@ If it is being used by containers, the containers are removed first. Print usage statement +#### **--include-pinned** + +Include pinned volumes in the removal operation. By default, pinned volumes are excluded from removal to protect important data. This flag must be used if you want to remove volumes that have been marked as pinned. + #### **--time**, **-t**=*seconds* Seconds to wait before forcibly stopping running containers that are using the specified volume. The --force option must be specified to use the --time option. Use -1 for infinite wait. @@ -59,7 +65,7 @@ $ podman volume rm --force myvol **125** The command fails for any other reason ## SEE ALSO -**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)** +**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-pin(1)](podman-volume-pin.1.md)** ## HISTORY November 2018, Originally compiled by Urvashi Mohnani diff --git a/docs/source/markdown/podman-volume.1.md b/docs/source/markdown/podman-volume.1.md index e94989eca3f..de35abe08ce 100644 --- a/docs/source/markdown/podman-volume.1.md +++ b/docs/source/markdown/podman-volume.1.md @@ -20,6 +20,7 @@ podman volume is a set of subcommands that manage volumes. | inspect | [podman-volume-inspect(1)](podman-volume-inspect.1.md) | Get detailed information on one or more 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. | +| pin | [podman-volume-pin(1)](podman-volume-pin.1.md) | Mark or unmark volumes as pinned. | | 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. | diff --git a/libpod/define/volume_inspect.go b/libpod/define/volume_inspect.go index ff880e221f8..e678d3d51e0 100644 --- a/libpod/define/volume_inspect.go +++ b/libpod/define/volume_inspect.go @@ -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 { diff --git a/libpod/reset.go b/libpod/reset.go index 60ccab1e497..b7343ac6058 100644 --- a/libpod/reset.go +++ b/libpod/reset.go @@ -94,7 +94,7 @@ func (r *Runtime) removeAllDirs() error { // Reset removes all Libpod files. // All containers, images, volumes, pods, and networks will be removed. // Calls Shutdown(), rendering the runtime unusable after this is run. -func (r *Runtime) Reset(ctx context.Context) error { +func (r *Runtime) Reset(ctx context.Context, includePinned bool) error { // Acquire the alive lock and hold it. // Ensures that we don't let other Podman commands run while we are // removing everything. @@ -164,6 +164,10 @@ func (r *Runtime) Reset(ctx context.Context) error { return err } for _, v := range volumes { + // Skip pinned volumes - they should not be removed during reset + if !includePinned && v.IsPinned() { + continue + } if err := r.RemoveVolume(ctx, v, true, &timeout); err != nil { if errors.Is(err, define.ErrNoSuchVolume) { continue diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go index dd9995454b6..0227c36ff5d 100644 --- a/libpod/runtime_volume.go +++ b/libpod/runtime_volume.go @@ -141,3 +141,40 @@ 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.IsPinned() && !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 +} diff --git a/libpod/volume.go b/libpod/volume.go index e388d7441f9..299db0b745a 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -112,6 +112,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 and reset operations by default + Pinned bool `json:"pinned,omitempty"` } // Name retrieves the volume's name @@ -280,6 +283,43 @@ func (v *Volume) UsesVolumeDriver() bool { return v.config.Driver != define.VolumeDriverLocal && v.config.Driver != "" } +// IsPinned returns whether this volume is marked as pinned. +// Pinned volumes are excluded from system prune and reset operations by default. +func (v *Volume) IsPinned() 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 and reset 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 + } + + // If the volume is already in the desired state, this is a no-op + if v.state.Pinned == pinned { + return nil + } + + v.state.Pinned = pinned + + return v.save() +} + func (v *Volume) Mount() (string, error) { v.lock.Lock() defer v.lock.Unlock() diff --git a/libpod/volume_inspect.go b/libpod/volume_inspect.go index 58790bb4494..a9ca5fe3344 100644 --- a/libpod/volume_inspect.go +++ b/libpod/volume_inspect.go @@ -72,5 +72,7 @@ func (v *Volume) Inspect() (*define.InspectVolumeData, error) { data.Timeout = v.runtime.config.Engine.VolumePluginTimeout } + data.Pinned = v.state.Pinned + return data, nil } diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index d1554c665a6..fe0693066a9 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -85,6 +85,15 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } + + // Set pinned status after volume creation if requested + if input.Pinned { + if err := vol.SetPinned(true); err != nil { + utils.InternalServerError(w, err) + return + } + } + inspectOut, err := vol.Inspect() if err != nil { utils.InternalServerError(w, err) @@ -160,7 +169,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 } @@ -173,8 +188,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 } @@ -190,6 +206,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.IsPinned() && !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) diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 30d645fa114..8a72b995c90 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -99,7 +99,7 @@ type ContainerEngine interface { //nolint:interfacebloat QuadletPrint(ctx context.Context, quadlet string) (string, error) QuadletRemove(ctx context.Context, quadlets []string, options QuadletRemoveOptions) (*QuadletRemoveReport, error) Renumber(ctx context.Context) error - Reset(ctx context.Context) error + Reset(ctx context.Context, options SystemResetOptions) error SetupRootless(ctx context.Context, noMoveProcess bool, cgroupMode string) error SecretCreate(ctx context.Context, name string, reader io.Reader, options SecretCreateOptions) (*SecretCreateReport, error) SecretInspect(ctx context.Context, nameOrIDs []string, options SecretInspectOptions) ([]*SecretInfoReport, []error, error) @@ -123,4 +123,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) } diff --git a/pkg/domain/entities/system.go b/pkg/domain/entities/system.go index 1b3703a7d18..d27557c9c98 100644 --- a/pkg/domain/entities/system.go +++ b/pkg/domain/entities/system.go @@ -10,6 +10,7 @@ type ( SystemPruneOptions = types.SystemPruneOptions SystemPruneReport = types.SystemPruneReport SystemMigrateOptions = types.SystemMigrateOptions + SystemResetOptions = types.SystemResetOptions SystemCheckOptions = types.SystemCheckOptions SystemCheckReport = types.SystemCheckReport SystemDfOptions = types.SystemDfOptions diff --git a/pkg/domain/entities/types/system.go b/pkg/domain/entities/types/system.go index 538b0d6eb98..8e4ef5dd6f6 100644 --- a/pkg/domain/entities/types/system.go +++ b/pkg/domain/entities/types/system.go @@ -42,11 +42,18 @@ type SystemCheckReport struct { // SystemPruneOptions provides options to prune system. type SystemPruneOptions struct { - All bool - Volume bool - Filters map[string][]string `json:"filters" schema:"filters"` - External bool - Build bool + All bool + Volume bool + Filters map[string][]string `json:"filters" schema:"filters"` + External bool + Build bool + VolumePruneOptions VolumePruneOptions `json:"volumePruneOptions" schema:"volumePruneOptions"` +} + +// VolumePruneOptions describes the options needed +// to prune a volume from the CLI +type VolumePruneOptions struct { + IncludePinned bool `json:"includePinned" schema:"includePinned"` } // SystemPruneReport provides report after system prune is executed. @@ -65,6 +72,11 @@ type SystemMigrateOptions struct { NewRuntime string } +// SystemResetOptions describes the options for resetting system storage +type SystemResetOptions struct { + IncludePinned bool +} + // SystemDfOptions describes the options for getting df information type SystemDfOptions struct { Format string diff --git a/pkg/domain/entities/types/volumes.go b/pkg/domain/entities/types/volumes.go index 12384a88151..590649be374 100644 --- a/pkg/domain/entities/types/volumes.go +++ b/pkg/domain/entities/types/volumes.go @@ -22,6 +22,10 @@ type VolumeCreateOptions struct { UID *int `schema:"uid"` // GID that the volume will be created as GID *int `schema:"gid"` + // Pinned indicates that this volume should be excluded from + // system prune and reset operations. This allows atomically + // creating and pinning a volume to avoid race conditions. + Pinned bool `schema:"pinned"` } type VolumeRmReport struct { diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go index 83d127c14a7..d3ee1f5e9dc 100644 --- a/pkg/domain/entities/volumes.go +++ b/pkg/domain/entities/volumes.go @@ -13,10 +13,11 @@ type VolumeCreateOptions = types.VolumeCreateOptions type VolumeConfigResponse = types.VolumeConfigResponse type VolumeRmOptions struct { - All bool - Force bool - Ignore bool - Timeout *uint + All bool + Force bool + Ignore bool + Timeout *uint + IncludePinned bool } type VolumeRmReport = types.VolumeRmReport @@ -26,7 +27,8 @@ type VolumeInspectReport = types.VolumeInspectReport // VolumePruneOptions describes the options needed // to prune a volume from the CLI type VolumePruneOptions struct { - Filters url.Values `json:"filters" schema:"filters"` + Filters url.Values `json:"filters" schema:"filters"` + IncludePinned bool `json:"includePinned" schema:"includePinned"` } type VolumeListOptions struct { @@ -54,3 +56,14 @@ type VolumeImportOptions struct { // Input will be closed upon being fully consumed Input io.Reader } + +// VolumePinOptions describes the options for pinning/unpinning volumes +type VolumePinOptions struct { + Unpin bool +} + +// VolumePinReport describes the response from pinning/unpinning a volume +type VolumePinReport struct { + Id string + Err error +} diff --git a/pkg/domain/filters/volumes.go b/pkg/domain/filters/volumes.go index e2cf8f2a9c4..ef0c0d3c772 100644 --- a/pkg/domain/filters/volumes.go +++ b/pkg/domain/filters/volumes.go @@ -52,6 +52,31 @@ func GenerateVolumeFilters(filter string, filterValues []string, runtime *libpod }, nil case "until": return createUntilFilterVolumeFunction(filterValues) + case "pinned": + for _, val := range filterValues { + switch strings.ToLower(val) { + case "true", "1", "false", "0": + default: + return nil, fmt.Errorf("%q is not a valid value for the \"pinned\" filter - must be true or false", val) + } + } + return func(v *libpod.Volume) bool { + for _, val := range filterValues { + pinned := v.IsPinned() + + switch strings.ToLower(val) { + case "true", "1": + if pinned { + return true + } + case "false", "0": + if !pinned { + return true + } + } + } + return false + }, nil case "dangling": for _, val := range filterValues { switch strings.ToLower(val) { @@ -101,6 +126,31 @@ func GeneratePruneVolumeFilters(filter string, filterValues []string, runtime *l }, nil case "until": return createUntilFilterVolumeFunction(filterValues) + case "pinned": + for _, val := range filterValues { + switch strings.ToLower(val) { + case "true", "1", "false", "0": + default: + return nil, fmt.Errorf("%q is not a valid value for the \"pinned\" filter - must be true or false", val) + } + } + return func(v *libpod.Volume) bool { + for _, val := range filterValues { + pinned := v.IsPinned() + + switch strings.ToLower(val) { + case "true", "1": + if pinned { + return true + } + case "false", "0": + if !pinned { + return true + } + } + } + return false + }, nil } return nil, fmt.Errorf("%q is an invalid volume filter", filter) } diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index 6655341672e..4d0e805dfef 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -296,8 +296,8 @@ func (ic *ContainerEngine) SystemDf(ctx context.Context, _ entities.SystemDfOpti }, nil } -func (ic *ContainerEngine) Reset(ctx context.Context) error { - return ic.Libpod.Reset(ctx) +func (ic *ContainerEngine) Reset(ctx context.Context, options entities.SystemResetOptions) error { + return ic.Libpod.Reset(ctx, options.IncludePinned) } func (ic *ContainerEngine) Renumber(_ context.Context) error { diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index ea9a8fdfcb8..46aeac4777f 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -50,6 +50,14 @@ func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.Volum if err != nil { return nil, err } + + // Set pinned status after volume creation if requested + if opts.Pinned { + if err := vol.SetPinned(true); err != nil { + return nil, err + } + } + return &entities.IDOrNameResponse{IDOrName: vol.Name()}, nil } @@ -82,6 +90,15 @@ func (ic *ContainerEngine) VolumeRm(ctx context.Context, namesOrIds []string, op } } for _, vol := range vols { + // Check if volume is pinned and --include-pinned flag is not set + if vol.IsPinned() && !opts.IncludePinned { + reports = append(reports, &entities.VolumeRmReport{ + Err: fmt.Errorf("volume %s is pinned and cannot be removed without --include-pinned flag", vol.Name()), + Id: vol.Name(), + }) + continue + } + reports = append(reports, &entities.VolumeRmReport{ Err: ic.Libpod.RemoveVolume(ctx, vol, opts.Force, opts.Timeout), Id: vol.Name(), @@ -141,11 +158,11 @@ func (ic *ContainerEngine) VolumePrune(ctx context.Context, options entities.Vol } funcs = append(funcs, filterFunc) } - return ic.pruneVolumesHelper(ctx, funcs) + return ic.pruneVolumesHelper(ctx, funcs, options.IncludePinned) } -func (ic *ContainerEngine) pruneVolumesHelper(ctx context.Context, filterFuncs []libpod.VolumeFilter) ([]*reports.PruneReport, error) { - pruned, err := ic.Libpod.PruneVolumes(ctx, filterFuncs) +func (ic *ContainerEngine) pruneVolumesHelper(ctx context.Context, filterFuncs []libpod.VolumeFilter, includePinned bool) ([]*reports.PruneReport, error) { + pruned, err := ic.Libpod.PruneVolumesWithOptions(ctx, filterFuncs, includePinned) if err != nil { return nil, err } @@ -265,6 +282,25 @@ func (ic *ContainerEngine) VolumeExport(_ context.Context, nameOrID string, opti return nil } +func (ic *ContainerEngine) VolumePin(_ context.Context, namesOrIds []string, opts entities.VolumePinOptions) ([]*entities.VolumePinReport, error) { + reports := make([]*entities.VolumePinReport, 0, len(namesOrIds)) + + for _, nameOrId := range namesOrIds { + report := &entities.VolumePinReport{Id: nameOrId} + + vol, err := ic.Libpod.LookupVolume(nameOrId) + if err != nil { + report.Err = err + } else { + report.Err = vol.SetPinned(!opts.Unpin) + } + + reports = append(reports, report) + } + + return reports, nil +} + func (ic *ContainerEngine) VolumeImport(_ context.Context, nameOrID string, options entities.VolumeImportOptions) error { vol, err := ic.Libpod.LookupVolume(nameOrID) if err != nil { diff --git a/pkg/domain/infra/tunnel/system.go b/pkg/domain/infra/tunnel/system.go index e7aefe90dc7..57d3aa33fe4 100644 --- a/pkg/domain/infra/tunnel/system.go +++ b/pkg/domain/infra/tunnel/system.go @@ -40,7 +40,7 @@ func (ic *ContainerEngine) Renumber(_ context.Context) error { return errors.New("lock renumbering is not supported on remote clients") } -func (ic *ContainerEngine) Reset(_ context.Context) error { +func (ic *ContainerEngine) Reset(_ context.Context, _ entities.SystemResetOptions) error { return errors.New("system reset is not supported on remote clients") } diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index d78a42143fd..73bdb519e67 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -121,3 +121,15 @@ func (ic *ContainerEngine) VolumeExport(_ context.Context, nameOrID string, opti func (ic *ContainerEngine) VolumeImport(_ context.Context, nameOrID string, options entities.VolumeImportOptions) error { return volumes.Import(ic.ClientCtx, nameOrID, options.Input) } + +func (ic *ContainerEngine) VolumePin(_ context.Context, namesOrIds []string, _ entities.VolumePinOptions) ([]*entities.VolumePinReport, error) { + reports := make([]*entities.VolumePinReport, 0, len(namesOrIds)) + for _, nameOrId := range namesOrIds { + report := &entities.VolumePinReport{ + Id: nameOrId, + Err: errors.New("volume pinning is not supported for remote clients"), + } + reports = append(reports, report) + } + return reports, nil +}