Skip to content
Open
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
12 changes: 11 additions & 1 deletion cmd/podman/volumes/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func init() {
_ = pruneCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteVolumePruneFilters)
flags.BoolP("force", "f", false, "Do not prompt for confirmation")
flags.BoolP("all", "a", false, "Remove all unused volumes, both anonymous and named")
flags.Bool("dry-run", false, "Show what would be pruned without actually pruning")
Comment thread
mheon marked this conversation as resolved.
}

func prune(cmd *cobra.Command, _ []string) error {
Expand Down Expand Up @@ -74,7 +75,13 @@ func prune(cmd *cobra.Command, _ []string) error {
pruneOptions.Filters.Set("all", "true")
}

if !force {
dryRun, _ := cmd.Flags().GetBool("dry-run")
if force && dryRun {
return errors.New("--force and --dry-run cannot be used together")
}
pruneOptions.DryRun = dryRun

if !force && !dryRun {
reader := bufio.NewReader(os.Stdin)
if allFlag {
fmt.Println("WARNING! This will remove all volumes not used by at least one container. The following volumes will be removed:")
Expand Down Expand Up @@ -135,6 +142,9 @@ func prune(cmd *cobra.Command, _ []string) error {
if err != nil {
return err
}
if dryRun {
fmt.Println("Volumes that would be pruned:")
}
return utils.PrintVolumePruneResults(responses, false)
}

Expand Down
10 changes: 10 additions & 0 deletions docs/source/markdown/podman-volume-prune.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ removal unless **--force** is used.

Remove all unused volumes (anonymous and named). Without this option, only anonymous unused volumes are removed.

#### **--dry-run**

Show which volumes would be pruned without removing them.

#### **--filter**

Provide filter values.
Expand Down Expand Up @@ -98,6 +102,12 @@ Prune unused volumes that do NOT have a specific label key:
$ podman volume prune --filter label!=environment
```

Preview all unused volumes without removing them.

```
$ podman volume prune --all --dry-run
```

## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**

Expand Down
31 changes: 22 additions & 9 deletions libpod/runtime_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,31 +111,44 @@ func (r *Runtime) GetAllVolumes() ([]*Volume, error) {
}

// PruneVolumes removes unused volumes from the system
func (r *Runtime) PruneVolumes(ctx context.Context, filterFuncs []VolumeFilter) ([]*reports.PruneReport, error) {
func (r *Runtime) PruneVolumes(ctx context.Context, filterFuncs []VolumeFilter, dryRun bool) ([]*reports.PruneReport, error) {
preports := make([]*reports.PruneReport, 0)
vols, err := r.Volumes(filterFuncs...)
if err != nil {
return nil, err
}

for _, vol := range vols {
dangling, err := vol.IsDangling()
if err != nil {
preports = append(preports, &reports.PruneReport{
Id: vol.Name(),
Err: err,
})
}
if !dangling {
continue
}

report := new(reports.PruneReport)
volSize, err := vol.Size()
if err != nil {
volSize = 0
}
report.Size = volSize
report.Id = vol.Name()
var timeout *uint
if err := r.RemoveVolume(ctx, vol, false, timeout); err != nil {
if !errors.Is(err, define.ErrVolumeBeingUsed) && !errors.Is(err, define.ErrVolumeRemoved) {
report.Err = err
if !dryRun {
var timeout *uint
if err := r.RemoveVolume(ctx, vol, false, timeout); err != nil {
if !errors.Is(err, define.ErrVolumeBeingUsed) && !errors.Is(err, define.ErrVolumeRemoved) {
report.Err = err
} else {
// We didn't remove the volume for some reason
continue
}
} else {
// We didn't remove the volume for some reason
continue
vol.newVolumeEvent(events.Prune)
}
} else {
vol.newVolumeEvent(events.Prune)
}
preports = append(preports, report)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/handlers/compat/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func PruneVolumes(w http.ResponseWriter, r *http.Request) {
filterFuncs = append(filterFuncs, filterFunc)
}

pruned, err := runtime.PruneVolumes(r.Context(), filterFuncs)
pruned, err := runtime.PruneVolumes(r.Context(), filterFuncs, false)
if err != nil {
utils.InternalServerError(w, err)
return
Expand Down
11 changes: 10 additions & 1 deletion pkg/api/handlers/libpod/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,16 @@ func PruneVolumes(w http.ResponseWriter, r *http.Request) {
}

func pruneVolumesHelper(r *http.Request) ([]*reports.PruneReport, error) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)

query := struct {
DryRun bool `schema:"dryrun"`
}{}

if err := decoder.Decode(&query, r.URL.Query()); err != nil {
return nil, err
}
filterMap, err := util.PrepareFilters(r)
if err != nil {
return nil, err
Expand All @@ -162,7 +171,7 @@ func pruneVolumesHelper(r *http.Request) ([]*reports.PruneReport, error) {
filterFuncs = append(filterFuncs, filterFunc)
}

reports, err := runtime.PruneVolumes(r.Context(), filterFuncs)
reports, err := runtime.PruneVolumes(r.Context(), filterFuncs, query.DryRun)
if err != nil {
return nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/api/server/register_volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
// - `anonymous` When true/false, restrict to anonymous or named volumes only.
// - `until=<timestamp>` Prune volumes created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time.
// - `label` (`label=<key>`, `label=<key>=<value>`, `label!=<key>`, or `label!=<key>=<value>`) Prune volumes with (or without, in case `label!=...` is used) the specified labels.
// - in: query
// name: dryrun
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The dryrun query parameter should include required: false and ideally a default: false.

// type: boolean
// required: false
// default: false
// description: Show which volumes would be pruned without removing them.
// responses:
// '200':
// "$ref": "#/responses/volumePruneLibpod"
Expand Down
16 changes: 16 additions & 0 deletions pkg/bindings/test/volumes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,20 @@ var _ = Describe("Podman volumes", func() {
Expect(err).ToNot(HaveOccurred())
Expect(pruned).To(HaveLen(1))
})

It("prune volume with dry-run", func() {
vol, err := volumes.Create(connText, entities.VolumeCreateOptions{Name: "vol"}, nil)
Expect(err).ToNot(HaveOccurred())

options := new(volumes.PruneOptions).
WithFilters(map[string][]string{"all": {"true"}}).
WithDryRun(true)

vols, err := volumes.Prune(connText, options)
Expect(err).ToNot(HaveOccurred())
Expect(reports.PruneReportsIds(vols)).To(ContainElement(vol.Name))

_, err = volumes.Inspect(connText, vol.Name, nil)
Expect(err).ToNot(HaveOccurred())
})
})
2 changes: 2 additions & 0 deletions pkg/bindings/volumes/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type ListOptions struct {
type PruneOptions struct {
// Filters applied to the pruning of volumes
Filters map[string][]string
// DryRun lists volumes that would be pruned without removing them.
DryRun *bool
}

// RemoveOptions are optional options for removing volumes
Expand Down
15 changes: 15 additions & 0 deletions pkg/bindings/volumes/types_prune_options.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkg/domain/entities/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type VolumeInspectReport = types.VolumeInspectReport
// - when filter "all" is true, all unused volumes are pruned.
type VolumePruneOptions struct {
Filters url.Values `json:"filters" schema:"filters"`
DryRun bool `json:"dry_run" schema:"dryrun"`
}

type VolumeListOptions struct {
Expand Down
6 changes: 3 additions & 3 deletions pkg/domain/infra/abi/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ func (ic *ContainerEngine) VolumePrune(ctx context.Context, options entities.Vol
}
funcs = append(funcs, filterFunc)
}
return ic.pruneVolumesHelper(ctx, funcs)
return ic.pruneVolumesHelper(ctx, funcs, options.DryRun)
}

func (ic *ContainerEngine) pruneVolumesHelper(ctx context.Context, filterFuncs []libpod.VolumeFilter) ([]*reports.PruneReport, error) {
pruned, err := ic.Libpod.PruneVolumes(ctx, filterFuncs)
func (ic *ContainerEngine) pruneVolumesHelper(ctx context.Context, filterFuncs []libpod.VolumeFilter, dryRun bool) ([]*reports.PruneReport, error) {
pruned, err := ic.Libpod.PruneVolumes(ctx, filterFuncs, dryRun)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/domain/infra/tunnel/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (ic *ContainerEngine) VolumeInspect(_ context.Context, namesOrIds []string,
}

func (ic *ContainerEngine) VolumePrune(_ context.Context, opts entities.VolumePruneOptions) ([]*reports.PruneReport, error) {
options := new(volumes.PruneOptions).WithFilters(opts.Filters)
options := new(volumes.PruneOptions).WithFilters(opts.Filters).WithDryRun(opts.DryRun)
return volumes.Prune(ic.ClientCtx, options)
}

Expand Down
77 changes: 77 additions & 0 deletions test/e2e/volume_prune_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package integration
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "go.podman.io/podman/v6/test/utils"
)

var _ = Describe("Podman volume prune", func() {
Expand Down Expand Up @@ -139,4 +140,80 @@ var _ = Describe("Podman volume prune", func() {
Expect(session.OutputToStringArray()).To(HaveLen(1))
Expect(session.OutputToStringArray()[0]).To(Equal(vol1))
})

It("podman volume prune --all --dry-run", func() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would like to see also other tests for example:

  • --dry-run without --all (anonymous-only volumes)
  • --dry-run combined with --force (should work but good to verify)
  • Negative test for volumes in use (should not appear in dry-run output)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Honny1
Thank you for the feedback.

Before adding the test code, I would like to share the results I observed in my remote environment.

## case 1: --dry-run without --all
$ podman volume prune --dry-run
Volumes that would be pruned:

## case 2: --dry-run combined with --force
$ podman volume prune --force --all --dry-run
Volumes that would be pruned:
vol1
validatepr-gocache
validatepr-gomodcache
validatepr-lintcache
validatepr-precommitcache

$ podman volume ls                           
DRIVER      VOLUME NAME
local       vol1
local       validatepr-gocache
local       validatepr-gomodcache
local       validatepr-lintcache
local       validatepr-precommitcache

## case 3: Negative test for volumes in use
$ podman run -d --name testctr -v vol1:/data alpine sleep 1000
$ podman volume prune --all --dry-run
Volumes that would be pruned:
validatepr-gocache
validatepr-gomodcache
validatepr-lintcache
validatepr-precommitcache

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do not forget to create also anonymous volumes.

vol1 := "vol1"
vol2 := "vol2"

podmanTest.PodmanExitCleanly("volume", "create", vol1)
podmanTest.PodmanExitCleanly("volume", "create", vol2)

session := podmanTest.PodmanExitCleanly("volume", "prune", "--all", "--dry-run")
Expect(session.OutputToString()).To(ContainSubstring("Volumes that would be pruned:"))
Expect(session.OutputToString()).To(ContainSubstring(vol1))
Expect(session.OutputToString()).To(ContainSubstring(vol2))

session = podmanTest.PodmanExitCleanly("volume", "ls", "-q")
Expect(session.OutputToStringArray()).To(HaveLen(2))
Expect(session.OutputToString()).To(ContainSubstring(vol1))
Expect(session.OutputToString()).To(ContainSubstring(vol2))
})

It("podman volume prune --filter --dry-run", func() {
vol1 := "vol1"
vol2 := "vol2"

podmanTest.PodmanExitCleanly("volume", "create", "--label", "dryrun=true", vol1)
podmanTest.PodmanExitCleanly("volume", "create", "--label", "dryrun=false", vol2)

session := podmanTest.PodmanExitCleanly("volume", "prune", "--dry-run", "--filter", "label=dryrun=true")
Expect(session.OutputToString()).To(ContainSubstring("Volumes that would be pruned:"))
Expect(session.OutputToString()).To(ContainSubstring(vol1))
Expect(session.OutputToString()).ToNot(ContainSubstring(vol2))

session = podmanTest.PodmanExitCleanly("volume", "ls", "-q")
Expect(session.OutputToStringArray()).To(HaveLen(2))
Expect(session.OutputToString()).To(ContainSubstring(vol1))
Expect(session.OutputToString()).To(ContainSubstring(vol2))
})

It("podman volume prune --dry-run only shows anonymous volumes", func() {
vol1 := "vol1"
anon_vol := podmanTest.PodmanExitCleanly("volume", "create").OutputToString()
podmanTest.PodmanExitCleanly("volume", "create", vol1)

session := podmanTest.PodmanExitCleanly("volume", "prune", "--dry-run")
Expect(session.OutputToString()).To(ContainSubstring("Volumes that would be pruned:"))
Expect(session.OutputToString()).To(ContainSubstring(anon_vol))

session = podmanTest.PodmanExitCleanly("volume", "ls", "-q")
Expect(session.OutputToStringArray()).To(HaveLen(2))
Expect(session.OutputToString()).To(ContainSubstring(vol1))
Expect(session.OutputToString()).To(ContainSubstring(anon_vol))
})

It("podman volume prune --all --dry-run excludes volumes in use", func() {
vol1 := "vol1"
vol2 := "vol2"

podmanTest.PodmanExitCleanly("volume", "create", vol1)
podmanTest.PodmanExitCleanly("volume", "create", vol2)
podmanTest.PodmanExitCleanly("create", "-v", "vol2:/data", ALPINE, "ls")

session := podmanTest.PodmanExitCleanly("volume", "prune", "--all", "--dry-run")
Expect(session.OutputToString()).To(ContainSubstring("Volumes that would be pruned:"))
Expect(session.OutputToString()).To(ContainSubstring(vol1))
Expect(session.OutputToString()).ToNot(ContainSubstring(vol2))

session = podmanTest.PodmanExitCleanly("volume", "ls", "-q")
Expect(session.OutputToStringArray()).To(HaveLen(2))
Expect(session.OutputToString()).To(ContainSubstring(vol1))
Expect(session.OutputToString()).To(ContainSubstring(vol2))
})

It("podman volume prune --force --dry-run fails", func() {
session := podmanTest.Podman([]string{"volume", "prune", "--force", "--dry-run"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitWithError(125, "--force and --dry-run cannot be used together"))
})
})