volumes: prune: add --all / -a option

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2023-04-20 12:54:56 +02:00
parent d4f2609ce8
commit 0dec5d20a2
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
10 changed files with 117 additions and 22 deletions

View File

@ -8,11 +8,15 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs"
units "github.com/docker/go-units" units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type pruneOptions struct { type pruneOptions struct {
all bool
force bool force bool
filter opts.FilterOpt filter opts.FilterOpt
} }
@ -41,18 +45,37 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused volumes, not just anonymous ones")
flags.SetAnnotation("all", "version", []string{"1.42"})
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation") flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
flags.Var(&options.filter, "filter", `Provide filter values (e.g. "label=<label>")`) flags.Var(&options.filter, "filter", `Provide filter values (e.g. "label=<label>")`)
return cmd return cmd
} }
const warning = `WARNING! This will remove all local volumes not used by at least one container. const (
unusedVolumesWarning = `WARNING! This will remove anonymous local volumes not used by at least one container.
Are you sure you want to continue?` Are you sure you want to continue?`
allVolumesWarning = `WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue?`
)
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) { func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
warning := unusedVolumesWarning
if versions.GreaterThanOrEqualTo(dockerCli.CurrentVersion(), "1.42") {
if options.all {
if pruneFilters.Contains("all") {
return 0, "", errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --all and --filter all=1"))
}
pruneFilters.Add("all", "true")
warning = allVolumesWarning
}
} else {
// API < v1.42 removes all volumes (anonymous and named) by default.
warning = allVolumesWarning
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil return 0, "", nil
} }

View File

@ -13,22 +13,26 @@ import (
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
"gotest.tools/v3/skip" "gotest.tools/v3/skip"
) )
func TestVolumePruneErrors(t *testing.T) { func TestVolumePruneErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
name string
args []string args []string
flags map[string]string flags map[string]string
volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error) volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error)
expectedError string expectedError string
}{ }{
{ {
name: "accepts no arguments",
args: []string{"foo"}, args: []string{"foo"},
expectedError: "accepts no argument", expectedError: "accepts no argument",
}, },
{ {
name: "forced but other error",
flags: map[string]string{ flags: map[string]string{
"force": "true", "force": "true",
}, },
@ -37,20 +41,75 @@ func TestVolumePruneErrors(t *testing.T) {
}, },
expectedError: "error pruning volumes", expectedError: "error pruning volumes",
}, },
{
name: "conflicting options",
flags: map[string]string{
"all": "true",
"filter": "all=1",
},
expectedError: "conflicting options: cannot specify both --all and --filter all=1",
},
} }
for _, tc := range testCases { for _, tc := range testCases {
cmd := NewPruneCommand( tc := tc
test.NewFakeCli(&fakeClient{ t.Run(tc.name, func(t *testing.T) {
volumePruneFunc: tc.volumePruneFunc, cmd := NewPruneCommand(
}), test.NewFakeCli(&fakeClient{
) volumePruneFunc: tc.volumePruneFunc,
cmd.SetArgs(tc.args) }),
for key, value := range tc.flags { )
cmd.Flags().Set(key, value) cmd.SetArgs(tc.args)
} for key, value := range tc.flags {
cmd.SetOut(io.Discard) cmd.Flags().Set(key, value)
cmd.SetErr(io.Discard) }
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
}
}
func TestVolumePruneSuccess(t *testing.T) {
testCases := []struct {
name string
args []string
volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error)
}{
{
name: "all",
args: []string{"--all"},
volumePruneFunc: func(pruneFilter filters.Args) (types.VolumesPruneReport, error) {
assert.Check(t, is.Equal([]string{"true"}, pruneFilter.Get("all")))
return types.VolumesPruneReport{}, nil
},
},
{
name: "all-forced",
args: []string{"--all", "--force"},
volumePruneFunc: func(pruneFilter filters.Args) (types.VolumesPruneReport, error) {
return types.VolumesPruneReport{}, nil
},
},
{
name: "label-filter",
args: []string{"--filter", "label=foobar"},
volumePruneFunc: func(pruneFilter filters.Args) (types.VolumesPruneReport, error) {
assert.Check(t, is.Equal([]string{"foobar"}, pruneFilter.Get("label")))
return types.VolumesPruneReport{}, nil
},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{volumePruneFunc: tc.volumePruneFunc})
cmd := NewPruneCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NilError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("volume-prune-success.%s.golden", tc.name))
})
} }
} }

