Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add volume prune --filter support #8689

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion cmd/podman/volumes/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -28,6 +30,7 @@ var (
RunE: prune,
ValidArgsFunction: completion.AutocompleteNone,
}
filter = []string{}
)

func init() {
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down
16 changes: 15 additions & 1 deletion docs/source/markdown/podman-volume-prune.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand All @@ -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
Expand All @@ -29,6 +41,8 @@ Print usage statement
$ podman volume prune

$ podman volume prune --force

$ podman volume prune --filter label=mylabel=mylabelvalue
```

## SEE ALSO
Expand Down
4 changes: 2 additions & 2 deletions libpod/runtime_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 7 additions & 5 deletions pkg/api/handlers/compat/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package compat
import (
"encoding/json"
"net/http"
"net/url"
"time"

"github.com/containers/podman/v2/libpod"
Expand Down Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion pkg/api/handlers/libpod/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
41 changes: 37 additions & 4 deletions pkg/bindings/test/volumes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

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

})
12 changes: 10 additions & 2 deletions pkg/bindings/volumes/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,23 @@ 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
)
conn, err := bindings.GetClient(ctx)
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
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/domain/entities/engine_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
7 changes: 7 additions & 0 deletions pkg/domain/entities/volumes.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package entities

import (
"net/url"
"time"

docker_api_types "github.com/docker/docker/api/types"
Expand Down Expand Up @@ -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"`
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was conflicted whether to use url.Values or the underlying type of map[string][]string for the filters. I saw it both ways throughout the codebase. I settled on url.Values but I don't feel strongly either way. I thought I remember seeing some mention of a refactor for filters so hopefully the inconsistency can be fixed then.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should use map[string][]string everywhere.
@baude @jwhonce WDYT?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once @baude / @jwhonce add their input I can fix the other things you mentioned @Luap99 (since the type will effect the changes you requested, even though its not much different).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just keep it for now. It will be reworked soon.

}

type VolumePruneReport struct {
Err error
Id string //nolint
Expand Down
20 changes: 20 additions & 0 deletions pkg/domain/filters/helpers.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion pkg/domain/filters/volumes.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/domain/infra/abi/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be passing down the prune filters.

podman system prune --filter should be passed down to volume helper as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that was my mistake. I can open a PR for that tonight.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks,
BTW great job on this PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rhatdan PR opened: #8724

if err != nil {
return nil, err
}
Expand Down
12 changes: 8 additions & 4 deletions pkg/domain/infra/abi/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/domain/infra/tunnel/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading