diff options
-rw-r--r-- | cmd/podman/volumes/prune.go | 16 | ||||
-rw-r--r-- | docs/source/markdown/podman-volume-prune.1.md | 16 | ||||
-rw-r--r-- | libpod/runtime_volume.go | 4 | ||||
-rw-r--r-- | pkg/api/handlers/compat/volumes.go | 12 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/volumes.go | 20 | ||||
-rw-r--r-- | pkg/bindings/test/volumes_test.go | 41 | ||||
-rw-r--r-- | pkg/bindings/volumes/volumes.go | 12 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 2 | ||||
-rw-r--r-- | pkg/domain/entities/volumes.go | 7 | ||||
-rw-r--r-- | pkg/domain/filters/helpers.go | 20 | ||||
-rw-r--r-- | pkg/domain/filters/volumes.go | 3 | ||||
-rw-r--r-- | pkg/domain/infra/abi/system.go | 2 | ||||
-rw-r--r-- | pkg/domain/infra/abi/volumes.go | 12 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/volumes.go | 4 | ||||
-rw-r--r-- | test/apiv2/30-volumes.at | 30 | ||||
-rw-r--r-- | test/e2e/volume_prune_test.go | 60 |
16 files changed, 236 insertions, 25 deletions
diff --git a/cmd/podman/volumes/prune.go b/cmd/podman/volumes/prune.go index d1370120b..0f3ba9ef6 100644 --- a/cmd/podman/volumes/prune.go +++ b/cmd/podman/volumes/prune.go @@ -8,10 +8,12 @@ import ( "strings" "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v2/cmd/podman/common" "github.com/containers/podman/v2/cmd/podman/registry" "github.com/containers/podman/v2/cmd/podman/utils" "github.com/containers/podman/v2/cmd/podman/validate" "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/podman/v2/pkg/domain/filters" "github.com/spf13/cobra" ) @@ -28,6 +30,7 @@ var ( RunE: prune, ValidArgsFunction: completion.AutocompleteNone, } + filter = []string{} ) func init() { @@ -37,10 +40,17 @@ func init() { Parent: volumeCmd, }) flags := pruneCommand.Flags() + + filterFlagName := "filter" + flags.StringArrayVar(&filter, filterFlagName, []string{}, "Provide filter values (e.g. 'label=<key>=<value>')") + _ = pruneCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteVolumeFilters) flags.BoolP("force", "f", false, "Do not prompt for confirmation") } func prune(cmd *cobra.Command, args []string) error { + var ( + pruneOptions = entities.VolumePruneOptions{} + ) // Prompt for confirmation if --force is not set force, err := cmd.Flags().GetBool("force") if err != nil { @@ -58,7 +68,11 @@ func prune(cmd *cobra.Command, args []string) error { return nil } } - responses, err := registry.ContainerEngine().VolumePrune(context.Background()) + pruneOptions.Filters, err = filters.ParseFilterArgumentsIntoFilters(filter) + if err != nil { + return err + } + responses, err := registry.ContainerEngine().VolumePrune(context.Background(), pruneOptions) if err != nil { return err } diff --git a/docs/source/markdown/podman-volume-prune.1.md b/docs/source/markdown/podman-volume-prune.1.md index b5f1b7e94..9477cb5d5 100644 --- a/docs/source/markdown/podman-volume-prune.1.md +++ b/docs/source/markdown/podman-volume-prune.1.md @@ -8,7 +8,8 @@ podman\-volume\-prune - Remove all unused volumes ## DESCRIPTION -Removes all unused volumes. You will be prompted to confirm the removal of all the +Removes unused volumes. By default all unused volumes will be removed, the **--filter** flag can +be used to filter specific volumes. You will be prompted to confirm the removal of all the unused volumes. To bypass the confirmation, use the **--force** flag. @@ -18,6 +19,17 @@ unused volumes. To bypass the confirmation, use the **--force** flag. Do not prompt for confirmation. +#### **--filter** + +Filter volumes to be pruned. Volumes can be filtered by the following attributes: + +- dangling +- driver +- label +- name +- opt +- scope + #### **--help** Print usage statement @@ -29,6 +41,8 @@ Print usage statement $ podman volume prune $ podman volume prune --force + +$ podman volume prune --filter label=mylabel=mylabelvalue ``` ## SEE ALSO diff --git a/libpod/runtime_volume.go b/libpod/runtime_volume.go index 055a243c0..10c32a119 100644 --- a/libpod/runtime_volume.go +++ b/libpod/runtime_volume.go @@ -133,9 +133,9 @@ func (r *Runtime) GetAllVolumes() ([]*Volume, error) { } // PruneVolumes removes unused volumes from the system -func (r *Runtime) PruneVolumes(ctx context.Context) (map[string]error, error) { +func (r *Runtime) PruneVolumes(ctx context.Context, filterFuncs []VolumeFilter) (map[string]error, error) { reports := make(map[string]error) - vols, err := r.GetAllVolumes() + vols, err := r.Volumes(filterFuncs...) if err != nil { return nil, err } diff --git a/pkg/api/handlers/compat/volumes.go b/pkg/api/handlers/compat/volumes.go index 71b848932..f76e18ee3 100644 --- a/pkg/api/handlers/compat/volumes.go +++ b/pkg/api/handlers/compat/volumes.go @@ -3,6 +3,7 @@ package compat import ( "encoding/json" "net/http" + "net/url" "time" "github.com/containers/podman/v2/libpod" @@ -254,14 +255,15 @@ func PruneVolumes(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) return } - // TODO: We have no ability to pass pruning filters to `PruneVolumes()` so - // we'll explicitly reject the request if we see any - if len(query.Filters) > 0 { - utils.InternalServerError(w, errors.New("filters for pruning volumes is not implemented")) + + f := (url.Values)(query.Filters) + filterFuncs, err := filters.GenerateVolumeFilters(f) + if err != nil { + utils.Error(w, "Something when wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse filters for %s", f.Encode())) return } - pruned, err := runtime.PruneVolumes(r.Context()) + pruned, err := runtime.PruneVolumes(r.Context(), filterFuncs) if err != nil { utils.InternalServerError(w, err) return diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index b0d40fd8b..b02a6a8ce 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -3,6 +3,7 @@ package libpod import ( "encoding/json" "net/http" + "net/url" "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod/define" @@ -180,8 +181,25 @@ func PruneVolumes(w http.ResponseWriter, r *http.Request) { func pruneVolumesHelper(r *http.Request) ([]*entities.VolumePruneReport, error) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) ) - pruned, err := runtime.PruneVolumes(r.Context()) + query := struct { + Filters map[string][]string `schema:"filters"` + }{ + // override any golang type defaults + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + return nil, err + } + + f := (url.Values)(query.Filters) + filterFuncs, err := filters.GenerateVolumeFilters(f) + if err != nil { + return nil, err + } + + pruned, err := runtime.PruneVolumes(r.Context(), filterFuncs) if err != nil { return nil, err } diff --git a/pkg/bindings/test/volumes_test.go b/pkg/bindings/test/volumes_test.go index dc90d4d00..861a02441 100644 --- a/pkg/bindings/test/volumes_test.go +++ b/pkg/bindings/test/volumes_test.go @@ -144,16 +144,15 @@ var _ = Describe("Podman volumes", func() { Expect(vols[0].Name).To(Equal("homer")) }) - // TODO we need to add filtering to tests It("prune unused volume", func() { // Pruning when no volumes present should be ok - _, err := volumes.Prune(connText) + _, err := volumes.Prune(connText, nil) Expect(err).To(BeNil()) // Removing an unused volume should work _, err = volumes.Create(connText, entities.VolumeCreateOptions{}) Expect(err).To(BeNil()) - vols, err := volumes.Prune(connText) + vols, err := volumes.Prune(connText, nil) Expect(err).To(BeNil()) Expect(len(vols)).To(BeNumerically("==", 1)) @@ -163,11 +162,45 @@ var _ = Describe("Podman volumes", func() { Expect(err).To(BeNil()) session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"}) session.Wait(45) - vols, err = volumes.Prune(connText) + vols, err = volumes.Prune(connText, nil) Expect(err).To(BeNil()) Expect(len(vols)).To(BeNumerically("==", 1)) _, err = volumes.Inspect(connText, "homer") Expect(err).To(BeNil()) + + // Removing volume with non matching filter shouldn't prune any volumes + filters := make(map[string][]string) + filters["label"] = []string{"label1=idontmatch"} + _, err = volumes.Create(connText, entities.VolumeCreateOptions{Label: map[string]string{ + "label1": "value1", + }}) + Expect(err).To(BeNil()) + vols, err = volumes.Prune(connText, filters) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 0)) + vol2, err := volumes.Create(connText, entities.VolumeCreateOptions{Label: map[string]string{ + "label1": "value2", + }}) + Expect(err).To(BeNil()) + _, err = volumes.Create(connText, entities.VolumeCreateOptions{Label: map[string]string{ + "label1": "value3", + }}) + Expect(err).To(BeNil()) + + // Removing volume with matching filter label and value should remove specific entry + filters = make(map[string][]string) + filters["label"] = []string{"label1=value2"} + vols, err = volumes.Prune(connText, filters) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 1)) + Expect(vols[0].Id).To(Equal(vol2.Name)) + + // Removing volumes with matching filter label should remove all matching volumes + filters = make(map[string][]string) + filters["label"] = []string{"label1"} + vols, err = volumes.Prune(connText, filters) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 2)) }) }) diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go index 00f1e5720..b1be257b8 100644 --- a/pkg/bindings/volumes/volumes.go +++ b/pkg/bindings/volumes/volumes.go @@ -75,7 +75,7 @@ func List(ctx context.Context, filters map[string][]string) ([]*entities.VolumeL } // Prune removes unused volumes from the local filesystem. -func Prune(ctx context.Context) ([]*entities.VolumePruneReport, error) { +func Prune(ctx context.Context, filters map[string][]string) ([]*entities.VolumePruneReport, error) { var ( pruned []*entities.VolumePruneReport ) @@ -83,7 +83,15 @@ func Prune(ctx context.Context) ([]*entities.VolumePruneReport, error) { if err != nil { return nil, err } - response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/prune", nil, nil) + params := url.Values{} + if len(filters) > 0 { + strFilters, err := bindings.FiltersToString(filters) + if err != nil { + return nil, err + } + params.Set("filters", strFilters) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/prune", params, nil) if err != nil { return nil, err } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 5ad475133..ac9073402 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -80,6 +80,6 @@ type ContainerEngine interface { VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IDOrNameResponse, error) VolumeInspect(ctx context.Context, namesOrIds []string, opts InspectOptions) ([]*VolumeInspectReport, []error, error) VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) - VolumePrune(ctx context.Context) ([]*VolumePruneReport, error) + VolumePrune(ctx context.Context, options VolumePruneOptions) ([]*VolumePruneReport, error) VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error) } diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go index 1bc1e4301..e6b29e374 100644 --- a/pkg/domain/entities/volumes.go +++ b/pkg/domain/entities/volumes.go @@ -1,6 +1,7 @@ package entities import ( + "net/url" "time" docker_api_types "github.com/docker/docker/api/types" @@ -109,6 +110,12 @@ type VolumeInspectReport struct { *VolumeConfigResponse } +// VolumePruneOptions describes the options needed +// to prune a volume from the CLI +type VolumePruneOptions struct { + Filters url.Values `json:"filters" schema:"filters"` +} + type VolumePruneReport struct { Err error Id string //nolint diff --git a/pkg/domain/filters/helpers.go b/pkg/domain/filters/helpers.go new file mode 100644 index 000000000..6a5fb68b1 --- /dev/null +++ b/pkg/domain/filters/helpers.go @@ -0,0 +1,20 @@ +package filters + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" +) + +func ParseFilterArgumentsIntoFilters(filters []string) (url.Values, error) { + parsedFilters := make(url.Values) + for _, f := range filters { + t := strings.SplitN(f, "=", 2) + if len(t) < 2 { + return parsedFilters, errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) + } + parsedFilters.Add(t[0], t[1]) + } + return parsedFilters, nil +} diff --git a/pkg/domain/filters/volumes.go b/pkg/domain/filters/volumes.go index 7819d3cdf..69bef4961 100644 --- a/pkg/domain/filters/volumes.go +++ b/pkg/domain/filters/volumes.go @@ -1,13 +1,14 @@ package filters import ( + "net/url" "strings" "github.com/containers/podman/v2/libpod" "github.com/pkg/errors" ) -func GenerateVolumeFilters(filters map[string][]string) ([]libpod.VolumeFilter, error) { +func GenerateVolumeFilters(filters url.Values) ([]libpod.VolumeFilter, error) { var vf []libpod.VolumeFilter for filter, v := range filters { for _, val := range v { diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index d6881fdc4..b6da364fc 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -224,7 +224,7 @@ func (ic *ContainerEngine) SystemPrune(ctx context.Context, options entities.Sys systemPruneReport.ImagePruneReport.Report.Id = append(systemPruneReport.ImagePruneReport.Report.Id, results...) } if options.Volume { - volumePruneReport, err := ic.pruneVolumesHelper(ctx) + volumePruneReport, err := ic.pruneVolumesHelper(ctx, nil) if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index a7262f61b..515e52754 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -127,12 +127,16 @@ func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []strin return reports, errs, nil } -func (ic *ContainerEngine) VolumePrune(ctx context.Context) ([]*entities.VolumePruneReport, error) { - return ic.pruneVolumesHelper(ctx) +func (ic *ContainerEngine) VolumePrune(ctx context.Context, options entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) { + filterFuncs, err := filters.GenerateVolumeFilters(options.Filters) + if err != nil { + return nil, err + } + return ic.pruneVolumesHelper(ctx, filterFuncs) } -func (ic *ContainerEngine) pruneVolumesHelper(ctx context.Context) ([]*entities.VolumePruneReport, error) { - pruned, err := ic.Libpod.PruneVolumes(ctx) +func (ic *ContainerEngine) pruneVolumesHelper(ctx context.Context, filterFuncs []libpod.VolumeFilter) ([]*entities.VolumePruneReport, error) { + pruned, err := ic.Libpod.PruneVolumes(ctx, filterFuncs) if err != nil { return nil, err } diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index c0df2bb7b..b431fc8bd 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -68,8 +68,8 @@ func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []strin return reports, errs, nil } -func (ic *ContainerEngine) VolumePrune(ctx context.Context) ([]*entities.VolumePruneReport, error) { - return volumes.Prune(ic.ClientCxt) +func (ic *ContainerEngine) VolumePrune(ctx context.Context, opts entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) { + return volumes.Prune(ic.ClientCxt, (map[string][]string)(opts.Filters)) } func (ic *ContainerEngine) VolumeList(ctx context.Context, opts entities.VolumeListOptions) ([]*entities.VolumeListReport, error) { diff --git a/test/apiv2/30-volumes.at b/test/apiv2/30-volumes.at index aa167a97a..2cfca9d08 100644 --- a/test/apiv2/30-volumes.at +++ b/test/apiv2/30-volumes.at @@ -20,6 +20,18 @@ t POST libpod/volumes/create \ .Labels.testlabel=testonly \ .Options.type=tmpfs \ .Options.o=nodev,noexec +t POST libpod/volumes/create \ + '"Name":"foo3","Label":{"testlabel":""},"Options":{"type":"tmpfs","o":"nodev,noexec"}}' 201 \ + .Name=foo3 \ + .Labels.testlabel="" \ + .Options.type=tmpfs \ + .Options.o=nodev,noexec +t POST libpod/volumes/create \ + '"Name":"foo4","Label":{"testlabel1":"testonly"},"Options":{"type":"tmpfs","o":"nodev,noexec"}}' 201 \ + .Name=foo4 \ + .Labels.testlabel1=testonly \ + .Options.type=tmpfs \ + .Options.o=nodev,noexec # Negative test # We have created a volume named "foo1" @@ -39,6 +51,12 @@ t GET libpod/volumes/json?filters=%7B%22name%22%3A%5B%22foo1%22%5D%7D 200 length t GET libpod/volumes/json?filters=%7B%22name%22%3A%20%5B%22foo1%22%2C%20%22foo2%22%5D%7D 200 length=2 .[0].Name=foo1 .[1].Name=foo2 # -G --data-urlencode 'filters={"name":["notexist"]}' t GET libpod/volumes/json?filters=%7B%22name%22%3A%5B%22notexists%22%5D%7D 200 length=0 +# -G --data-urlencode 'filters={"label":["testlabel"]}' +t GET libpod/volumes/json?filters=%7B%22label%22:%5B%22testlabel%22%5D%7D 200 length=2 +# -G --data-urlencode 'filters={"label":["testlabel=testonly"]}' +t GET libpod/volumes/json?filters=%7B%22label%22:%5B%22testlabel=testonly%22%5D%7D 200 length=1 +# -G --data-urlencode 'filters={"label":["testlabel1=testonly"]}' +t GET libpod/volumes/json?filters=%7B%22label%22:%5B%22testlabel1=testonly%22%5D%7D 200 length=1 ## inspect volume t GET libpod/volumes/foo1/json 200 \ @@ -60,6 +78,18 @@ t DELETE libpod/volumes/foo1 404 \ .message~.* \ .response=404 +## Prune volumes with label matching 'testlabel1=testonly' +# -G --data-urlencode 'filters={"label":["testlabel1=testonly"]}' +t POST libpod/volumes/prune?filters=%7B%22label%22:%5B%22testlabel1=testonly%22%5D%7D "" 200 +# -G --data-urlencode 'filters={"label":["testlabel1=testonly"]}' +t GET libpod/volumes/json?filters=%7B%22label%22:%5B%22testlabel1=testonly%22%5D%7D 200 length=0 + +## Prune volumes with label matching 'testlabel' +# -G --data-urlencode 'filters={"label":["testlabel"]}' +t POST libpod/volumes/prune?filters=%7B%22label%22:%5B%22testlabel%22%5D%7D "" 200 +# -G --data-urlencode 'filters={"label":["testlabel"]}' +t GET libpod/volumes/json?filters=%7B%22label%22:%5B%22testlabel%22%5D%7D 200 length=0 + ## Prune volumes t POST libpod/volumes/prune "" 200 #After prune volumes, there should be no volume existing diff --git a/test/e2e/volume_prune_test.go b/test/e2e/volume_prune_test.go index c8521ebe7..a910c47a7 100644 --- a/test/e2e/volume_prune_test.go +++ b/test/e2e/volume_prune_test.go @@ -62,6 +62,66 @@ var _ = Describe("Podman volume prune", func() { podmanTest.Cleanup() }) + It("podman prune volume --filter", func() { + session := podmanTest.Podman([]string{"volume", "create", "--label", "label1=value1", "myvol1"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "create", "--label", "sharedlabel1=slv1", "myvol2"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "create", "--label", "sharedlabel1=slv2", "myvol3"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "create", "--label", "sharedlabel1", "myvol4"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"create", "-v", "myvol5:/myvol5", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"create", "-v", "myvol6:/myvol6", ALPINE, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(7)) + + session = podmanTest.Podman([]string{"volume", "prune", "--force", "--filter", "label=label1=value1"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(6)) + + session = podmanTest.Podman([]string{"volume", "prune", "--force", "--filter", "label=sharedlabel1=slv1"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(5)) + + session = podmanTest.Podman([]string{"volume", "prune", "--force", "--filter", "label=sharedlabel1"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"volume", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(3)) + + podmanTest.Cleanup() + }) + It("podman system prune --volume", func() { session := podmanTest.Podman([]string{"volume", "create"}) session.WaitWithDefaultTimeout() |