Skip to content

Commit f1ceb8c

Browse files
authored
Merge pull request docker#6236 from thaJeztah/system_prune_register
system prune: refactor to use "register" functions
2 parents 68fc942 + 3b6a556 commit f1ceb8c

File tree

8 files changed

+348
-75
lines changed

8 files changed

+348
-75
lines changed

cli/command/builder/prune.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,22 @@ import (
99
"github.com/docker/cli/cli"
1010
"github.com/docker/cli/cli/command"
1111
"github.com/docker/cli/cli/command/completion"
12+
"github.com/docker/cli/cli/command/system/pruner"
1213
"github.com/docker/cli/internal/prompt"
1314
"github.com/docker/cli/opts"
1415
"github.com/docker/go-units"
1516
"github.com/moby/moby/api/types/build"
17+
"github.com/moby/moby/api/types/versions"
1618
"github.com/spf13/cobra"
1719
)
1820

21+
func init() {
22+
// Register the prune command to run as part of "docker system prune"
23+
if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil {
24+
panic(err)
25+
}
26+
}
27+
1928
type pruneOptions struct {
2029
force bool
2130
all bool
@@ -104,7 +113,30 @@ type cancelledErr struct{ error }
104113

105114
func (cancelledErr) Cancelled() {}
106115

107-
// CachePrune executes a prune command for build cache
108-
func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
109-
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
116+
type errNotImplemented struct{ error }
117+
118+
func (errNotImplemented) NotImplemented() {}
119+
120+
// pruneFn prunes the build cache for use in "docker system prune" and
121+
// returns the amount of space reclaimed and a detailed output string.
122+
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
123+
if ver := dockerCLI.Client().ClientVersion(); ver != "" && versions.LessThan(ver, "1.31") {
124+
// Not supported on older daemons.
125+
return 0, "", errNotImplemented{errors.New("builder prune requires API version 1.31 or greater")}
126+
}
127+
if !options.Confirmed {
128+
// Dry-run: perform validation and produce confirmation before pruning.
129+
var confirmMsg string
130+
if options.All {
131+
confirmMsg = "all build cache"
132+
} else {
133+
confirmMsg = "unused build cache"
134+
}
135+
return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")}
136+
}
137+
return runPrune(ctx, dockerCLI, pruneOptions{
138+
force: true,
139+
all: options.All,
140+
filter: options.Filter,
141+
})
110142
}

cli/command/container/prune.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ import (
77
"github.com/docker/cli/cli"
88
"github.com/docker/cli/cli/command"
99
"github.com/docker/cli/cli/command/completion"
10+
"github.com/docker/cli/cli/command/system/pruner"
1011
"github.com/docker/cli/internal/prompt"
1112
"github.com/docker/cli/opts"
1213
"github.com/docker/go-units"
1314
"github.com/pkg/errors"
1415
"github.com/spf13/cobra"
1516
)
1617

18+
func init() {
19+
// Register the prune command to run as part of "docker system prune"
20+
if err := pruner.Register(pruner.TypeContainer, pruneFn); err != nil {
21+
panic(err)
22+
}
23+
}
24+
1725
type pruneOptions struct {
1826
force bool
1927
filter opts.FilterOpt
@@ -85,8 +93,16 @@ type cancelledErr struct{ error }
8593

8694
func (cancelledErr) Cancelled() {}
8795

88-
// RunPrune calls the Container Prune API
89-
// This returns the amount of space reclaimed and a detailed output string
90-
func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) {
91-
return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter})
96+
// pruneFn calls the Container Prune API for use in "docker system prune",
97+
// and returns the amount of space reclaimed and a detailed output string.
98+
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
99+
if !options.Confirmed {
100+
// Dry-run: perform validation and produce confirmation before pruning.
101+
confirmMsg := "all stopped containers"
102+
return 0, confirmMsg, cancelledErr{errors.New("containers prune has been cancelled")}
103+
}
104+
return runPrune(ctx, dockerCLI, pruneOptions{
105+
force: true,
106+
filter: options.Filter,
107+
})
92108
}

cli/command/image/prune.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@ import (
99
"github.com/docker/cli/cli"
1010
"github.com/docker/cli/cli/command"
1111
"github.com/docker/cli/cli/command/completion"
12+
"github.com/docker/cli/cli/command/system/pruner"
1213
"github.com/docker/cli/internal/prompt"
1314
"github.com/docker/cli/opts"
1415
"github.com/docker/go-units"
1516
"github.com/pkg/errors"
1617
"github.com/spf13/cobra"
1718
)
1819

20+
func init() {
21+
// Register the prune command to run as part of "docker system prune"
22+
if err := pruner.Register(pruner.TypeImage, pruneFn); err != nil {
23+
panic(err)
24+
}
25+
}
26+
1927
type pruneOptions struct {
2028
force bool
2129
all bool
@@ -109,8 +117,22 @@ type cancelledErr struct{ error }
109117

110118
func (cancelledErr) Cancelled() {}
111119

112-
// RunPrune calls the Image Prune API
113-
// This returns the amount of space reclaimed and a detailed output string
114-
func RunPrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
115-
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
120+
// pruneFn calls the Image Prune API for use in "docker system prune",
121+
// and returns the amount of space reclaimed and a detailed output string.
122+
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
123+
if !options.Confirmed {
124+
// Dry-run: perform validation and produce confirmation before pruning.
125+
var confirmMsg string
126+
if options.All {
127+
confirmMsg = "all images without at least one container associated to them"
128+
} else {
129+
confirmMsg = "all dangling images"
130+
}
131+
return 0, confirmMsg, cancelledErr{errors.New("image prune has been cancelled")}
132+
}
133+
return runPrune(ctx, dockerCLI, pruneOptions{
134+
force: true,
135+
all: options.All,
136+
filter: options.Filter,
137+
})
116138
}

cli/command/network/prune.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@ import (
77

88
"github.com/docker/cli/cli"
99
"github.com/docker/cli/cli/command"
10+
"github.com/docker/cli/cli/command/system/pruner"
1011
"github.com/docker/cli/internal/prompt"
1112
"github.com/docker/cli/opts"
1213
"github.com/spf13/cobra"
1314
)
1415

16+
func init() {
17+
// Register the prune command to run as part of "docker system prune"
18+
if err := pruner.Register(pruner.TypeNetwork, pruneFn); err != nil {
19+
panic(err)
20+
}
21+
}
22+
1523
type pruneOptions struct {
1624
force bool
1725
filter opts.FilterOpt
@@ -80,9 +88,17 @@ type cancelledErr struct{ error }
8088

8189
func (cancelledErr) Cancelled() {}
8290

83-
// RunPrune calls the Network Prune API
84-
// This returns the amount of space reclaimed and a detailed output string
85-
func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) {
86-
output, err := runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter})
91+
// pruneFn calls the Network Prune API for use in "docker system prune"
92+
// and returns the amount of space reclaimed and a detailed output string.
93+
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
94+
if !options.Confirmed {
95+
// Dry-run: perform validation and produce confirmation before pruning.
96+
confirmMsg := "all networks not used by at least one container"
97+
return 0, confirmMsg, cancelledErr{errors.New("network prune has been cancelled")}
98+
}
99+
output, err := runPrune(ctx, dockerCLI, pruneOptions{
100+
force: true,
101+
filter: options.Filter,
102+
})
87103
return 0, output, err
88104
}

cli/command/system/prune.go

Lines changed: 74 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,28 @@ package system
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"fmt"
78
"sort"
89
"text/template"
910

