From 02d578b63769e065383f6a418e4157ea7b0befee Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 3 Aug 2025 20:21:33 +0200 Subject: [PATCH 1/4] system prune: use register function for prune functions Introduce a "prune" package in which we maintain a list of prune functions that are registered. Known prune "content-types" are included in a pre-defined order, after which additional content can be registered. Using this approach no longer requires the "RunPrune" functions to be exported, and allows additional content-types to be introduced without having to import those packages into the system package, so keeping things more decoupled. Signed-off-by: Sebastiaan van Stijn --- cli/command/builder/prune.go | 20 +++++ cli/command/container/prune.go | 19 +++++ cli/command/image/prune.go | 20 +++++ cli/command/network/prune.go | 20 +++++ cli/command/system/prune.go | 39 +++++----- cli/command/system/prune_test.go | 7 ++ cli/command/system/pruner/pruner.go | 115 ++++++++++++++++++++++++++++ cli/command/volume/prune.go | 19 +++++ 8 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 cli/command/system/pruner/pruner.go diff --git a/cli/command/builder/prune.go b/cli/command/builder/prune.go index af83aa450..98bc79305 100644 --- a/cli/command/builder/prune.go +++ b/cli/command/builder/prune.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" @@ -16,6 +17,13 @@ import ( "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { force bool all bool @@ -105,6 +113,18 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} // CachePrune executes a prune command for build cache +// +// Deprecated: this function was only used internally and will be removed in the next release. func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter}) } + +// pruneFn prunes the build cache for use in "docker system prune" and +// returns the amount of space reclaimed and a detailed output string. +func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + return runPrune(ctx, dockerCLI, pruneOptions{ + force: true, + all: options.All, + filter: options.Filter, + }) +} diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go index d75338718..3903d08cd 100644 --- a/cli/command/container/prune.go +++ b/cli/command/container/prune.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" @@ -14,6 +15,13 @@ import ( "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeContainer, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { force bool filter opts.FilterOpt @@ -87,6 +95,17 @@ func (cancelledErr) Cancelled() {} // RunPrune calls the Container Prune API // This returns the amount of space reclaimed and a detailed output string +// +// Deprecated: this function was only used internally and will be removed in the next release. func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) } + +// pruneFn calls the Container Prune API for use in "docker system prune", +// and returns the amount of space reclaimed and a detailed output string. +func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + return runPrune(ctx, dockerCLI, pruneOptions{ + force: true, + filter: options.Filter, + }) +} diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index eec9b1c0b..e0411f5dd 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" @@ -16,6 +17,13 @@ import ( "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeImage, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { force bool all bool @@ -111,6 +119,18 @@ func (cancelledErr) Cancelled() {} // RunPrune calls the Image Prune API // This returns the amount of space reclaimed and a detailed output string +// +// Deprecated: this function was only used internally and will be removed in the next release. func RunPrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter}) } + +// pruneFn calls the Container Prune API for use in "docker system prune", +// and returns the amount of space reclaimed and a detailed output string. +func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + return runPrune(ctx, dockerCLI, pruneOptions{ + force: true, + all: options.All, + filter: options.Filter, + }) +} diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go index fce7adf3e..123c11ebf 100644 --- a/cli/command/network/prune.go +++ b/cli/command/network/prune.go @@ -7,11 +7,19 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeNetwork, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { force bool filter opts.FilterOpt @@ -82,7 +90,19 @@ func (cancelledErr) Cancelled() {} // RunPrune calls the Network Prune API // This returns the amount of space reclaimed and a detailed output string +// +// Deprecated: this function was only used internally and will be removed in the next release. func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { output, err := runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) return 0, output, err } + +// pruneFn calls the Network Prune API for use in "docker system prune" +// and returns the amount of space reclaimed and a detailed output string. +func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + output, err := runPrune(ctx, dockerCLI, pruneOptions{ + force: true, + filter: options.Filter, + }) + return 0, output, err +} diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index c6eb9f263..828b0c4d2 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -9,12 +9,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/builder" "github.com/docker/cli/cli/command/completion" - "github.com/docker/cli/cli/command/container" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/command/network" - "github.com/docker/cli/cli/command/volume" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" @@ -85,21 +81,28 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) return cancelledErr{errors.New("system prune has been cancelled")} } } - pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){ - container.RunPrune, - network.RunPrune, - } - if options.pruneVolumes { - pruneFuncs = append(pruneFuncs, volume.RunPrune) - } - pruneFuncs = append(pruneFuncs, image.RunPrune) - if options.pruneBuildCache { - pruneFuncs = append(pruneFuncs, builder.CachePrune) - } var spaceReclaimed uint64 - for _, pruneFn := range pruneFuncs { - spc, output, err := pruneFn(ctx, dockerCli, options.all, options.filter) + for contentType, pruneFn := range pruner.List() { + switch contentType { + case pruner.TypeVolume: + if !options.pruneVolumes { + continue + } + case pruner.TypeBuildCache: + if !options.pruneBuildCache { + continue + } + case pruner.TypeContainer, pruner.TypeNetwork, pruner.TypeImage: + // no special handling; keeping the "exhaustive" linter happy. + default: + // other pruners; no special handling; keeping the "exhaustive" linter happy. + } + + spc, output, err := pruneFn(ctx, dockerCli, pruner.PruneOptions{ + All: options.all, + Filter: options.filter, + }) if err != nil { return err } diff --git a/cli/command/system/prune_test.go b/cli/command/system/prune_test.go index ad00a1958..3b47c4d45 100644 --- a/cli/command/system/prune_test.go +++ b/cli/command/system/prune_test.go @@ -13,6 +13,13 @@ import ( "github.com/moby/moby/api/types/network" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" + + // Make sure pruners are registered for tests (they're included automatically when building). + _ "github.com/docker/cli/cli/command/builder" + _ "github.com/docker/cli/cli/command/container" + _ "github.com/docker/cli/cli/command/image" + _ "github.com/docker/cli/cli/command/network" + _ "github.com/docker/cli/cli/command/volume" ) func TestPrunePromptPre131DoesNotIncludeBuildCache(t *testing.T) { diff --git a/cli/command/system/pruner/pruner.go b/cli/command/system/pruner/pruner.go new file mode 100644 index 000000000..f3701430d --- /dev/null +++ b/cli/command/system/pruner/pruner.go @@ -0,0 +1,115 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.23 + +// Package pruner registers "prune" functions to be included as part of +// "docker system prune". +package pruner + +import ( + "context" + "errors" + "fmt" + "iter" + "maps" + "slices" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/opts" +) + +// ContentType is an identifier for content that can be pruned. +type ContentType string + +// Pre-defined content-types to prune. Additional types can be registered, +// and will be pruned after the list of pre-defined types. +const ( + TypeContainer ContentType = "container" + TypeNetwork ContentType = "network" + TypeImage ContentType = "image" + TypeVolume ContentType = "volume" + TypeBuildCache ContentType = "buildcache" +) + +// pruneOrder is the order in which ContentType must be pruned. The order +// in which pruning happens is important to make sure that resources are +// released before pruning (e.g., a "container" can use a "network" and +// "volume", so containers must be pruned before networks and volumes). +var pruneOrder = []ContentType{ + TypeContainer, + TypeNetwork, + TypeVolume, + TypeImage, + TypeBuildCache, +} + +// PruneFunc is the signature for prune-functions. It returns details about +// the content pruned; +// +// - spaceReclaimed is the amount of data removed (in bytes). +// - details is arbitrary information about the content pruned. +type PruneFunc func(ctx context.Context, dockerCLI command.Cli, pruneOpts PruneOptions) (spaceReclaimed uint64, details string, _ error) + +type PruneOptions struct { + All bool + Filter opts.FilterOpt +} + +// registered holds a map of PruneFunc functions registered through [Register]. +// It is considered immutable after startup. +var registered map[ContentType]PruneFunc + +// Register registers a [PruneFunc] under the given name to be included in +// "docker system prune". It is designed to be called in an init function +// and is not safe for concurrent use. +// +// For example: +// +// func init() { +// // Register the prune command to run as part of "docker system prune". +// if err := prune.Register(prune.TypeImage, prunerFn); err != nil { +// panic(err) +// } +// } +func Register(name ContentType, pruneFunc PruneFunc) error { + if name == "" { + return errors.New("error registering pruner: invalid prune type: cannot be empty") + } + if pruneFunc == nil { + return errors.New("error registering pruner: prune function is nil for " + string(name)) + } + if registered == nil { + registered = make(map[ContentType]PruneFunc) + } + if _, exists := registered[name]; exists { + return fmt.Errorf("error registering pruner: content-type %s is already registered", name) + } + registered[name] = pruneFunc + return nil +} + +// List iterates over all registered pruners, starting with known pruners +// in their predefined order, followed by any others (sorted alphabetically). +func List() iter.Seq2[ContentType, PruneFunc] { + all := maps.Clone(registered) + ordered := make([]ContentType, 0, len(all)) + for _, ct := range pruneOrder { + if _, ok := all[ct]; ok { + ordered = append(ordered, ct) + delete(all, ct) + } + } + // append any remaining content-types (if any) that may be registered. + if len(all) > 0 { + ordered = append(ordered, slices.Sorted(maps.Keys(all))...) + } + + return func(yield func(ContentType, PruneFunc) bool) { + for _, ct := range ordered { + if fn := registered[ct]; fn != nil { + if !yield(ct, fn) { + return + } + } + } + } +} diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go index 1eb3e7b02..e617529f0 100644 --- a/cli/command/volume/prune.go +++ b/cli/command/volume/prune.go @@ -8,6 +8,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" @@ -15,6 +16,13 @@ import ( "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeVolume, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { all bool force bool @@ -112,6 +120,17 @@ func (cancelledErr) Cancelled() {} // RunPrune calls the Volume Prune API // This returns the amount of space reclaimed and a detailed output string +// +// Deprecated: this function was only used internally and will be removed in the next release. func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) } + +// pruneFn calls the Volume Prune API for use in "docker system prune", +// and returns the amount of space reclaimed and a detailed output string. +func pruneFn(ctx context.Context, dockerCli command.Cli, options pruner.PruneOptions) (uint64, string, error) { + return runPrune(ctx, dockerCli, pruneOptions{ + force: true, + filter: options.Filter, + }) +} From a888c4091ceec19eb511a1b7f80674baff20205c Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 3 Aug 2025 22:04:07 +0200 Subject: [PATCH 2/4] system prune: delegate confirmation message and validation This adds a "dry-run" / "pre-check" option for prune-functions, which delegates constructing the confirmation message (what is about to be pruned) and validation of the given options to the prune-functions. This helps separating concerns, and doesn't enforce knowledge about what's supported by each content-type onto the system prune command. Signed-off-by: Sebastiaan van Stijn --- cli/command/builder/prune.go | 10 ++++ cli/command/container/prune.go | 5 ++ cli/command/image/prune.go | 12 +++- cli/command/network/prune.go | 5 ++ cli/command/system/prune.go | 86 ++++++++++++++++++----------- cli/command/system/pruner/pruner.go | 35 ++++++++++-- cli/command/volume/prune.go | 11 ++++ 7 files changed, 125 insertions(+), 39 deletions(-) diff --git a/cli/command/builder/prune.go b/cli/command/builder/prune.go index 98bc79305..3748228e6 100644 --- a/cli/command/builder/prune.go +++ b/cli/command/builder/prune.go @@ -122,6 +122,16 @@ func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opt // pruneFn prunes the build cache for use in "docker system prune" and // returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + if !options.Confirmed { + // Dry-run: perform validation and produce confirmation before pruning. + var confirmMsg string + if options.All { + confirmMsg = "all build cache" + } else { + confirmMsg = "unused build cache" + } + return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")} + } return runPrune(ctx, dockerCLI, pruneOptions{ force: true, all: options.All, diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go index 3903d08cd..7b03c1533 100644 --- a/cli/command/container/prune.go +++ b/cli/command/container/prune.go @@ -104,6 +104,11 @@ func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.Fi // pruneFn calls the Container Prune API for use in "docker system prune", // and returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + if !options.Confirmed { + // Dry-run: perform validation and produce confirmation before pruning. + confirmMsg := "all stopped containers" + return 0, confirmMsg, cancelledErr{errors.New("containers prune has been cancelled")} + } return runPrune(ctx, dockerCLI, pruneOptions{ force: true, filter: options.Filter, diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index e0411f5dd..f4b7476e1 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -125,9 +125,19 @@ func RunPrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts. return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter}) } -// pruneFn calls the Container Prune API for use in "docker system prune", +// pruneFn calls the Image Prune API for use in "docker system prune", // and returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + if !options.Confirmed { + // Dry-run: perform validation and produce confirmation before pruning. + var confirmMsg string + if options.All { + confirmMsg = "all images without at least one container associated to them" + } else { + confirmMsg = "all dangling images" + } + return 0, confirmMsg, cancelledErr{errors.New("image prune has been cancelled")} + } return runPrune(ctx, dockerCLI, pruneOptions{ force: true, all: options.All, diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go index 123c11ebf..f388ac966 100644 --- a/cli/command/network/prune.go +++ b/cli/command/network/prune.go @@ -100,6 +100,11 @@ func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.Fi // pruneFn calls the Network Prune API for use in "docker system prune" // and returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + if !options.Confirmed { + // Dry-run: perform validation and produce confirmation before pruning. + confirmMsg := "all networks not used by at least one container" + return 0, confirmMsg, cancelledErr{errors.New("network prune has been cancelled")} + } output, err := runPrune(ctx, dockerCLI, pruneOptions{ force: true, filter: options.Filter, diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index 828b0c4d2..f06772417 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -3,10 +3,12 @@ package system import ( "bytes" "context" + "errors" "fmt" "sort" "text/template" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" @@ -16,7 +18,6 @@ import ( "github.com/docker/go-units" "github.com/fvbommel/sortorder" "github.com/moby/moby/api/types/versions" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -68,16 +69,21 @@ const confirmationTemplate = `WARNING! This will remove: Are you sure you want to continue?` func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) error { - // TODO version this once "until" filter is supported for volumes - if options.pruneVolumes && options.filter.Value().Contains("until") { - return errors.New(`ERROR: The "until" filter is not supported with "--volumes"`) + // prune requires either force, or a user to confirm after prompting. + confirmed := options.force + + // Validate the given options for each pruner and construct a confirmation-message. + confirmationMessage, err := dryRun(ctx, dockerCli, options) + if err != nil { + return err } - if !options.force { - r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)) + if !confirmed { + var err error + confirmed, err = prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage) if err != nil { return err } - if !r { + if !confirmed { return cancelledErr{errors.New("system prune has been cancelled")} } } @@ -100,8 +106,9 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) } spc, output, err := pruneFn(ctx, dockerCli, pruner.PruneOptions{ - All: options.all, - Filter: options.filter, + Confirmed: confirmed, + All: options.all, + Filter: options.filter, }) if err != nil { return err @@ -121,28 +128,42 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// confirmationMessage constructs a confirmation message that depends on the cli options. -func confirmationMessage(dockerCli command.Cli, options pruneOptions) string { - t := template.Must(template.New("confirmation message").Parse(confirmationTemplate)) - - warnings := []string{ - "all stopped containers", - "all networks not used by at least one container", - } - if options.pruneVolumes { - warnings = append(warnings, "all anonymous volumes not used by at least one container") - } - if options.all { - warnings = append(warnings, "all images without at least one container associated to them") - } else { - warnings = append(warnings, "all dangling images") - } - if options.pruneBuildCache { - if options.all { - warnings = append(warnings, "all build cache") - } else { - warnings = append(warnings, "unused build cache") +// dryRun validates the given options for each prune-function and constructs +// a confirmation message that depends on the cli options. +func dryRun(ctx context.Context, dockerCli command.Cli, options pruneOptions) (string, error) { + var ( + errs []error + warnings []string + ) + for contentType, pruneFn := range pruner.List() { + switch contentType { + case pruner.TypeVolume: + if !options.pruneVolumes { + continue + } + case pruner.TypeBuildCache: + if !options.pruneBuildCache { + continue + } } + // Always run with "[pruner.PruneOptions.Confirmed] = false" + // to perform validation of the given options and produce + // a confirmation message for the pruner. + _, confirmMsg, err := pruneFn(ctx, dockerCli, pruner.PruneOptions{ + All: options.all, + Filter: options.filter, + }) + // A "canceled" error is expected in dry-run mode; any other error + // must be returned as a "fatal" error. + if err != nil && !errdefs.IsCanceled(err) { + errs = append(errs, err) + } + if confirmMsg != "" { + warnings = append(warnings, confirmMsg) + } + } + if len(errs) > 0 { + return "", errors.Join(errs...) } var filters []string @@ -161,6 +182,7 @@ func confirmationMessage(dockerCli command.Cli, options pruneOptions) string { } var buffer bytes.Buffer - t.Execute(&buffer, map[string][]string{"warnings": warnings, "filters": filters}) - return buffer.String() + t := template.Must(template.New("confirmation message").Parse(confirmationTemplate)) + _ = t.Execute(&buffer, map[string][]string{"warnings": warnings, "filters": filters}) + return buffer.String(), nil } diff --git a/cli/command/system/pruner/pruner.go b/cli/command/system/pruner/pruner.go index f3701430d..e8a4e6fcd 100644 --- a/cli/command/system/pruner/pruner.go +++ b/cli/command/system/pruner/pruner.go @@ -42,16 +42,39 @@ var pruneOrder = []ContentType{ TypeBuildCache, } -// PruneFunc is the signature for prune-functions. It returns details about -// the content pruned; +// PruneFunc is the signature for prune-functions. The action performed +// depends on the [PruneOptions.Confirmed] field. // -// - spaceReclaimed is the amount of data removed (in bytes). -// - details is arbitrary information about the content pruned. +// - If [PruneOptions.Confirmed] is "false", the PruneFunc must be run +// in "dry-run" mode and return a short description of what content +// will be pruned (for example, "all stopped containers") instead of +// executing the prune. This summary is presented to the user as a +// confirmation message. It may return a [ErrCancelled] to indicate +// the operation was canceled. Any other error is considered a +// validation error of the given options (such as a filter that +// is not supported. +// - If [PruneOptions.Confirmed] is "true", the PruneFunc must execute +// the prune with the given options. +// +// After a successful prune the PruneFunc must return details about the +// content pruned; +// +// - spaceReclaimed is the amount of data removed (in bytes), if any. +// - details is arbitrary information about the content pruned to be +// presented to the user. +// +// [ErrCancelled]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/errdefs#ErrCancelled type PruneFunc func(ctx context.Context, dockerCLI command.Cli, pruneOpts PruneOptions) (spaceReclaimed uint64, details string, _ error) type PruneOptions struct { - All bool - Filter opts.FilterOpt + // Confirmed indicates whether pruning was confirmed (or "forced") + // by the user. If not set, the PruneFunc must be run in "dry-run" + // mode and return a short description of what content will be pruned + // (for example, "all stopped containers") instead of executing the + // prune. This summary is presented to the user as a confirmation message. + Confirmed bool + All bool // Remove all unused content not just dangling (exact meaning differs per content-type). + Filter opts.FilterOpt } // registered holds a map of PruneFunc functions registered through [Register]. diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go index e617529f0..d80eaebd0 100644 --- a/cli/command/volume/prune.go +++ b/cli/command/volume/prune.go @@ -129,6 +129,17 @@ func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.Fi // pruneFn calls the Volume Prune API for use in "docker system prune", // and returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCli command.Cli, options pruner.PruneOptions) (uint64, string, error) { + // TODO version this once "until" filter is supported for volumes + // Ideally, this check wasn't done on the CLI because the list of + // filters that is supported by the daemon may evolve over time. + if options.Filter.Value().Contains("until") { + return 0, "", errors.New(`ERROR: The "until" filter is not supported with "--volumes"`) + } + if !options.Confirmed { + // Dry-run: perform validation and produce confirmation before pruning. + confirmMsg := "all anonymous volumes not used by at least one container" + return 0, confirmMsg, cancelledErr{errors.New("volume prune has been cancelled")} + } return runPrune(ctx, dockerCli, pruneOptions{ force: true, filter: options.Filter, From bf8cb43025d97eb24fa8d8a85821598f0dd240e5 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 3 Aug 2025 22:59:23 +0200 Subject: [PATCH 3/4] system prune: delegate version check Move the version-check for pruners to the pruner, which can return a [ErrNotImplemented] error to indicate they won't be run with the API version that's used. This helps separating concerns, and doesn't enforce knowledge about what's supported by each content-type onto the system prune command. [ErrNotImplemented]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/errdefs#ErrNotImplemented Signed-off-by: Sebastiaan van Stijn --- cli/command/builder/prune.go | 9 +++++++++ cli/command/system/prune.go | 29 +++++++++++------------------ cli/command/system/pruner/pruner.go | 8 +++++--- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/cli/command/builder/prune.go b/cli/command/builder/prune.go index 3748228e6..6add072ad 100644 --- a/cli/command/builder/prune.go +++ b/cli/command/builder/prune.go @@ -14,6 +14,7 @@ import ( "github.com/docker/cli/opts" "github.com/docker/go-units" "github.com/moby/moby/api/types/build" + "github.com/moby/moby/api/types/versions" "github.com/spf13/cobra" ) @@ -112,6 +113,10 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} +type errNotImplemented struct{ error } + +func (errNotImplemented) NotImplemented() {} + // CachePrune executes a prune command for build cache // // Deprecated: this function was only used internally and will be removed in the next release. @@ -122,6 +127,10 @@ func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opt // pruneFn prunes the build cache for use in "docker system prune" and // returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { + if ver := dockerCLI.Client().ClientVersion(); ver != "" && versions.LessThan(ver, "1.31") { + // Not supported on older daemons. + return 0, "", errNotImplemented{errors.New("builder prune requires API version 1.31 or greater")} + } if !options.Confirmed { // Dry-run: perform validation and produce confirmation before pruning. var confirmMsg string diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index f06772417..c6e77f41e 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -17,16 +17,14 @@ import ( "github.com/docker/cli/opts" "github.com/docker/go-units" "github.com/fvbommel/sortorder" - "github.com/moby/moby/api/types/versions" "github.com/spf13/cobra" ) type pruneOptions struct { - force bool - all bool - pruneVolumes bool - pruneBuildCache bool - filter opts.FilterOpt + force bool + all bool + pruneVolumes bool + filter opts.FilterOpt } // newPruneCommand creates a new cobra.Command for `docker prune` @@ -38,7 +36,6 @@ func newPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove unused data", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - options.pruneBuildCache = versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31") return runPrune(cmd.Context(), dockerCli, options) }, Annotations: map[string]string{"version": "1.25"}, @@ -95,11 +92,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) if !options.pruneVolumes { continue } - case pruner.TypeBuildCache: - if !options.pruneBuildCache { - continue - } - case pruner.TypeContainer, pruner.TypeNetwork, pruner.TypeImage: + case pruner.TypeContainer, pruner.TypeNetwork, pruner.TypeImage, pruner.TypeBuildCache: // no special handling; keeping the "exhaustive" linter happy. default: // other pruners; no special handling; keeping the "exhaustive" linter happy. @@ -110,7 +103,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) All: options.all, Filter: options.filter, }) - if err != nil { + if err != nil && !errdefs.IsNotImplemented(err) { return err } spaceReclaimed += spc @@ -141,10 +134,10 @@ func dryRun(ctx context.Context, dockerCli command.Cli, options pruneOptions) (s if !options.pruneVolumes { continue } - case pruner.TypeBuildCache: - if !options.pruneBuildCache { - continue - } + case pruner.TypeContainer, pruner.TypeNetwork, pruner.TypeImage, pruner.TypeBuildCache: + // no special handling; keeping the "exhaustive" linter happy. + default: + // other pruners; no special handling; keeping the "exhaustive" linter happy. } // Always run with "[pruner.PruneOptions.Confirmed] = false" // to perform validation of the given options and produce @@ -155,7 +148,7 @@ func dryRun(ctx context.Context, dockerCli command.Cli, options pruneOptions) (s }) // A "canceled" error is expected in dry-run mode; any other error // must be returned as a "fatal" error. - if err != nil && !errdefs.IsCanceled(err) { + if err != nil && !errdefs.IsCanceled(err) && !errdefs.IsNotImplemented(err) { errs = append(errs, err) } if confirmMsg != "" { diff --git a/cli/command/system/pruner/pruner.go b/cli/command/system/pruner/pruner.go index e8a4e6fcd..f82857b92 100644 --- a/cli/command/system/pruner/pruner.go +++ b/cli/command/system/pruner/pruner.go @@ -50,9 +50,10 @@ var pruneOrder = []ContentType{ // will be pruned (for example, "all stopped containers") instead of // executing the prune. This summary is presented to the user as a // confirmation message. It may return a [ErrCancelled] to indicate -// the operation was canceled. Any other error is considered a -// validation error of the given options (such as a filter that -// is not supported. +// the operation was canceled or a [ErrNotImplemented] if the prune +// function is not implemented for the daemon's API version. Any +// other error is considered a validation error for the given options +// (such as a filter that is not supported). // - If [PruneOptions.Confirmed] is "true", the PruneFunc must execute // the prune with the given options. // @@ -64,6 +65,7 @@ var pruneOrder = []ContentType{ // presented to the user. // // [ErrCancelled]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/errdefs#ErrCancelled +// [ErrNotImplemented]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/errdefs#ErrNotImplemented type PruneFunc func(ctx context.Context, dockerCLI command.Cli, pruneOpts PruneOptions) (spaceReclaimed uint64, details string, _ error) type PruneOptions struct { From 3b6a55653307f4c46cfa6da1985b078e1eaf2c99 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 3 Aug 2025 22:13:01 +0200 Subject: [PATCH 4/4] cli/command: remove exported "RunPrune" functions These are no longer used, and unlikely to be used externally. Signed-off-by: Sebastiaan van Stijn --- cli/command/builder/prune.go | 7 ------- cli/command/container/prune.go | 8 -------- cli/command/image/prune.go | 8 -------- cli/command/network/prune.go | 9 --------- cli/command/volume/prune.go | 8 -------- 5 files changed, 40 deletions(-) diff --git a/cli/command/builder/prune.go b/cli/command/builder/prune.go index 6add072ad..2e57aa2fd 100644 --- a/cli/command/builder/prune.go +++ b/cli/command/builder/prune.go @@ -117,13 +117,6 @@ type errNotImplemented struct{ error } func (errNotImplemented) NotImplemented() {} -// CachePrune executes a prune command for build cache -// -// Deprecated: this function was only used internally and will be removed in the next release. -func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter}) -} - // pruneFn prunes the build cache for use in "docker system prune" and // returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go index 7b03c1533..82c319ccd 100644 --- a/cli/command/container/prune.go +++ b/cli/command/container/prune.go @@ -93,14 +93,6 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// RunPrune calls the Container Prune API -// This returns the amount of space reclaimed and a detailed output string -// -// Deprecated: this function was only used internally and will be removed in the next release. -func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) -} - // pruneFn calls the Container Prune API for use in "docker system prune", // and returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index f4b7476e1..7673a1199 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -117,14 +117,6 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// RunPrune calls the Image Prune API -// This returns the amount of space reclaimed and a detailed output string -// -// Deprecated: this function was only used internally and will be removed in the next release. -func RunPrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter}) -} - // pruneFn calls the Image Prune API for use in "docker system prune", // and returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go index f388ac966..b2b230a60 100644 --- a/cli/command/network/prune.go +++ b/cli/command/network/prune.go @@ -88,15 +88,6 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// RunPrune calls the Network Prune API -// This returns the amount of space reclaimed and a detailed output string -// -// Deprecated: this function was only used internally and will be removed in the next release. -func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { - output, err := runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) - return 0, output, err -} - // pruneFn calls the Network Prune API for use in "docker system prune" // and returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) { diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go index d80eaebd0..dd93aa09b 100644 --- a/cli/command/volume/prune.go +++ b/cli/command/volume/prune.go @@ -118,14 +118,6 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// RunPrune calls the Volume Prune API -// This returns the amount of space reclaimed and a detailed output string -// -// Deprecated: this function was only used internally and will be removed in the next release. -func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) -} - // pruneFn calls the Volume Prune API for use in "docker system prune", // and returns the amount of space reclaimed and a detailed output string. func pruneFn(ctx context.Context, dockerCli command.Cli, options pruner.PruneOptions) (uint64, string, error) {