From d8c7b93dd5e2fe21f2ad7d7dfb88981824bc6c9a Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:39 +0200 Subject: [PATCH 01/29] Add Protected field to volume configuration and data structures Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/define/volume_inspect.go | 3 +++ libpod/volume.go | 3 +++ libpod/volume_inspect.go | 2 ++ 3 files changed, 8 insertions(+) diff --git a/libpod/define/volume_inspect.go b/libpod/define/volume_inspect.go index ff880e221f8..fd809baefd7 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 + // Protected indicates that this volume should be excluded from + // system prune operations by default. + Protected bool `json:"Protected,omitempty"` } type VolumeReload struct { diff --git a/libpod/volume.go b/libpod/volume.go index e388d7441f9..2005338b977 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -77,6 +77,9 @@ type VolumeConfig struct { StorageImageID string `json:"storageImageID,omitempty"` // MountLabel is the SELinux label to assign to mount points MountLabel string `json:"mountlabel,omitempty"` + // Protected indicates that this volume should be excluded from + // system prune operations by default + Protected bool `json:"protected,omitempty"` } // VolumeState holds the volume's mutable state. diff --git a/libpod/volume_inspect.go b/libpod/volume_inspect.go index 58790bb4494..d9d7c26ac68 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.Protected = v.config.Protected + return data, nil } From e47cc0778ded907de5e8b3122c463aef28f3af04 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:39 +0200 Subject: [PATCH 02/29] Implement volume protection methods and creation options Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/options.go | 14 ++++++++++++++ libpod/volume.go | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/libpod/options.go b/libpod/options.go index 41d6bc4a78c..f07a59a8c13 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1744,6 +1744,20 @@ func withSetAnon() VolumeCreateOption { } } +// WithVolumeProtected sets the protected flag for the volume. +// Protected volumes are excluded from system prune operations by default. +func WithVolumeProtected() VolumeCreateOption { + return func(volume *Volume) error { + if volume.valid { + return define.ErrVolumeFinalized + } + + volume.config.Protected = 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 { diff --git a/libpod/volume.go b/libpod/volume.go index 2005338b977..16ec0622d15 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -283,6 +283,31 @@ func (v *Volume) UsesVolumeDriver() bool { return v.config.Driver != define.VolumeDriverLocal && v.config.Driver != "" } +// Protected returns whether this volume is marked as protected. +// Protected volumes are excluded from system prune operations by default. +func (v *Volume) Protected() bool { + return v.config.Protected +} + +// SetProtected sets the protected status of the volume. +// Protected volumes are excluded from system prune operations by default. +func (v *Volume) SetProtected(protected bool) error { + if !v.valid { + return define.ErrVolumeRemoved + } + + v.lock.Lock() + defer v.lock.Unlock() + + if err := v.update(); err != nil { + return err + } + + v.config.Protected = protected + + return v.save() +} + func (v *Volume) Mount() (string, error) { v.lock.Lock() defer v.lock.Unlock() From 0e4d810ff143a85d64e89049702bc05e5592bf67 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:40 +0200 Subject: [PATCH 03/29] Add runtime methods for protected volume handling and pruning Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/runtime_volume.go | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go index dd9995454b6..801383d5b2c 100644 --- a/libpod/runtime_volume.go +++ b/libpod/runtime_volume.go @@ -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, includeProtected 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 protected volumes unless explicitly requested + if vol.Protected() && !includeProtected { + 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 +} + +// SetVolumeProtected sets the protected status of a volume by name. +// Protected volumes are excluded from system prune operations by default. +func (r *Runtime) SetVolumeProtected(volumeName string, protected bool) error { + if !r.valid { + return define.ErrRuntimeStopped + } + + vol, err := r.state.Volume(volumeName) + if err != nil { + return err + } + + return vol.SetProtected(protected) +} From 582a324444cdf1a31cff35e1f550dc130246a814 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:40 +0200 Subject: [PATCH 04/29] Add protected volume support to entity types and interfaces Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- pkg/domain/entities/engine_container.go | 1 + pkg/domain/entities/types/system.go | 17 ++++++++++++----- pkg/domain/entities/types/volumes.go | 3 +++ pkg/domain/entities/volumes.go | 23 ++++++++++++++++++----- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 30d645fa114..48144018c6a 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -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 + VolumeProtect(ctx context.Context, namesOrIds []string, opts VolumeProtectOptions) ([]*VolumeProtectReport, error) } diff --git a/pkg/domain/entities/types/system.go b/pkg/domain/entities/types/system.go index 538b0d6eb98..3df42c1d386 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 { + IncludeProtected bool `json:"includeProtected" schema:"includeProtected"` } // SystemPruneReport provides report after system prune is executed. diff --git a/pkg/domain/entities/types/volumes.go b/pkg/domain/entities/types/volumes.go index 12384a88151..973af52b55f 100644 --- a/pkg/domain/entities/types/volumes.go +++ b/pkg/domain/entities/types/volumes.go @@ -22,6 +22,9 @@ type VolumeCreateOptions struct { UID *int `schema:"uid"` // GID that the volume will be created as GID *int `schema:"gid"` + // Protected indicates that this volume should be excluded from + // system prune operations by default + Protected bool `schema:"protected"` } type VolumeRmReport struct { diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go index 83d127c14a7..ed422e59ef3 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 + IncludeProtected 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"` + IncludeProtected bool `json:"includeProtected" schema:"includeProtected"` } type VolumeListOptions struct { @@ -54,3 +56,14 @@ type VolumeImportOptions struct { // Input will be closed upon being fully consumed Input io.Reader } + +// VolumeProtectOptions describes the options for protecting/unprotecting volumes +type VolumeProtectOptions struct { + Unprotect bool +} + +// VolumeProtectReport describes the response from protecting/unprotecting a volume +type VolumeProtectReport struct { + Id string + Err error +} From 5093a24ac0cc47779b1733a040c081d8f09a3525 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:40 +0200 Subject: [PATCH 05/29] Implement protected volume filtering support Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- pkg/domain/filters/volumes.go | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pkg/domain/filters/volumes.go b/pkg/domain/filters/volumes.go index e2cf8f2a9c4..2e2001dbda8 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 "protected": + 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 \"protected\" filter - must be true or false", val) + } + } + return func(v *libpod.Volume) bool { + for _, val := range filterValues { + protected := v.Protected() + + switch strings.ToLower(val) { + case "true", "1": + if protected { + return true + } + case "false", "0": + if !protected { + 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 "protected": + 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 \"protected\" filter - must be true or false", val) + } + } + return func(v *libpod.Volume) bool { + for _, val := range filterValues { + protected := v.Protected() + + switch strings.ToLower(val) { + case "true", "1": + if protected { + return true + } + case "false", "0": + if !protected { + return true + } + } + } + return false + }, nil } return nil, fmt.Errorf("%q is an invalid volume filter", filter) } From 6c9fab0f795b138effda83d8c06e9e644bf73571 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:41 +0200 Subject: [PATCH 06/29] Add protected volume support to ABI implementation Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- pkg/domain/infra/abi/volumes.go | 35 ++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index ea9a8fdfcb8..6da2446b25e 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -46,6 +46,10 @@ func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.Volum volumeOptions = append(volumeOptions, libpod.WithVolumeGID(*opts.GID), libpod.WithVolumeNoChown()) } + if opts.Protected { + volumeOptions = append(volumeOptions, libpod.WithVolumeProtected()) + } + vol, err := ic.Libpod.NewVolume(ctx, volumeOptions...) if err != nil { return nil, err @@ -82,6 +86,15 @@ func (ic *ContainerEngine) VolumeRm(ctx context.Context, namesOrIds []string, op } } for _, vol := range vols { + // Check if volume is protected and --include-protected flag is not set + if vol.Protected() && !opts.IncludeProtected { + reports = append(reports, &entities.VolumeRmReport{ + Err: fmt.Errorf("volume %s is protected and cannot be removed without --include-protected 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 +154,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.IncludeProtected) } -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, includeProtected bool) ([]*reports.PruneReport, error) { + pruned, err := ic.Libpod.PruneVolumesWithOptions(ctx, filterFuncs, includeProtected) if err != nil { return nil, err } @@ -265,6 +278,22 @@ func (ic *ContainerEngine) VolumeExport(_ context.Context, nameOrID string, opti return nil } +func (ic *ContainerEngine) VolumeProtect(ctx context.Context, namesOrIds []string, opts entities.VolumeProtectOptions) ([]*entities.VolumeProtectReport, error) { + var reports []*entities.VolumeProtectReport + + for _, nameOrId := range namesOrIds { + report := &entities.VolumeProtectReport{Id: nameOrId} + + if err := ic.Libpod.SetVolumeProtected(nameOrId, !opts.Unprotect); err != nil { + report.Err = err + } + + 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 { From 26df32dd7356584608a7d7b7affbbe10902cba6c Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:41 +0200 Subject: [PATCH 07/29] Add HTTP API support for protected volumes Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- pkg/api/handlers/libpod/volumes.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index d1554c665a6..910d502afc4 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -80,6 +80,10 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { volumeOptions = append(volumeOptions, libpod.WithVolumeGID(*input.GID), libpod.WithVolumeNoChown()) } + if input.Protected { + volumeOptions = append(volumeOptions, libpod.WithVolumeProtected()) + } + vol, err := runtime.NewVolume(r.Context(), volumeOptions...) if err != nil { utils.InternalServerError(w, err) @@ -160,7 +164,13 @@ func pruneVolumesHelper(r *http.Request) ([]*reports.PruneReport, error) { filterFuncs = append(filterFuncs, filterFunc) } - reports, err := runtime.PruneVolumes(r.Context(), filterFuncs) + // Check for includeProtected parameter + includeProtected := false + if includeParam := r.URL.Query().Get("includeProtected"); includeParam == "true" { + includeProtected = true + } + + reports, err := runtime.PruneVolumesWithOptions(r.Context(), filterFuncs, includeProtected) if err != nil { return nil, err } @@ -173,8 +183,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"` + IncludeProtected bool `schema:"includeProtected"` }{ // override any golang type defaults } @@ -190,6 +201,13 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { utils.VolumeNotFound(w, name, err) return } + // Check if volume is protected and --include-protected flag is not set + if vol.Protected() && !query.IncludeProtected { + utils.Error(w, http.StatusBadRequest, + fmt.Errorf("volume %s is protected and cannot be removed without includeProtected=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) From b3385301def3c708c103f01c2a9ec7f84f9d5308 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:42 +0200 Subject: [PATCH 08/29] Add --protected flag to volume create command Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/volumes/create.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cmd/podman/volumes/create.go b/cmd/podman/volumes/create.go index 78c09048949..6e01a3c9eb1 100644 --- a/cmd/podman/volumes/create.go +++ b/cmd/podman/volumes/create.go @@ -31,11 +31,12 @@ var ( var ( createOpts = entities.VolumeCreateOptions{} opts = struct { - Label []string - Opts []string - Ignore bool - UID int - GID int + Label []string + Opts []string + Ignore bool + UID int + GID int + Protected 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) + + protectedFlagName := "protected" + flags.BoolVar(&opts.Protected, protectedFlagName, false, "Mark volume as protected (excluded from system prune by default)") + _ = createCommand.RegisterFlagCompletionFunc(protectedFlagName, 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.Protected = opts.Protected response, err := registry.ContainerEngine().VolumeCreate(context.Background(), createOpts) if err != nil { return err From c15a40f685892d09e04ce9fe6042ee5cd99b1e75 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:42 +0200 Subject: [PATCH 09/29] Add volume protect/unprotect command Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/volumes/protect.go | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 cmd/podman/volumes/protect.go diff --git a/cmd/podman/volumes/protect.go b/cmd/podman/volumes/protect.go new file mode 100644 index 00000000000..d1f50a74cdc --- /dev/null +++ b/cmd/podman/volumes/protect.go @@ -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 ( + protectDescription = `Mark or unmark a volume as protected. + +Protected volumes are excluded from system prune operations by default.` + + protectCommand = &cobra.Command{ + Use: "protect [options] VOLUME [VOLUME...]", + Short: "Mark or unmark volume as protected", + Long: protectDescription, + RunE: protect, + ValidArgsFunction: common.AutocompleteVolumes, + Example: `podman volume protect myvol + podman volume protect --unprotect myvol + podman volume protect vol1 vol2 vol3`, + } +) + +var ( + protectOptions = entities.VolumeProtectOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: protectCommand, + Parent: volumeCmd, + }) + flags := protectCommand.Flags() + flags.BoolVar(&protectOptions.Unprotect, "unprotect", false, "Remove protection from volume") +} + +func protect(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("must specify at least one volume name") + } + + responses, err := registry.ContainerEngine().VolumeProtect(context.Background(), args, protectOptions) + if err != nil { + return err + } + + for _, r := range responses { + if r.Err != nil { + fmt.Printf("Error protecting volume %s: %v\n", r.Id, r.Err) + } else { + if protectOptions.Unprotect { + fmt.Printf("Volume %s is now unprotected\n", r.Id) + } else { + fmt.Printf("Volume %s is now protected\n", r.Id) + } + } + } + + return nil +} From 2a52a0e09432b138d74e5e1e01dba01c2dc9891b Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:43 +0200 Subject: [PATCH 10/29] Add --include-protected flag to volume rm command Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/volumes/rm.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/podman/volumes/rm.go b/cmd/podman/volumes/rm.go index e4da2b02a34..9b694bf985e 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 + includeProtected 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(&includeProtected, "include-protected", false, "Include protected 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.IncludeProtected = includeProtected 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) } From a719edd15c36a371077a20da5a720c3325af748f Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:21:43 +0200 Subject: [PATCH 11/29] Add protected volume support to system prune command Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/system/prune.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go index f5d44eb2598..95d317a9800 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 + includeProtected 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(&includeProtected, "include-protected", false, "Include protected volumes in prune operation") filterFlagName := "filter" flags.StringArrayVar(&filters, filterFlagName, []string{}, "Provide filter values (e.g. 'label==')") _ = pruneCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePruneFilters) @@ -86,6 +88,11 @@ func prune(_ *cobra.Command, _ []string) error { if err != nil { return err } + + // Set the include protected flag for volume pruning + if pruneOptions.Volume { + pruneOptions.VolumePruneOptions.IncludeProtected = includeProtected + } response, err := registry.ContainerEngine().SystemPrune(context.Background(), pruneOptions) if err != nil { @@ -126,6 +133,11 @@ func prune(_ *cobra.Command, _ []string) error { } func createPruneWarningMessage(pruneOpts entities.SystemPruneOptions) string { + protectedNote := "" + if pruneOpts.Volume && !pruneOpts.VolumePruneOptions.IncludeProtected { + protectedNote = " (excluding protected volumes)" + } + if pruneOpts.All { return `WARNING! This command removes: - all stopped containers @@ -137,7 +149,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` + protectedNote + ` - all dangling images - all dangling build cache From a69f4077e15a927a2f024d29453e968c8a6d63fd Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:17:14 +0200 Subject: [PATCH 12/29] Add VolumeProtect stub implementation for tunnel/remote clients Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- pkg/domain/infra/tunnel/volumes.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index d78a42143fd..24d32192dd4 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) VolumeProtect(ctx context.Context, namesOrIds []string, opts entities.VolumeProtectOptions) ([]*entities.VolumeProtectReport, error) { + reports := make([]*entities.VolumeProtectReport, 0, len(namesOrIds)) + for _, nameOrId := range namesOrIds { + report := &entities.VolumeProtectReport{ + Id: nameOrId, + Err: errors.New("volume protection is not supported for remote clients"), + } + reports = append(reports, report) + } + return reports, nil +} From 3e91c2b75568fabab92e27792f81cd89da82c0e0 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:17:14 +0200 Subject: [PATCH 13/29] Fix volume pruning to respect include protected flag in system prune Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/system/prune.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go index 95d317a9800..bd1fbd3fbf0 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -82,6 +82,11 @@ func prune(_ *cobra.Command, _ []string) error { return nil } } + + // Set the include protected flag for volume pruning + if pruneOptions.Volume { + pruneOptions.VolumePruneOptions.IncludeProtected = includeProtected + } // Remove all unused pods, containers, images, networks, and volume data. pruneOptions.Filters, err = parse.FilterArgumentsIntoFilters(filters) From 13659c63b4d843a0da91c533b6ebfd8706b2e594 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:46:21 +0200 Subject: [PATCH 14/29] Rename term "protected" to "pinned" see discussion in https://github.com/containers/podman/issues/23217 Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/system/prune.go | 22 ++++----- cmd/podman/volumes/create.go | 20 ++++---- cmd/podman/volumes/pin.go | 66 +++++++++++++++++++++++++ cmd/podman/volumes/protect.go | 66 ------------------------- cmd/podman/volumes/rm.go | 10 ++-- libpod/define/volume_inspect.go | 4 +- libpod/options.go | 8 +-- libpod/runtime_volume.go | 14 +++--- libpod/volume.go | 20 ++++---- libpod/volume_inspect.go | 2 +- pkg/api/handlers/libpod/volumes.go | 26 +++++----- pkg/domain/entities/engine_container.go | 2 +- pkg/domain/entities/types/system.go | 2 +- pkg/domain/entities/types/volumes.go | 4 +- pkg/domain/entities/volumes.go | 24 ++++----- pkg/domain/filters/volumes.go | 20 ++++---- pkg/domain/infra/abi/volumes.go | 24 ++++----- pkg/domain/infra/tunnel/volumes.go | 8 +-- 18 files changed, 171 insertions(+), 171 deletions(-) create mode 100644 cmd/podman/volumes/pin.go delete mode 100644 cmd/podman/volumes/protect.go diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go index bd1fbd3fbf0..aba077649bb 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -36,8 +36,8 @@ var ( ValidArgsFunction: completion.AutocompleteNone, Example: `podman system prune`, } - force bool - includeProtected bool + force bool + includePinned bool ) func init() { @@ -51,7 +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(&includeProtected, "include-protected", false, "Include protected volumes in prune operation") + 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) @@ -83,9 +83,9 @@ func prune(_ *cobra.Command, _ []string) error { } } - // Set the include protected flag for volume pruning + // Set the include pinned flag for volume pruning if pruneOptions.Volume { - pruneOptions.VolumePruneOptions.IncludeProtected = includeProtected + pruneOptions.VolumePruneOptions.IncludePinned = includePinned } // Remove all unused pods, containers, images, networks, and volume data. @@ -94,9 +94,9 @@ func prune(_ *cobra.Command, _ []string) error { return err } - // Set the include protected flag for volume pruning + // Set the include pinned flag for volume pruning if pruneOptions.Volume { - pruneOptions.VolumePruneOptions.IncludeProtected = includeProtected + pruneOptions.VolumePruneOptions.IncludePinned = includePinned } response, err := registry.ContainerEngine().SystemPrune(context.Background(), pruneOptions) @@ -138,9 +138,9 @@ func prune(_ *cobra.Command, _ []string) error { } func createPruneWarningMessage(pruneOpts entities.SystemPruneOptions) string { - protectedNote := "" - if pruneOpts.Volume && !pruneOpts.VolumePruneOptions.IncludeProtected { - protectedNote = " (excluding protected volumes)" + pinnedNote := "" + if pruneOpts.Volume && !pruneOpts.VolumePruneOptions.IncludePinned { + pinnedNote = " (excluding pinned volumes)" } if pruneOpts.All { @@ -154,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` + protectedNote + ` + - all networks not used by at least one container%s%s` + pinnedNote + ` - all dangling images - all dangling build cache diff --git a/cmd/podman/volumes/create.go b/cmd/podman/volumes/create.go index 6e01a3c9eb1..d10b31a0f6a 100644 --- a/cmd/podman/volumes/create.go +++ b/cmd/podman/volumes/create.go @@ -31,12 +31,12 @@ var ( var ( createOpts = entities.VolumeCreateOptions{} opts = struct { - Label []string - Opts []string - Ignore bool - UID int - GID int - Protected bool + Label []string + Opts []string + Ignore bool + UID int + GID int + Pinned bool }{} ) @@ -70,9 +70,9 @@ func init() { flags.IntVar(&opts.GID, gidFlagName, 0, "Set the GID of the volume owner") _ = createCommand.RegisterFlagCompletionFunc(gidFlagName, completion.AutocompleteNone) - protectedFlagName := "protected" - flags.BoolVar(&opts.Protected, protectedFlagName, false, "Mark volume as protected (excluded from system prune by default)") - _ = createCommand.RegisterFlagCompletionFunc(protectedFlagName, 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 { @@ -97,7 +97,7 @@ func create(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("gid") { createOpts.GID = &opts.GID } - createOpts.Protected = opts.Protected + 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..6f3a2d356c2 --- /dev/null +++ b/cmd/podman/volumes/pin.go @@ -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 +} diff --git a/cmd/podman/volumes/protect.go b/cmd/podman/volumes/protect.go deleted file mode 100644 index d1f50a74cdc..00000000000 --- a/cmd/podman/volumes/protect.go +++ /dev/null @@ -1,66 +0,0 @@ -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 ( - protectDescription = `Mark or unmark a volume as protected. - -Protected volumes are excluded from system prune operations by default.` - - protectCommand = &cobra.Command{ - Use: "protect [options] VOLUME [VOLUME...]", - Short: "Mark or unmark volume as protected", - Long: protectDescription, - RunE: protect, - ValidArgsFunction: common.AutocompleteVolumes, - Example: `podman volume protect myvol - podman volume protect --unprotect myvol - podman volume protect vol1 vol2 vol3`, - } -) - -var ( - protectOptions = entities.VolumeProtectOptions{} -) - -func init() { - registry.Commands = append(registry.Commands, registry.CliCommand{ - Command: protectCommand, - Parent: volumeCmd, - }) - flags := protectCommand.Flags() - flags.BoolVar(&protectOptions.Unprotect, "unprotect", false, "Remove protection from volume") -} - -func protect(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("must specify at least one volume name") - } - - responses, err := registry.ContainerEngine().VolumeProtect(context.Background(), args, protectOptions) - if err != nil { - return err - } - - for _, r := range responses { - if r.Err != nil { - fmt.Printf("Error protecting volume %s: %v\n", r.Id, r.Err) - } else { - if protectOptions.Unprotect { - fmt.Printf("Volume %s is now unprotected\n", r.Id) - } else { - fmt.Printf("Volume %s is now protected\n", r.Id) - } - } - } - - return nil -} diff --git a/cmd/podman/volumes/rm.go b/cmd/podman/volumes/rm.go index 9b694bf985e..208cf3a1dcf 100644 --- a/cmd/podman/volumes/rm.go +++ b/cmd/podman/volumes/rm.go @@ -33,9 +33,9 @@ var ( ) var ( - rmOptions = entities.VolumeRmOptions{} - stopTimeout int - includeProtected bool + rmOptions = entities.VolumeRmOptions{} + stopTimeout int + includePinned bool ) func init() { @@ -46,7 +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(&includeProtected, "include-protected", false, "Include protected volumes in removal operation") + 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) @@ -64,7 +64,7 @@ func rm(cmd *cobra.Command, args []string) error { timeout := uint(stopTimeout) rmOptions.Timeout = &timeout } - rmOptions.IncludeProtected = includeProtected + 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()) { diff --git a/libpod/define/volume_inspect.go b/libpod/define/volume_inspect.go index fd809baefd7..e678d3d51e0 100644 --- a/libpod/define/volume_inspect.go +++ b/libpod/define/volume_inspect.go @@ -63,9 +63,9 @@ type InspectVolumeData struct { StorageID string `json:"StorageID,omitempty"` // LockNumber is the number of the volume's Libpod lock. LockNumber uint32 - // Protected indicates that this volume should be excluded from + // Pinned indicates that this volume should be excluded from // system prune operations by default. - Protected bool `json:"Protected,omitempty"` + Pinned bool `json:"Pinned,omitempty"` } type VolumeReload struct { diff --git a/libpod/options.go b/libpod/options.go index f07a59a8c13..51219f0af0f 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1744,15 +1744,15 @@ func withSetAnon() VolumeCreateOption { } } -// WithVolumeProtected sets the protected flag for the volume. -// Protected volumes are excluded from system prune operations by default. -func WithVolumeProtected() 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.config.Protected = true + volume.config.Pinned = true return nil } diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go index 801383d5b2c..f66ae1794fb 100644 --- a/libpod/runtime_volume.go +++ b/libpod/runtime_volume.go @@ -143,7 +143,7 @@ func (r *Runtime) PruneVolumes(ctx context.Context, filterFuncs []VolumeFilter) } // PruneVolumesWithOptions removes unused volumes from the system with options -func (r *Runtime) PruneVolumesWithOptions(ctx context.Context, filterFuncs []VolumeFilter, includeProtected bool) ([]*reports.PruneReport, error) { +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 { @@ -151,8 +151,8 @@ func (r *Runtime) PruneVolumesWithOptions(ctx context.Context, filterFuncs []Vol } for _, vol := range vols { - // Skip protected volumes unless explicitly requested - if vol.Protected() && !includeProtected { + // Skip pinned volumes unless explicitly requested + if vol.Pinned() && !includePinned { continue } @@ -179,9 +179,9 @@ func (r *Runtime) PruneVolumesWithOptions(ctx context.Context, filterFuncs []Vol return preports, nil } -// SetVolumeProtected sets the protected status of a volume by name. -// Protected volumes are excluded from system prune operations by default. -func (r *Runtime) SetVolumeProtected(volumeName string, protected bool) error { +// 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 } @@ -191,5 +191,5 @@ func (r *Runtime) SetVolumeProtected(volumeName string, protected bool) error { return err } - return vol.SetProtected(protected) + return vol.SetPinned(pinned) } diff --git a/libpod/volume.go b/libpod/volume.go index 16ec0622d15..906b5e72e73 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -77,9 +77,9 @@ type VolumeConfig struct { StorageImageID string `json:"storageImageID,omitempty"` // MountLabel is the SELinux label to assign to mount points MountLabel string `json:"mountlabel,omitempty"` - // Protected indicates that this volume should be excluded from + // Pinned indicates that this volume should be excluded from // system prune operations by default - Protected bool `json:"protected,omitempty"` + Pinned bool `json:"pinned,omitempty"` } // VolumeState holds the volume's mutable state. @@ -283,15 +283,15 @@ func (v *Volume) UsesVolumeDriver() bool { return v.config.Driver != define.VolumeDriverLocal && v.config.Driver != "" } -// Protected returns whether this volume is marked as protected. -// Protected volumes are excluded from system prune operations by default. -func (v *Volume) Protected() bool { - return v.config.Protected +// Pinned returns whether this volume is marked as pinned. +// Pinned volumes are excluded from system prune operations by default. +func (v *Volume) Pinned() bool { + return v.config.Pinned } -// SetProtected sets the protected status of the volume. -// Protected volumes are excluded from system prune operations by default. -func (v *Volume) SetProtected(protected bool) error { +// 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 } @@ -303,7 +303,7 @@ func (v *Volume) SetProtected(protected bool) error { return err } - v.config.Protected = protected + v.config.Pinned = pinned return v.save() } diff --git a/libpod/volume_inspect.go b/libpod/volume_inspect.go index d9d7c26ac68..dc743d23683 100644 --- a/libpod/volume_inspect.go +++ b/libpod/volume_inspect.go @@ -72,7 +72,7 @@ func (v *Volume) Inspect() (*define.InspectVolumeData, error) { data.Timeout = v.runtime.config.Engine.VolumePluginTimeout } - data.Protected = v.config.Protected + data.Pinned = v.config.Pinned return data, nil } diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 910d502afc4..154832be568 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -80,8 +80,8 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { volumeOptions = append(volumeOptions, libpod.WithVolumeGID(*input.GID), libpod.WithVolumeNoChown()) } - if input.Protected { - volumeOptions = append(volumeOptions, libpod.WithVolumeProtected()) + if input.Pinned { + volumeOptions = append(volumeOptions, libpod.WithVolumePinned()) } vol, err := runtime.NewVolume(r.Context(), volumeOptions...) @@ -164,13 +164,13 @@ func pruneVolumesHelper(r *http.Request) ([]*reports.PruneReport, error) { filterFuncs = append(filterFuncs, filterFunc) } - // Check for includeProtected parameter - includeProtected := false - if includeParam := r.URL.Query().Get("includeProtected"); includeParam == "true" { - includeProtected = true + // Check for includePinned parameter + includePinned := false + if includeParam := r.URL.Query().Get("includePinned"); includeParam == "true" { + includePinned = true } - reports, err := runtime.PruneVolumesWithOptions(r.Context(), filterFuncs, includeProtected) + reports, err := runtime.PruneVolumesWithOptions(r.Context(), filterFuncs, includePinned) if err != nil { return nil, err } @@ -183,9 +183,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"` - IncludeProtected bool `schema:"includeProtected"` + Force bool `schema:"force"` + Timeout *uint `schema:"timeout"` + IncludePinned bool `schema:"includePinned"` }{ // override any golang type defaults } @@ -201,10 +201,10 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { utils.VolumeNotFound(w, name, err) return } - // Check if volume is protected and --include-protected flag is not set - if vol.Protected() && !query.IncludeProtected { + // 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 protected and cannot be removed without includeProtected=true parameter", vol.Name())) + fmt.Errorf("volume %s is pinned and cannot be removed without includePinned=true parameter", vol.Name())) return } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 48144018c6a..476a485456f 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -123,5 +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 - VolumeProtect(ctx context.Context, namesOrIds []string, opts VolumeProtectOptions) ([]*VolumeProtectReport, error) + VolumePin(ctx context.Context, namesOrIds []string, opts VolumePinOptions) ([]*VolumePinReport, error) } diff --git a/pkg/domain/entities/types/system.go b/pkg/domain/entities/types/system.go index 3df42c1d386..4dca8f1963a 100644 --- a/pkg/domain/entities/types/system.go +++ b/pkg/domain/entities/types/system.go @@ -53,7 +53,7 @@ type SystemPruneOptions struct { // VolumePruneOptions describes the options needed // to prune a volume from the CLI type VolumePruneOptions struct { - IncludeProtected bool `json:"includeProtected" schema:"includeProtected"` + IncludePinned bool `json:"includePinned" schema:"includePinned"` } // SystemPruneReport provides report after system prune is executed. diff --git a/pkg/domain/entities/types/volumes.go b/pkg/domain/entities/types/volumes.go index 973af52b55f..cf0e00a0589 100644 --- a/pkg/domain/entities/types/volumes.go +++ b/pkg/domain/entities/types/volumes.go @@ -22,9 +22,9 @@ type VolumeCreateOptions struct { UID *int `schema:"uid"` // GID that the volume will be created as GID *int `schema:"gid"` - // Protected indicates that this volume should be excluded from + // Pinned indicates that this volume should be excluded from // system prune operations by default - Protected bool `schema:"protected"` + Pinned bool `schema:"pinned"` } type VolumeRmReport struct { diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go index ed422e59ef3..d3ee1f5e9dc 100644 --- a/pkg/domain/entities/volumes.go +++ b/pkg/domain/entities/volumes.go @@ -13,11 +13,11 @@ type VolumeCreateOptions = types.VolumeCreateOptions type VolumeConfigResponse = types.VolumeConfigResponse type VolumeRmOptions struct { - All bool - Force bool - Ignore bool - Timeout *uint - IncludeProtected bool + All bool + Force bool + Ignore bool + Timeout *uint + IncludePinned bool } type VolumeRmReport = types.VolumeRmReport @@ -27,8 +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"` - IncludeProtected bool `json:"includeProtected" schema:"includeProtected"` + Filters url.Values `json:"filters" schema:"filters"` + IncludePinned bool `json:"includePinned" schema:"includePinned"` } type VolumeListOptions struct { @@ -57,13 +57,13 @@ type VolumeImportOptions struct { Input io.Reader } -// VolumeProtectOptions describes the options for protecting/unprotecting volumes -type VolumeProtectOptions struct { - Unprotect bool +// VolumePinOptions describes the options for pinning/unpinning volumes +type VolumePinOptions struct { + Unpin bool } -// VolumeProtectReport describes the response from protecting/unprotecting a volume -type VolumeProtectReport struct { +// 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 2e2001dbda8..44cfb9b17ed 100644 --- a/pkg/domain/filters/volumes.go +++ b/pkg/domain/filters/volumes.go @@ -52,25 +52,25 @@ func GenerateVolumeFilters(filter string, filterValues []string, runtime *libpod }, nil case "until": return createUntilFilterVolumeFunction(filterValues) - case "protected": + 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 \"protected\" filter - must be true or false", val) + 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 { - protected := v.Protected() + pinned := v.Pinned() switch strings.ToLower(val) { case "true", "1": - if protected { + if pinned { return true } case "false", "0": - if !protected { + if !pinned { return true } } @@ -126,25 +126,25 @@ func GeneratePruneVolumeFilters(filter string, filterValues []string, runtime *l }, nil case "until": return createUntilFilterVolumeFunction(filterValues) - case "protected": + 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 \"protected\" filter - must be true or false", val) + 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 { - protected := v.Protected() + pinned := v.Pinned() switch strings.ToLower(val) { case "true", "1": - if protected { + if pinned { return true } case "false", "0": - if !protected { + if !pinned { return true } } diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index 6da2446b25e..c26813eee9f 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -46,8 +46,8 @@ func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.Volum volumeOptions = append(volumeOptions, libpod.WithVolumeGID(*opts.GID), libpod.WithVolumeNoChown()) } - if opts.Protected { - volumeOptions = append(volumeOptions, libpod.WithVolumeProtected()) + if opts.Pinned { + volumeOptions = append(volumeOptions, libpod.WithVolumePinned()) } vol, err := ic.Libpod.NewVolume(ctx, volumeOptions...) @@ -86,10 +86,10 @@ func (ic *ContainerEngine) VolumeRm(ctx context.Context, namesOrIds []string, op } } for _, vol := range vols { - // Check if volume is protected and --include-protected flag is not set - if vol.Protected() && !opts.IncludeProtected { + // Check if volume is pinned and --include-pinned flag is not set + if vol.Pinned() && !opts.IncludePinned { reports = append(reports, &entities.VolumeRmReport{ - Err: fmt.Errorf("volume %s is protected and cannot be removed without --include-protected flag", vol.Name()), + Err: fmt.Errorf("volume %s is pinned and cannot be removed without --include-pinned flag", vol.Name()), Id: vol.Name(), }) continue @@ -154,11 +154,11 @@ func (ic *ContainerEngine) VolumePrune(ctx context.Context, options entities.Vol } funcs = append(funcs, filterFunc) } - return ic.pruneVolumesHelper(ctx, funcs, options.IncludeProtected) + return ic.pruneVolumesHelper(ctx, funcs, options.IncludePinned) } -func (ic *ContainerEngine) pruneVolumesHelper(ctx context.Context, filterFuncs []libpod.VolumeFilter, includeProtected bool) ([]*reports.PruneReport, error) { - pruned, err := ic.Libpod.PruneVolumesWithOptions(ctx, filterFuncs, includeProtected) +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 } @@ -278,13 +278,13 @@ func (ic *ContainerEngine) VolumeExport(_ context.Context, nameOrID string, opti return nil } -func (ic *ContainerEngine) VolumeProtect(ctx context.Context, namesOrIds []string, opts entities.VolumeProtectOptions) ([]*entities.VolumeProtectReport, error) { - var reports []*entities.VolumeProtectReport +func (ic *ContainerEngine) VolumePin(ctx context.Context, namesOrIds []string, opts entities.VolumePinOptions) ([]*entities.VolumePinReport, error) { + var reports []*entities.VolumePinReport for _, nameOrId := range namesOrIds { - report := &entities.VolumeProtectReport{Id: nameOrId} + report := &entities.VolumePinReport{Id: nameOrId} - if err := ic.Libpod.SetVolumeProtected(nameOrId, !opts.Unprotect); err != nil { + if err := ic.Libpod.SetVolumePinned(nameOrId, !opts.Unpin); err != nil { report.Err = err } diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index 24d32192dd4..8479c7097f0 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -122,12 +122,12 @@ func (ic *ContainerEngine) VolumeImport(_ context.Context, nameOrID string, opti return volumes.Import(ic.ClientCtx, nameOrID, options.Input) } -func (ic *ContainerEngine) VolumeProtect(ctx context.Context, namesOrIds []string, opts entities.VolumeProtectOptions) ([]*entities.VolumeProtectReport, error) { - reports := make([]*entities.VolumeProtectReport, 0, len(namesOrIds)) +func (ic *ContainerEngine) VolumePin(ctx context.Context, namesOrIds []string, opts entities.VolumePinOptions) ([]*entities.VolumePinReport, error) { + reports := make([]*entities.VolumePinReport, 0, len(namesOrIds)) for _, nameOrId := range namesOrIds { - report := &entities.VolumeProtectReport{ + report := &entities.VolumePinReport{ Id: nameOrId, - Err: errors.New("volume protection is not supported for remote clients"), + Err: errors.New("volume pinning is not supported for remote clients"), } reports = append(reports, report) } From 82f8a0738cd3f522e24a4174af0df9299282f533 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:00:06 +0200 Subject: [PATCH 15/29] Move Pinned field from VolumeConfig to VolumeState Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/volume.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libpod/volume.go b/libpod/volume.go index 906b5e72e73..63dcd6d4aa0 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -77,9 +77,6 @@ type VolumeConfig struct { StorageImageID string `json:"storageImageID,omitempty"` // MountLabel is the SELinux label to assign to mount points MountLabel string `json:"mountlabel,omitempty"` - // Pinned indicates that this volume should be excluded from - // system prune operations by default - Pinned bool `json:"pinned,omitempty"` } // VolumeState holds the volume's mutable state. @@ -115,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 operations by default + Pinned bool `json:"pinned,omitempty"` } // Name retrieves the volume's name From 0b2277696536107b74a3d0606824c670247c142f Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:00:07 +0200 Subject: [PATCH 16/29] Update Pinned() method to read from state with proper locking Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/volume.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libpod/volume.go b/libpod/volume.go index 63dcd6d4aa0..e239cb8debf 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -286,7 +286,14 @@ func (v *Volume) UsesVolumeDriver() bool { // Pinned returns whether this volume is marked as pinned. // Pinned volumes are excluded from system prune operations by default. func (v *Volume) Pinned() bool { - return v.config.Pinned + 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. From bd5c99f910f3946ca70a3f3dd1df13257d258dd9 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:00:07 +0200 Subject: [PATCH 17/29] Update remaining methods to use state.Pinned instead of config.Pinned Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/options.go | 2 +- libpod/volume.go | 2 +- libpod/volume_inspect.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libpod/options.go b/libpod/options.go index 51219f0af0f..6d5448ac3e7 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1752,7 +1752,7 @@ func WithVolumePinned() VolumeCreateOption { return define.ErrVolumeFinalized } - volume.config.Pinned = true + volume.state.Pinned = true return nil } diff --git a/libpod/volume.go b/libpod/volume.go index e239cb8debf..a2b68f4647f 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -310,7 +310,7 @@ func (v *Volume) SetPinned(pinned bool) error { return err } - v.config.Pinned = pinned + v.state.Pinned = pinned return v.save() } diff --git a/libpod/volume_inspect.go b/libpod/volume_inspect.go index dc743d23683..a9ca5fe3344 100644 --- a/libpod/volume_inspect.go +++ b/libpod/volume_inspect.go @@ -72,7 +72,7 @@ func (v *Volume) Inspect() (*define.InspectVolumeData, error) { data.Timeout = v.runtime.config.Engine.VolumePluginTimeout } - data.Pinned = v.config.Pinned + data.Pinned = v.state.Pinned return data, nil } From f474ae8a794e37e15b88a7a6355cab1a02be7c90 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:10:03 +0200 Subject: [PATCH 18/29] Rename Volume.Pinned() to Volume.IsPinned() for clarity Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/runtime_volume.go | 2 +- libpod/volume.go | 6 +++--- pkg/api/handlers/libpod/volumes.go | 2 +- pkg/domain/filters/volumes.go | 4 ++-- pkg/domain/infra/abi/volumes.go | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go index f66ae1794fb..cc4ac7a4134 100644 --- a/libpod/runtime_volume.go +++ b/libpod/runtime_volume.go @@ -152,7 +152,7 @@ func (r *Runtime) PruneVolumesWithOptions(ctx context.Context, filterFuncs []Vol for _, vol := range vols { // Skip pinned volumes unless explicitly requested - if vol.Pinned() && !includePinned { + if vol.IsPinned() && !includePinned { continue } diff --git a/libpod/volume.go b/libpod/volume.go index a2b68f4647f..1e326c51f74 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -283,9 +283,9 @@ 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 { +// 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() diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 154832be568..d507dedc89f 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -202,7 +202,7 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { return } // Check if volume is pinned and --include-pinned flag is not set - if vol.Pinned() && !query.IncludePinned { + 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 diff --git a/pkg/domain/filters/volumes.go b/pkg/domain/filters/volumes.go index 44cfb9b17ed..ef0c0d3c772 100644 --- a/pkg/domain/filters/volumes.go +++ b/pkg/domain/filters/volumes.go @@ -62,7 +62,7 @@ func GenerateVolumeFilters(filter string, filterValues []string, runtime *libpod } return func(v *libpod.Volume) bool { for _, val := range filterValues { - pinned := v.Pinned() + pinned := v.IsPinned() switch strings.ToLower(val) { case "true", "1": @@ -136,7 +136,7 @@ func GeneratePruneVolumeFilters(filter string, filterValues []string, runtime *l } return func(v *libpod.Volume) bool { for _, val := range filterValues { - pinned := v.Pinned() + pinned := v.IsPinned() switch strings.ToLower(val) { case "true", "1": diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index c26813eee9f..5b02e3626ad 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -87,7 +87,7 @@ 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.Pinned() && !opts.IncludePinned { + 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(), From bce524b15ea002b1102282843bc528210dcddeea Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:10:04 +0200 Subject: [PATCH 19/29] Remove unused volume creation option and runtime method Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/options.go | 14 -------------- libpod/runtime_volume.go | 15 --------------- 2 files changed, 29 deletions(-) diff --git a/libpod/options.go b/libpod/options.go index 6d5448ac3e7..41d6bc4a78c 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1744,20 +1744,6 @@ 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 { diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go index cc4ac7a4134..0227c36ff5d 100644 --- a/libpod/runtime_volume.go +++ b/libpod/runtime_volume.go @@ -178,18 +178,3 @@ func (r *Runtime) PruneVolumesWithOptions(ctx context.Context, filterFuncs []Vol } 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) -} From 21af724a13445454f6f2c166115fc3a486bb4f53 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:10:04 +0200 Subject: [PATCH 20/29] Refactor volume creation to set pinned status after creation Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- pkg/api/handlers/libpod/volumes.go | 13 +++++++++---- pkg/domain/infra/abi/volumes.go | 17 ++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index d507dedc89f..7c9d5bc072f 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -80,15 +80,20 @@ 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) 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) diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index 5b02e3626ad..afff2fe2316 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -46,14 +46,18 @@ func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.Volum volumeOptions = append(volumeOptions, libpod.WithVolumeGID(*opts.GID), libpod.WithVolumeNoChown()) } - if opts.Pinned { - volumeOptions = append(volumeOptions, libpod.WithVolumePinned()) - } - vol, err := ic.Libpod.NewVolume(ctx, volumeOptions...) 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 } @@ -284,8 +288,11 @@ func (ic *ContainerEngine) VolumePin(ctx context.Context, namesOrIds []string, o for _, nameOrId := range namesOrIds { report := &entities.VolumePinReport{Id: nameOrId} - if err := ic.Libpod.SetVolumePinned(nameOrId, !opts.Unpin); err != nil { + vol, err := ic.Libpod.LookupVolume(nameOrId) + if err != nil { report.Err = err + } else { + report.Err = vol.SetPinned(!opts.Unpin) } reports = append(reports, report) From 8495adfa65279c30b141787353ee69538ae1b5cc Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:10:05 +0200 Subject: [PATCH 21/29] Add early return optimization to Volume.SetPinned() Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/volume.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libpod/volume.go b/libpod/volume.go index 1e326c51f74..276a70c1859 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -310,6 +310,11 @@ func (v *Volume) SetPinned(pinned bool) error { 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() From 488fbb99fc8571fe2f0adb12cc482655d9e389be Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:10:06 +0200 Subject: [PATCH 22/29] Add support for pinned volumes in system reset operation Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- libpod/reset.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libpod/reset.go b/libpod/reset.go index 60ccab1e497..48ee7a00a2a 100644 --- a/libpod/reset.go +++ b/libpod/reset.go @@ -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 v.IsPinned() { + continue + } if err := r.RemoveVolume(ctx, v, true, &timeout); err != nil { if errors.Is(err, define.ErrNoSuchVolume) { continue From e9bfbee6a8daccb0c98e8b5ff9d2612b77f45326 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:10:06 +0200 Subject: [PATCH 23/29] Update documentation and comments to include system reset operations Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/system/reset.go | 4 ++-- cmd/podman/volumes/pin.go | 2 +- libpod/volume.go | 4 ++-- pkg/domain/entities/types/volumes.go | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/podman/system/reset.go b/cmd/podman/system/reset.go index c302c0c6f3a..61154ee7c41 100644 --- a/cmd/podman/system/reset.go +++ b/cmd/podman/system/reset.go @@ -19,7 +19,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{ @@ -60,7 +60,7 @@ func reset(_ *cobra.Command, _ []string) { - all networks - all build cache - all machines - - all volumes`) + - all volumes (excluding pinned volumes)`) info, _ := registry.ContainerEngine().Info(registry.Context()) // lets not hard fail in case of an error diff --git a/cmd/podman/volumes/pin.go b/cmd/podman/volumes/pin.go index 6f3a2d356c2..dde0d5f37b5 100644 --- a/cmd/podman/volumes/pin.go +++ b/cmd/podman/volumes/pin.go @@ -13,7 +13,7 @@ import ( var ( pinDescription = `Mark or unmark a volume as pinned. -Pinned volumes are excluded from system prune operations by default.` +Pinned volumes are excluded from system prune and system reset operations.` pinCommand = &cobra.Command{ Use: "pin [options] VOLUME [VOLUME...]", diff --git a/libpod/volume.go b/libpod/volume.go index 276a70c1859..299db0b745a 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -113,7 +113,7 @@ type VolumeState struct { // 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 + // system prune and reset operations by default Pinned bool `json:"pinned,omitempty"` } @@ -297,7 +297,7 @@ func (v *Volume) IsPinned() bool { } // SetPinned sets the pinned status of the volume. -// Pinned volumes are excluded from system prune operations by default. +// 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 diff --git a/pkg/domain/entities/types/volumes.go b/pkg/domain/entities/types/volumes.go index cf0e00a0589..590649be374 100644 --- a/pkg/domain/entities/types/volumes.go +++ b/pkg/domain/entities/types/volumes.go @@ -23,7 +23,8 @@ type VolumeCreateOptions struct { // GID that the volume will be created as GID *int `schema:"gid"` // Pinned indicates that this volume should be excluded from - // system prune operations by default + // system prune and reset operations. This allows atomically + // creating and pinning a volume to avoid race conditions. Pinned bool `schema:"pinned"` } From 8e637aa149a33f29a5f1fae1c6629534213cf1b4 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:10:07 +0200 Subject: [PATCH 24/29] Simplify system prune warning message generation Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/system/prune.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go index aba077649bb..2797ebe76a4 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -138,11 +138,6 @@ func prune(_ *cobra.Command, _ []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 @@ -154,7 +149,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` + pinnedNote + ` + - all networks not used by at least one container%s%s (excluding pinned volumes) - all dangling images - all dangling build cache From 61fa665eec1ac265114cd6eb65b0cc8f279ea694 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 02:02:46 +0200 Subject: [PATCH 25/29] Add support for including pinned volumes in reset Adds a new flag `--include-pinned` to podman system reset that allows users to optionally include pinned volumes in the reset operation. Previously, pinned volumes were always excluded from being reset. This change: - Updates warning messages to correctly reflect volume behavior - Modifies the reset functionality to respect the new inclusion flag - Propagates the new option through the system architecture - Clarifies in the prune warning message that pinned volumes are optionally included Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/system/prune.go | 2 +- cmd/podman/system/reset.go | 19 +++++++++++++++---- libpod/reset.go | 4 ++-- pkg/domain/entities/engine_container.go | 2 +- pkg/domain/entities/types/system.go | 5 +++++ pkg/domain/infra/abi/system.go | 4 ++-- pkg/domain/infra/tunnel/system.go | 2 +- 7 files changed, 27 insertions(+), 11 deletions(-) diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go index 2797ebe76a4..4845b859fc4 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -149,7 +149,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 (excluding pinned volumes) + - 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 61154ee7c41..df8d2cbebc6 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/v5/pkg/domain/entities" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/completion" @@ -32,7 +33,8 @@ var ( ValidArgsFunction: completion.AutocompleteNone, } - forceFlag bool + forceFlag bool + includePinned 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(&includePinned, "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 includePinned { + volumeMsg = " - all volumes" + } + fmt.Printf(`WARNING! This will remove: - all containers - all pods - all images - all networks - all build cache - all machines - - all volumes (excluding pinned 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: includePinned, + } + if err := registry.ContainerEngine().Reset(registry.Context(), resetOptions); err != nil { logrus.Error(err) } diff --git a/libpod/reset.go b/libpod/reset.go index 48ee7a00a2a..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. @@ -165,7 +165,7 @@ func (r *Runtime) Reset(ctx context.Context) error { } for _, v := range volumes { // Skip pinned volumes - they should not be removed during reset - if v.IsPinned() { + if !includePinned && v.IsPinned() { continue } if err := r.RemoveVolume(ctx, v, true, &timeout); err != nil { diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 476a485456f..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) diff --git a/pkg/domain/entities/types/system.go b/pkg/domain/entities/types/system.go index 4dca8f1963a..9581e78afb2 100644 --- a/pkg/domain/entities/types/system.go +++ b/pkg/domain/entities/types/system.go @@ -72,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/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/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") } From 3260d5b8300c2331fcbfe831377b0537bf0fe3db Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 02:51:31 +0200 Subject: [PATCH 26/29] DOCS: Add volume pinning functionality Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- docs/source/markdown/podman-system-prune.1.md | 6 ++- docs/source/markdown/podman-system-reset.1.md | 6 +++ .../source/markdown/podman-volume-create.1.md | 11 +++- .../markdown/podman-volume-inspect.1.md | 1 + docs/source/markdown/podman-volume-ls.1.md.in | 12 +++++ docs/source/markdown/podman-volume-pin.1.md | 53 +++++++++++++++++++ docs/source/markdown/podman-volume-prune.1.md | 8 ++- docs/source/markdown/podman-volume-rm.1.md | 8 ++- docs/source/markdown/podman-volume.1.md | 1 + 9 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 docs/source/markdown/podman-volume-pin.1.md 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..b0865f3bc7d 100644 --- a/docs/source/markdown/podman-volume-inspect.1.md +++ b/docs/source/markdown/podman-volume-inspect.1.md @@ -45,6 +45,7 @@ Valid placeholders for the Go template are listed below: | .StorageID | StorageID of the volume | | .Timeout | Timeout of the volume | | .UID | UID the volume was created with | +| .Pinned | Whether the volume is pinned | #### **--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..4ef373bec42 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 | @@ -60,6 +61,7 @@ Valid placeholders for the Go template are listed below: | .StorageID | StorageID of the volume | | .Timeout | Timeout of the volume | | .UID | UID of volume | +| .Pinned | Whether the volume is pinned | | .VolumeConfigResponse ... | Don't use | #### **--help** @@ -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..dd8bb0ec4b0 --- /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 + +#### **--unpin** + +Remove the pinned status from the specified volumes instead of pinning them. + +#### **--help** + +Print usage statement. + +## 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. | From 9de90a4225ef717c0f98731e70cf524fbbebbb3e Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Sat, 4 Oct 2025 03:09:20 +0200 Subject: [PATCH 27/29] Bugfix for pinned volumes in system reset * Renames the global variable 'includePinned' to 'resetIncludePinned' to improve code clarity and prevent potential variable shadowing. * Adds SystemResetOptions type alias to the entities package. Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/common/completion.go | 1 + cmd/podman/system/prune.go | 5 ----- cmd/podman/system/reset.go | 10 +++++----- cmd/podman/volumes/prune.go | 4 +++- docs/source/markdown/podman-volume-inspect.1.md | 2 +- docs/source/markdown/podman-volume-ls.1.md.in | 2 +- docs/source/markdown/podman-volume-pin.1.md | 8 ++++---- pkg/domain/entities/system.go | 1 + pkg/domain/infra/abi/volumes.go | 2 +- 9 files changed, 17 insertions(+), 18 deletions(-) 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 4845b859fc4..0d2c174a32b 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -93,11 +93,6 @@ func prune(_ *cobra.Command, _ []string) error { 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 { diff --git a/cmd/podman/system/reset.go b/cmd/podman/system/reset.go index df8d2cbebc6..fc5305c0739 100644 --- a/cmd/podman/system/reset.go +++ b/cmd/podman/system/reset.go @@ -33,8 +33,8 @@ var ( ValidArgsFunction: completion.AutocompleteNone, } - forceFlag bool - includePinned bool + forceFlag bool + resetIncludePinned bool ) func init() { @@ -44,7 +44,7 @@ func init() { }) flags := systemResetCommand.Flags() flags.BoolVarP(&forceFlag, "force", "f", false, "Do not prompt for confirmation") - flags.BoolVar(&includePinned, "include-pinned", false, "Include pinned volumes in reset operation") + flags.BoolVar(&resetIncludePinned, "include-pinned", false, "Include pinned volumes in reset operation") } func reset(_ *cobra.Command, _ []string) { @@ -57,7 +57,7 @@ func reset(_ *cobra.Command, _ []string) { if !forceFlag { reader := bufio.NewReader(os.Stdin) volumeMsg := " - all volumes (excluding pinned volumes)" - if includePinned { + if resetIncludePinned { volumeMsg = " - all volumes" } fmt.Printf(`WARNING! This will remove: @@ -102,7 +102,7 @@ func reset(_ *cobra.Command, _ []string) { // ContainerEngine() is unusable and shut down after this. resetOptions := entities.SystemResetOptions{ - IncludePinned: includePinned, + IncludePinned: resetIncludePinned, } if err := registry.ContainerEngine().Reset(registry.Context(), resetOptions); err != nil { logrus.Error(err) 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/docs/source/markdown/podman-volume-inspect.1.md b/docs/source/markdown/podman-volume-inspect.1.md index b0865f3bc7d..a0f89a3475e 100644 --- a/docs/source/markdown/podman-volume-inspect.1.md +++ b/docs/source/markdown/podman-volume-inspect.1.md @@ -44,8 +44,8 @@ Valid placeholders for the Go template are listed below: | .Status ... | Status of the volume | | .StorageID | StorageID of the volume | | .Timeout | Timeout of the volume | -| .UID | UID the volume was created with | | .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 4ef373bec42..707cd898880 100644 --- a/docs/source/markdown/podman-volume-ls.1.md.in +++ b/docs/source/markdown/podman-volume-ls.1.md.in @@ -60,8 +60,8 @@ Valid placeholders for the Go template are listed below: | .Status ... | Status of the volume | | .StorageID | StorageID of the volume | | .Timeout | Timeout of the volume | -| .UID | UID of volume | | .Pinned | Whether the volume is pinned | +| .UID | UID of volume | | .VolumeConfigResponse ... | Don't use | #### **--help** diff --git a/docs/source/markdown/podman-volume-pin.1.md b/docs/source/markdown/podman-volume-pin.1.md index dd8bb0ec4b0..ee5bafde2b7 100644 --- a/docs/source/markdown/podman-volume-pin.1.md +++ b/docs/source/markdown/podman-volume-pin.1.md @@ -16,14 +16,14 @@ By default, **podman volume pin** marks volumes as pinned. Use the **--unpin** o ## OPTIONS -#### **--unpin** - -Remove the pinned status from the specified volumes instead of pinning them. - #### **--help** Print usage statement. +#### **--unpin** + +Remove the pinned status from the specified volumes instead of pinning them. + ## EXAMPLES Mark a volume as pinned. 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/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index afff2fe2316..c3f576463bd 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -283,7 +283,7 @@ func (ic *ContainerEngine) VolumeExport(_ context.Context, nameOrID string, opti } func (ic *ContainerEngine) VolumePin(ctx context.Context, namesOrIds []string, opts entities.VolumePinOptions) ([]*entities.VolumePinReport, error) { - var reports []*entities.VolumePinReport + reports := make([]*entities.VolumePinReport, 0, len(namesOrIds)) for _, nameOrId := range namesOrIds { report := &entities.VolumePinReport{Id: nameOrId} From 09835abf8ded6bdbcf14306677edab7d900e8fc7 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 11 Dec 2025 03:24:04 +0100 Subject: [PATCH 28/29] Update import paths from v5 to v6 Updates Podman package import references from version 5 to version 6. Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/system/reset.go | 2 +- cmd/podman/volumes/pin.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/podman/system/reset.go b/cmd/podman/system/reset.go index fc5305c0739..dae31e821b8 100644 --- a/cmd/podman/system/reset.go +++ b/cmd/podman/system/reset.go @@ -11,7 +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/v5/pkg/domain/entities" + "github.com/containers/podman/v6/pkg/domain/entities" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "go.podman.io/common/pkg/completion" diff --git a/cmd/podman/volumes/pin.go b/cmd/podman/volumes/pin.go index dde0d5f37b5..439e0d3e7e2 100644 --- a/cmd/podman/volumes/pin.go +++ b/cmd/podman/volumes/pin.go @@ -4,9 +4,9 @@ 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/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" ) From afe78aacdfd299a107ed22b5fad36e66a8f95381 Mon Sep 17 00:00:00 2001 From: tobwen <1864057+tobwen@users.noreply.github.com> Date: Thu, 11 Dec 2025 03:30:47 +0100 Subject: [PATCH 29/29] Linting... Signed-off-by: tobwen <1864057+tobwen@users.noreply.github.com> --- cmd/podman/system/prune.go | 2 +- cmd/podman/volumes/pin.go | 2 +- pkg/api/handlers/libpod/volumes.go | 2 +- pkg/domain/entities/types/system.go | 2 +- pkg/domain/infra/abi/volumes.go | 4 ++-- pkg/domain/infra/tunnel/volumes.go | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/podman/system/prune.go b/cmd/podman/system/prune.go index 0d2c174a32b..a6c58a65f6c 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -82,7 +82,7 @@ func prune(_ *cobra.Command, _ []string) error { return nil } } - + // Set the include pinned flag for volume pruning if pruneOptions.Volume { pruneOptions.VolumePruneOptions.IncludePinned = includePinned diff --git a/cmd/podman/volumes/pin.go b/cmd/podman/volumes/pin.go index 439e0d3e7e2..3568d4f184e 100644 --- a/cmd/podman/volumes/pin.go +++ b/cmd/podman/volumes/pin.go @@ -40,7 +40,7 @@ func init() { flags.BoolVar(&pinOptions.Unpin, "unpin", false, "Remove pinning from volume") } -func pin(cmd *cobra.Command, args []string) error { +func pin(_ *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("must specify at least one volume name") } diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 7c9d5bc072f..fe0693066a9 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -208,7 +208,7 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { } // Check if volume is pinned and --include-pinned flag is not set if vol.IsPinned() && !query.IncludePinned { - utils.Error(w, http.StatusBadRequest, + utils.Error(w, http.StatusBadRequest, fmt.Errorf("volume %s is pinned and cannot be removed without includePinned=true parameter", vol.Name())) return } diff --git a/pkg/domain/entities/types/system.go b/pkg/domain/entities/types/system.go index 9581e78afb2..8e4ef5dd6f6 100644 --- a/pkg/domain/entities/types/system.go +++ b/pkg/domain/entities/types/system.go @@ -47,7 +47,7 @@ type SystemPruneOptions struct { Filters map[string][]string `json:"filters" schema:"filters"` External bool Build bool - VolumePruneOptions VolumePruneOptions `json:"volumePruneOptions" schema:"volumePruneOptions"` + VolumePruneOptions VolumePruneOptions `json:"volumePruneOptions" schema:"volumePruneOptions"` } // VolumePruneOptions describes the options needed diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index c3f576463bd..46aeac4777f 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -98,7 +98,7 @@ func (ic *ContainerEngine) VolumeRm(ctx context.Context, namesOrIds []string, op }) continue } - + reports = append(reports, &entities.VolumeRmReport{ Err: ic.Libpod.RemoveVolume(ctx, vol, opts.Force, opts.Timeout), Id: vol.Name(), @@ -282,7 +282,7 @@ func (ic *ContainerEngine) VolumeExport(_ context.Context, nameOrID string, opti return nil } -func (ic *ContainerEngine) VolumePin(ctx context.Context, namesOrIds []string, opts entities.VolumePinOptions) ([]*entities.VolumePinReport, error) { +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 { diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index 8479c7097f0..73bdb519e67 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -122,7 +122,7 @@ func (ic *ContainerEngine) VolumeImport(_ context.Context, nameOrID string, opti return volumes.Import(ic.ClientCtx, nameOrID, options.Input) } -func (ic *ContainerEngine) VolumePin(ctx context.Context, namesOrIds []string, opts entities.VolumePinOptions) ([]*entities.VolumePinReport, error) { +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{