11+
"github.com/containerd/errdefs"
1012
"github.com/docker/cli/cli"
1113
"github.com/docker/cli/cli/command"
12-
"github.com/docker/cli/cli/command/builder"
1314
"github.com/docker/cli/cli/command/completion"
14-
"github.com/docker/cli/cli/command/container"
15-
"github.com/docker/cli/cli/command/image"
16-
"github.com/docker/cli/cli/command/network"
17-
"github.com/docker/cli/cli/command/volume"
15+
"github.com/docker/cli/cli/command/system/pruner"
1816
"github.com/docker/cli/internal/prompt"
1917
"github.com/docker/cli/opts"
2018
"github.com/docker/go-units"
2119
"github.com/fvbommel/sortorder"
22-
"github.com/moby/moby/api/types/versions"
23-
"github.com/pkg/errors"
2420
"github.com/spf13/cobra"
2521
)
2622

2723
type pruneOptions struct {
28-
force bool
29-
all bool
30-
pruneVolumes bool
31-
pruneBuildCache bool
32-
filter opts.FilterOpt
24+
force bool
25+
all bool
26+
pruneVolumes bool
27+
filter opts.FilterOpt
3328
}
3429

3530
// newPruneCommand creates a new cobra.Command for `docker prune`
@@ -41,7 +36,6 @@ func newPruneCommand(dockerCli command.Cli) *cobra.Command {
4136
Short: "Remove unused data",
4237
Args: cli.NoArgs,
4338
RunE: func(cmd *cobra.Command, args []string) error {
44-
options.pruneBuildCache = versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
4539
return runPrune(cmd.Context(), dockerCli, options)
4640
},
4741
Annotations: map[string]string{"version": "1.25"},
@@ -72,35 +66,44 @@ const confirmationTemplate = `WARNING! This will remove:
7266
Are you sure you want to continue?`
7367

7468
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) error {
75-
// TODO version this once "until" filter is supported for volumes
76-
if options.pruneVolumes && options.filter.Value().Contains("until") {
77-
return errors.New(`ERROR: The "until" filter is not supported with "--volumes"`)
69+
// prune requires either force, or a user to confirm after prompting.
70+
confirmed := options.force
71+
72+
// Validate the given options for each pruner and construct a confirmation-message.
73+
confirmationMessage, err := dryRun(ctx, dockerCli, options)
74+
if err != nil {
75+
return err
7876
}
79-
if !options.force {
80-
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options))
77+
if !confirmed {
78+
var err error
79+
confirmed, err = prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage)
8180
if err != nil {
8281
return err
8382
}
84-
if !r {
83+
if !confirmed {
8584
return cancelledErr{errors.New("system prune has been cancelled")}
8685
}
8786
}
88-
pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){
89-
container.RunPrune,
90-
network.RunPrune,
91-
}
92-
if options.pruneVolumes {
93-
pruneFuncs = append(pruneFuncs, volume.RunPrune)
94-
}
95-
pruneFuncs = append(pruneFuncs, image.RunPrune)
96-
if options.pruneBuildCache {
97-
pruneFuncs = append(pruneFuncs, builder.CachePrune)
98-
}
9987

