From dad3c39fea15b2938a337bb42c65f2af6d1ecec4 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/17] 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 c4b45a04f5..693f1709a9 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 68c15009c3..7a22f3a26d 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -76,6 +76,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 f4a3cc889d..4b07b05ec9 100644 --- a/libpod/volume_inspect.go +++ b/libpod/volume_inspect.go @@ -75,5 +75,7 @@ func (v *Volume) Inspect() (*define.InspectVolumeData, error) { data.Timeout = v.runtime.config.Engine.VolumePluginTimeout } + data.Protected = v.config.Protected + return data, nil } From 9392e2db0d8408ce0c1651e76272c10df3691571 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/17] 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 5a24d89015..dceda1b911 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1753,6 +1753,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 7a22f3a26d..c2fb00c602 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -286,6 +286,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 13893f0b235986f8d8edd1f25b018ac175d704af 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/17] 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 ff04eec3cc..5d1026faa4 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 639d7bd3a3d33f8a9ab00e79e12ebf66e40386a4 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/17] 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 8af843bb97..5c356875c1 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -122,4 +122,5 @@ type ContainerEngine interface { //nolint:interfacebloat VolumeReload(ctx context.Context) (*VolumeReloadReport, error) VolumeExport(ctx context.Context, nameOrID string, options VolumeExportOptions) error VolumeImport(ctx context.Context, nameOrID string, options VolumeImportOptions) error + 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 97310428ec..d38b1bcbf1 100644 --- a/pkg/domain/entities/types/system.go +++ b/pkg/domain/entities/types/system.go @@ -39,11 +39,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 06e3727f10..eec3945463 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 fe310f4fe3..0b83c97de4 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 2f3cc9bf41c17100e1abb5c3438793542b791dd2 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/17] 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 07e2cac86c..827a03a0fd 100644 --- a/pkg/domain/filters/volumes.go +++ b/pkg/domain/filters/volumes.go @@ -61,6 +61,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) { @@ -110,6 +135,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 fc26910a69694798566b4df4ef78b58507b04b99 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/17] 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 ce32fa571c..5825c5c287 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -48,6 +48,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 @@ -84,6 +88,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(), @@ -143,11 +156,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 } @@ -267,6 +280,22 @@ func (ic *ContainerEngine) VolumeExport(ctx context.Context, nameOrID string, op 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(ctx context.Context, nameOrID string, options entities.VolumeImportOptions) error { vol, err := ic.Libpod.LookupVolume(nameOrID) if err != nil { From 15262c466fafb5673e8c24320b8eaa6ea9553c83 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/17] 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 5f9d5e3376..e8b70e5618 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -83,6 +83,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) @@ -163,7 +167,13 @@ func pruneVolumesHelper(r *http.Request) ([]*reports.PruneReport, error) { filterFuncs = append(filterFuncs, filterFunc) } - reports, err := runtime.PruneVolumes(r.Context(), filterFuncs) + // Check for 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 } @@ -176,8 +186,9 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { decoder = r.Context().Value(api.DecoderKey).(*schema.Decoder) ) query := struct { - Force bool `schema:"force"` - Timeout *uint `schema:"timeout"` + Force bool `schema:"force"` + Timeout *uint `schema:"timeout"` + IncludeProtected bool `schema:"includeProtected"` }{ // override any golang type defaults } @@ -193,6 +204,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 10a33f6c272b7032414204429a0405bf5649a71a 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/17] 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 52817bb99f..57f4d53b21 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 { @@ -94,6 +99,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 df7a67f5998ab8ee1b01431f7aa2abe7d2117544 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/17] 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 0000000000..d1f50a74cd --- /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 7709b0e5d23716df5e635cdfdf1a20ebea07aa66 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/17] 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 358a3704c2..400d4c855a 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) @@ -64,6 +66,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()) { @@ -76,9 +79,6 @@ func rm(cmd *cobra.Command, args []string) error { if r.Err == nil { fmt.Println(r.Id) } else { - if rmOptions.Force && strings.Contains(r.Err.Error(), define.ErrNoSuchVolume.Error()) { - continue - } setExitCode(r.Err) errs = append(errs, r.Err) } From b41d4b1c91ac3e4ee2c6807821d4ff20d6d52ba9 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/17] 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 d8991c21ad..872ab71580 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(cmd *cobra.Command, args []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(cmd *cobra.Command, args []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 78e4ac6e57a9578d0f7ed02897c1fac977a28099 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/17] 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 d8ba4007f1..76998c40f9 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -121,3 +121,15 @@ func (ic *ContainerEngine) VolumeExport(ctx context.Context, nameOrID string, op func (ic *ContainerEngine) VolumeImport(ctx 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 66450f5adc02942900b1c2eb5d313a32d3c46b93 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/17] 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 872ab71580..2d4d15089e 100644 --- a/cmd/podman/system/prune.go +++ b/cmd/podman/system/prune.go @@ -82,6 +82,11 @@ func prune(cmd *cobra.Command, args []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 7f5491aab3a83ca73f901f8c6863cf111fd139ce 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/17] 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 2d4d15089e..930914bf2a 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(cmd *cobra.Command, args []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(cmd *cobra.Command, args []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(cmd *cobra.Command, args []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 57f4d53b21..f1dc3bf2d5 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 { @@ -99,7 +99,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 0000000000..6f3a2d356c --- /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 d1f50a74cd..0000000000 --- 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 400d4c855a..9ec15c6c93 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) @@ -66,7 +66,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 693f1709a9..a0f22b311c 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 dceda1b911..57b7ce5662 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1753,15 +1753,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 5d1026faa4..9cbd8581a0 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 c2fb00c602..14b4b20882 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -76,9 +76,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. @@ -286,15 +286,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 } @@ -306,7 +306,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 4b07b05ec9..01b66d1b77 100644 --- a/libpod/volume_inspect.go +++ b/libpod/volume_inspect.go @@ -75,7 +75,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 e8b70e5618..a1a5daa13c 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -83,8 +83,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...) @@ -167,13 +167,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 } @@ -186,9 +186,9 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { decoder = r.Context().Value(api.DecoderKey).(*schema.Decoder) ) query := struct { - Force bool `schema:"force"` - Timeout *uint `schema:"timeout"` - IncludeProtected bool `schema:"includeProtected"` + Force bool `schema:"force"` + Timeout *uint `schema:"timeout"` + IncludePinned bool `schema:"includePinned"` }{ // override any golang type defaults } @@ -204,10 +204,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 5c356875c1..923b486923 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -122,5 +122,5 @@ type ContainerEngine interface { //nolint:interfacebloat VolumeReload(ctx context.Context) (*VolumeReloadReport, error) VolumeExport(ctx context.Context, nameOrID string, options VolumeExportOptions) error VolumeImport(ctx context.Context, nameOrID string, options VolumeImportOptions) error - 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 d38b1bcbf1..c1bdb78f67 100644 --- a/pkg/domain/entities/types/system.go +++ b/pkg/domain/entities/types/system.go @@ -50,7 +50,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 eec3945463..bce0bebac6 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 0b83c97de4..8eb626f40a 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 827a03a0fd..26e01f2b5b 100644 --- a/pkg/domain/filters/volumes.go +++ b/pkg/domain/filters/volumes.go @@ -61,25 +61,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 } } @@ -135,25 +135,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 5825c5c287..1c4ccfbc6a 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -48,8 +48,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...) @@ -88,10 +88,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 @@ -156,11 +156,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 } @@ -280,13 +280,13 @@ func (ic *ContainerEngine) VolumeExport(ctx context.Context, nameOrID string, op 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 76998c40f9..86d08ef10e 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -122,12 +122,12 @@ func (ic *ContainerEngine) VolumeImport(ctx context.Context, nameOrID string, op 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 9e37833ae634b87f8f5e7539a779dce7f0ecf9e0 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/17] 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 14b4b20882..49f5fd4d43 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -76,9 +76,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. @@ -114,6 +111,9 @@ type VolumeState struct { UIDChowned int `json:"uidChowned,omitempty"` // GIDChowned is the GID the volume was chowned to. GIDChowned int `json:"gidChowned,omitempty"` + // Pinned indicates that this volume should be excluded from + // system prune operations by default + Pinned bool `json:"pinned,omitempty"` } // Name retrieves the volume's name From bc5b267ff15d62ad44891ecd0248740c74da9b59 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/17] 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 49f5fd4d43..cf6134ebcb 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -289,7 +289,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 8c6bb95deda5665c09fb97c8c32fc24e55c6e1d7 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/17] 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 57b7ce5662..0e50cc94a9 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1761,7 +1761,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 cf6134ebcb..42d98acca7 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -313,7 +313,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 01b66d1b77..1a350311fe 100644 --- a/libpod/volume_inspect.go +++ b/libpod/volume_inspect.go @@ -75,7 +75,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 }