View File

@ -1,2 +1,2 @@
WARNING! This will remove all local volumes not used by at least one container. WARNING! This will remove anonymous local volumes not used by at least one container.
Are you sure you want to continue? [y/N] Total reclaimed space: 0B Are you sure you want to continue? [y/N] Total reclaimed space: 0B

View File

@ -0,0 +1 @@
Total reclaimed space: 0B

View File

@ -0,0 +1,2 @@
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] Total reclaimed space: 0B

View File

@ -0,0 +1,2 @@
WARNING! This will remove anonymous local volumes not used by at least one container.
Are you sure you want to continue? [y/N] Total reclaimed space: 0B

View File

@ -1,4 +1,4 @@
WARNING! This will remove all local volumes not used by at least one container. WARNING! This will remove anonymous local volumes not used by at least one container.
Are you sure you want to continue? [y/N] Deleted Volumes: Are you sure you want to continue? [y/N] Deleted Volumes:
foo foo
bar bar

View File

@ -5383,7 +5383,7 @@ _docker_volume_prune() {
case "$cur" in case "$cur" in
-*) -*)
COMPREPLY=( $( compgen -W "--filter --force -f --help" -- "$cur" ) ) COMPREPLY=( $( compgen -W "--all -a --filter --force -f --help" -- "$cur" ) )
;; ;;
esac esac
} }

View File

@ -2527,6 +2527,8 @@ __docker_volume_subcommand() {
(prune) (prune)
_arguments $(__docker_arguments) \ _arguments $(__docker_arguments) \
$opts_help \ $opts_help \
"($help -a --all)"{-a,--all}"[Remove all unused local volumes, not just anonymous ones]" \
"($help)*--filter=[Filter values]:filter:__docker_complete_prune_filters" \
"($help -f --force)"{-f,--force}"[Do not prompt for confirmation]" && ret=0 "($help -f --force)"{-f,--force}"[Do not prompt for confirmation]" && ret=0
;; ;;
(rm) (rm)

View File

@ -5,24 +5,26 @@ Remove all unused local volumes
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:----------------------|:---------|:--------|:---------------------------------------------| |:------------------------------|:---------|:--------|:---------------------------------------------------|
| [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `label=<label>`) | | [`-a`](#all), [`--all`](#all) | | | Remove all unused volumes, not just anonymous ones |
| `-f`, `--force` | | | Do not prompt for confirmation | | [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `label=<label>`) |
| `-f`, `--force` | | | Do not prompt for confirmation |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->
## Description ## Description
Remove all unused local volumes. Unused local volumes are those which are not referenced by any containers Remove all unused local volumes. Unused local volumes are those which are not
referenced by any containers. By default, it only removes anonymous volumes.
## Examples ## Examples
```console ```console
$ docker volume prune $ docker volume prune
WARNING! This will remove all local volumes not used by at least one container. WARNING! This will remove anonymous local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y Are you sure you want to continue? [y/N] y
Deleted Volumes: Deleted Volumes:
07c7bdf3e34ab76d921894c2b834f073721fccfbbcba792aa7648e3a7a664c2e 07c7bdf3e34ab76d921894c2b834f073721fccfbbcba792aa7648e3a7a664c2e
@ -31,6 +33,10 @@ my-named-vol
Total reclaimed space: 36 B Total reclaimed space: 36 B
``` ```
### <a name="all"></a> Filtering (--all, -a)
Use the `--all` flag to prune both unused anonymous and named volumes.
### <a name="filter"></a> Filtering (--filter) ### <a name="filter"></a> Filtering (--filter)
The filtering flag (`--filter`) format is of "key=value". If there is more The filtering flag (`--filter`) format is of "key=value". If there is more