10088
var spaceReclaimed uint64
101-
for _, pruneFn := range pruneFuncs {
102-
spc, output, err := pruneFn(ctx, dockerCli, options.all, options.filter)
103-
if err != nil {
89+
for contentType, pruneFn := range pruner.List() {
90+
switch contentType {
91+
case pruner.TypeVolume:
92+
if !options.pruneVolumes {
93+
continue
94+
}
95+
case pruner.TypeContainer, pruner.TypeNetwork, pruner.TypeImage, pruner.TypeBuildCache:
96+
// no special handling; keeping the "exhaustive" linter happy.
97+
default:
98+
// other pruners; no special handling; keeping the "exhaustive" linter happy.
99+
}
100+
101+
spc, output, err := pruneFn(ctx, dockerCli, pruner.PruneOptions{
102+
Confirmed: confirmed,
103+
All: options.all,
104+
Filter: options.filter,
105+
})
106+
if err != nil && !errdefs.IsNotImplemented(err) {
104107
return err
105108
}
106109
spaceReclaimed += spc
@@ -118,29 +121,43 @@ type cancelledErr struct{ error }
118121

119122
func (cancelledErr) Cancelled() {}
120123

121-
// confirmationMessage constructs a confirmation message that depends on the cli options.
122-
func confirmationMessage(dockerCli command.Cli, options pruneOptions) string {
123-
t := template.Must(template.New("confirmation message").Parse(confirmationTemplate))
124-
125-
warnings := []string{
126-
"all stopped containers",
127-
"all networks not used by at least one container",
128-
}
129-
if options.pruneVolumes {
130-
warnings = append(warnings, "all anonymous volumes not used by at least one container")
131-
}
132-
if options.all {
133-
warnings = append(warnings, "all images without at least one container associated to them")
134-
} else {
135-
warnings = append(warnings, "all dangling images")
136-
}
137-
if options.pruneBuildCache {
138-
if options.all {
139-
warnings = append(warnings, "all build cache")
140-
} else {
141-
warnings = append(warnings, "unused build cache")
124+
// dryRun validates the given options for each prune-function and constructs
125+
// a confirmation message that depends on the cli options.
126+
func dryRun(ctx context.Context, dockerCli command.Cli, options pruneOptions) (string, error) {
127+
var (
128+
errs []error
129+
warnings []string
130+
)
131+
for contentType, pruneFn := range pruner.List() {
132+
switch contentType {
133+
case pruner.TypeVolume:
134+
if !options.pruneVolumes {
135+
continue
136+
}
137+
case pruner.TypeContainer, pruner.TypeNetwork, pruner.TypeImage, pruner.TypeBuildCache:
138+
// no special handling; keeping the "exhaustive" linter happy.
139+
default:
140+
// other pruners; no special handling; keeping the "exhaustive" linter happy.
141+
}
142+
// Always run with "[pruner.PruneOptions.Confirmed] = false"
143+
// to perform validation of the given options and produce
144+
// a confirmation message for the pruner.
145+
_, confirmMsg, err := pruneFn(ctx, dockerCli, pruner.PruneOptions{
146+
All: options.all,
147+
Filter: options.filter,
148+
})
149+
// A "canceled" error is expected in dry-run mode; any other error
150+
// must be returned as a "fatal" error.
151+
if err != nil && !errdefs.IsCanceled(err) && !errdefs.IsNotImplemented(err) {
152+
errs = append(errs, err)
153+
}
154+
if confirmMsg != "" {
155+
warnings = append(warnings, confirmMsg)
142156
}
143157
}
158+
if len(errs) > 0 {
159+
return "", errors.Join(errs...)
160+
}
144161

145162
var filters []string
146163
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
@@ -158,6 +175,7 @@ func confirmationMessage(dockerCli command.Cli, options pruneOptions) string {
158175
}
159176

160177
var buffer bytes.Buffer
161-
t.Execute(&buffer, map[string][]string{"warnings": warnings, "filters": filters})
162-
return buffer.String()
178+
t := template.Must(template.New("confirmation message").Parse(confirmationTemplate))
179+
_ = t.Execute(&buffer, map[string][]string{"warnings": warnings, "filters": filters})
180+
return buffer.String(), nil
163181
}

cli/command/system/prune_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ import (
1313
"github.com/moby/moby/api/types/network"
1414
"gotest.tools/v3/assert"
1515
is "gotest.tools/v3/assert/cmp"
16+
17+
// Make sure pruners are registered for tests (they're included automatically when building).
18+
_ "github.com/docker/cli/cli/command/builder"
19+
_ "github.com/docker/cli/cli/command/container"
20+
_ "github.com/docker/cli/cli/command/image"
21+
_ "github.com/docker/cli/cli/command/network"
22+
_ "github.com/docker/cli/cli/command/volume"
1623
)
1724

1825
func TestPrunePromptPre131DoesNotIncludeBuildCache(t *testing.T) {

0 commit comments

Comments
 (0)