Add volume prune --filter support

This change adds support for the `--filter` / `?filters` arguments on
the `podman volume prune` subcommand.

  * Adds ParseFilterArgumentsIntoFilters helper for consistent
    Filter string slice handling
  * Adds `--filter` support to podman volume prune cli
  * Adds `?filters...` support to podman volume prune api
  * Updates apiv2 / e2e tests

Closes #8672

Signed-off-by: Baron Lenardson <lenardson.baron@gmail.com>
This commit is contained in:
Baron Lenardson
2020-12-10 20:30:28 -06:00
parent 9216be2008
commit a0204ada09
16 changed files with 236 additions and 25 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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))
})
})

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -214,7 +214,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
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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

View File

@ -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()