Merge pull request #8689 from bblenard/issue-8672-volume-prune

Add volume prune --filter support
This commit is contained in:
OpenShift Merge Robot 2020-12-14 09:22:30 -05:00 committed by GitHub
commit e5741b984e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 236 additions and 25 deletions

View File

@ -8,10 +8,12 @@ import (
"strings" "strings"
"github.com/containers/common/pkg/completion" "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/registry"
"github.com/containers/podman/v2/cmd/podman/utils" "github.com/containers/podman/v2/cmd/podman/utils"
"github.com/containers/podman/v2/cmd/podman/validate" "github.com/containers/podman/v2/cmd/podman/validate"
"github.com/containers/podman/v2/pkg/domain/entities" "github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/domain/filters"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -28,6 +30,7 @@ var (
RunE: prune, RunE: prune,
ValidArgsFunction: completion.AutocompleteNone, ValidArgsFunction: completion.AutocompleteNone,
} }
filter = []string{}
) )
func init() { func init() {
@ -37,10 +40,17 @@ func init() {
Parent: volumeCmd, Parent: volumeCmd,
}) })
flags := pruneCommand.Flags() 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") flags.BoolP("force", "f", false, "Do not prompt for confirmation")
} }
func prune(cmd *cobra.Command, args []string) error { func prune(cmd *cobra.Command, args []string) error {
var (
pruneOptions = entities.VolumePruneOptions{}
)
// Prompt for confirmation if --force is not set // Prompt for confirmation if --force is not set
force, err := cmd.Flags().GetBool("force") force, err := cmd.Flags().GetBool("force")
if err != nil { if err != nil {
@ -58,7 +68,11 @@ func prune(cmd *cobra.Command, args []string) error {
return nil 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 { if err != nil {
return err return err
} }

View File

@ -8,7 +8,8 @@ podman\-volume\-prune - Remove all unused volumes
## DESCRIPTION ## 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. 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. 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** #### **--help**
Print usage statement Print usage statement
@ -29,6 +41,8 @@ Print usage statement
$ podman volume prune $ podman volume prune
$ podman volume prune --force $ podman volume prune --force
$ podman volume prune --filter label=mylabel=mylabelvalue
``` ```
## SEE ALSO ## SEE ALSO

View File

@ -133,9 +133,9 @@ func (r *Runtime) GetAllVolumes() ([]*Volume, error) {
} }
// PruneVolumes removes unused volumes from the system // 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) reports := make(map[string]error)
vols, err := r.GetAllVolumes() vols, err := r.Volumes(filterFuncs...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -3,6 +3,7 @@ package compat
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/url"
"time" "time"
"github.com/containers/podman/v2/libpod" "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())) utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
return return
} }
// TODO: We have no ability to pass pruning filters to `PruneVolumes()` so
// we'll explicitly reject the request if we see any f := (url.Values)(query.Filters)
if len(query.Filters) > 0 { filterFuncs, err := filters.GenerateVolumeFilters(f)
utils.InternalServerError(w, errors.New("filters for pruning volumes is not implemented")) if err != nil {
utils.Error(w, "Something when wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse filters for %s", f.Encode()))
return return
} }
pruned, err := runtime.PruneVolumes(r.Context()) pruned, err := runtime.PruneVolumes(r.Context(), filterFuncs)
if err != nil { if err != nil {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return

View File

@ -3,6 +3,7 @@ package libpod
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/url"
"github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/libpod/define" "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) { func pruneVolumesHelper(r *http.Request) ([]*entities.VolumePruneReport, error) {
var ( var (
runtime = r.Context().Value("runtime").(*libpod.Runtime) 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -144,16 +144,15 @@ var _ = Describe("Podman volumes", func() {
Expect(vols[0].Name).To(Equal("homer")) Expect(vols[0].Name).To(Equal("homer"))
}) })
// TODO we need to add filtering to tests
It("prune unused volume", func() { It("prune unused volume", func() {
// Pruning when no volumes present should be ok // Pruning when no volumes present should be ok
_, err := volumes.Prune(connText) _, err := volumes.Prune(connText, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
// Removing an unused volume should work // Removing an unused volume should work
_, err = volumes.Create(connText, entities.VolumeCreateOptions{}) _, err = volumes.Create(connText, entities.VolumeCreateOptions{})
Expect(err).To(BeNil()) Expect(err).To(BeNil())
vols, err := volumes.Prune(connText) vols, err := volumes.Prune(connText, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(len(vols)).To(BeNumerically("==", 1)) Expect(len(vols)).To(BeNumerically("==", 1))
@ -163,11 +162,45 @@ var _ = Describe("Podman volumes", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"}) session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"})
session.Wait(45) session.Wait(45)
vols, err = volumes.Prune(connText) vols, err = volumes.Prune(connText, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(len(vols)).To(BeNumerically("==", 1)) Expect(len(vols)).To(BeNumerically("==", 1))
_, err = volumes.Inspect(connText, "homer") _, err = volumes.Inspect(connText, "homer")
Expect(err).To(BeNil()) 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. // 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 ( var (
pruned []*entities.VolumePruneReport pruned []*entities.VolumePruneReport
) )
@ -83,7 +83,15 @@ func Prune(ctx context.Context) ([]*entities.VolumePruneReport, error) {
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -80,6 +80,6 @@ type ContainerEngine interface {
VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IDOrNameResponse, error) VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IDOrNameResponse, error)
VolumeInspect(ctx context.Context, namesOrIds []string, opts InspectOptions) ([]*VolumeInspectReport, []error, error) VolumeInspect(ctx context.Context, namesOrIds []string, opts InspectOptions) ([]*VolumeInspectReport, []error, error)
VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, 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) VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error)
} }

View File

@ -1,6 +1,7 @@
package entities package entities
import ( import (
"net/url"
"time" "time"
docker_api_types "github.com/docker/docker/api/types" docker_api_types "github.com/docker/docker/api/types"
@ -109,6 +110,12 @@ type VolumeInspectReport struct {
*VolumeConfigResponse *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 { type VolumePruneReport struct {
Err error Err error
Id string //nolint 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 package filters
import ( import (
"net/url"
"strings" "strings"
"github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod"
"github.com/pkg/errors" "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 var vf []libpod.VolumeFilter
for filter, v := range filters { for filter, v := range filters {
for _, val := range v { for _, val := range v {

View File

@ -224,7 +224,7 @@ func (ic *ContainerEngine) SystemPrune(ctx context.Context, options entities.Sys
systemPruneReport.ImagePruneReport.Report.Id = append(systemPruneReport.ImagePruneReport.Report.Id, results...) systemPruneReport.ImagePruneReport.Report.Id = append(systemPruneReport.ImagePruneReport.Report.Id, results...)
} }
if options.Volume { if options.Volume {
volumePruneReport, err := ic.pruneVolumesHelper(ctx) volumePruneReport, err := ic.pruneVolumesHelper(ctx, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -127,12 +127,16 @@ func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []strin
return reports, errs, nil return reports, errs, nil
} }
func (ic *ContainerEngine) VolumePrune(ctx context.Context) ([]*entities.VolumePruneReport, error) { func (ic *ContainerEngine) VolumePrune(ctx context.Context, options entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) {
return ic.pruneVolumesHelper(ctx) 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) { func (ic *ContainerEngine) pruneVolumesHelper(ctx context.Context, filterFuncs []libpod.VolumeFilter) ([]*entities.VolumePruneReport, error) {
pruned, err := ic.Libpod.PruneVolumes(ctx) pruned, err := ic.Libpod.PruneVolumes(ctx, filterFuncs)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -68,8 +68,8 @@ func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []strin
return reports, errs, nil return reports, errs, nil
} }
func (ic *ContainerEngine) VolumePrune(ctx context.Context) ([]*entities.VolumePruneReport, error) { func (ic *ContainerEngine) VolumePrune(ctx context.Context, opts entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) {
return volumes.Prune(ic.ClientCxt) return volumes.Prune(ic.ClientCxt, (map[string][]string)(opts.Filters))
} }
func (ic *ContainerEngine) VolumeList(ctx context.Context, opts entities.VolumeListOptions) ([]*entities.VolumeListReport, error) { 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 \ .Labels.testlabel=testonly \
.Options.type=tmpfs \ .Options.type=tmpfs \
.Options.o=nodev,noexec .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 # Negative test
# We have created a volume named "foo1" # 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 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"]}' # -G --data-urlencode 'filters={"name":["notexist"]}'
t GET libpod/volumes/json?filters=%7B%22name%22%3A%5B%22notexists%22%5D%7D 200 length=0 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 ## inspect volume
t GET libpod/volumes/foo1/json 200 \ t GET libpod/volumes/foo1/json 200 \
@ -60,6 +78,18 @@ t DELETE libpod/volumes/foo1 404 \
.message~.* \ .message~.* \
.response=404 .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 ## Prune volumes
t POST libpod/volumes/prune "" 200 t POST libpod/volumes/prune "" 200
#After prune volumes, there should be no volume existing #After prune volumes, there should be no volume existing

View File

@ -62,6 +62,66 @@ var _ = Describe("Podman volume prune", func() {
podmanTest.Cleanup() 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() { It("podman system prune --volume", func() {
session := podmanTest.Podman([]string{"volume", "create"}) session := podmanTest.Podman([]string{"volume", "create"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()