From 4586dc2c606d9993d39d4ab4c8c694af615b69be Mon Sep 17 00:00:00 2001 From: Shuai Yuan Date: Thu, 25 Jun 2026 17:12:20 +0800 Subject: [PATCH] Honor label filters for volume prune when all is set NormalizeVolumePruneFilters discarded every query filter when the "all" pseudo-filter was set, deleting label/label!/until before they reached the volume filter generator. As a result `podman volume prune --all --filter label=foo` ignored the label and pruned every unused volume. "all" only widens the prune scope from anonymous-only to all unused volumes; it is orthogonal to the label filters, which must still select which of those volumes are removed. Drop only the "all" key and keep the remaining filters so they continue to apply. NormalizeVolumePruneFilters is shared by the local (abi), remote (libpod API), and Docker-compat prune paths, so all three were affected. Signed-off-by: Shuai Yuan --- docs/source/markdown/podman-volume-prune.1.md | 7 +++++++ pkg/util/filters.go | 9 ++++++--- pkg/util/filters_test.go | 13 +++++++++++++ test/e2e/volume_prune_test.go | 18 ++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/source/markdown/podman-volume-prune.1.md b/docs/source/markdown/podman-volume-prune.1.md index 224a9429b6..175ef89395 100644 --- a/docs/source/markdown/podman-volume-prune.1.md +++ b/docs/source/markdown/podman-volume-prune.1.md @@ -21,6 +21,8 @@ removal unless **--force** is used. Remove all unused volumes (anonymous and named). Without this option, only anonymous unused volumes are removed. +**--all** can be combined with **--filter**: **--all** widens the set of candidate volumes to all unused ones, and the filters then restrict which of those are removed. For example, **--all --filter label!=keep** removes every unused volume that does not have the *keep* label. + #### **--dry-run** Show which volumes would be pruned without removing them. @@ -77,6 +79,11 @@ Prune all unused volumes (anonymous and named). $ podman volume prune --all --force ``` +Prune all unused volumes except those with a specific label (combine **--all** with a filter). +``` +$ podman volume prune --all --force --filter label!=keep +``` + Prune all unused volumes using the filter (equivalent to **--all**). ``` $ podman volume prune --filter all=true --force diff --git a/pkg/util/filters.go b/pkg/util/filters.go index 7e7d1d2739..a64957fb3e 100644 --- a/pkg/util/filters.go +++ b/pkg/util/filters.go @@ -66,9 +66,12 @@ func NormalizeVolumePruneFilters(f url.Values) url.Values { } allValue := f.Get("all") if strings.EqualFold(allValue, "true") || allValue == "1" { - for key := range f { - f.Del(key) - } + // "all" only widens the scope from anonymous-only to every unused + // volume; it is orthogonal to the other filters. Drop just the "all" + // key and keep the rest (e.g. label/label!, until) so they continue to + // constrain which volumes are pruned, matching Docker, which ANDs the + // "all" scope together with the label filters. + f.Del("all") return f } f.Del("all") diff --git a/pkg/util/filters_test.go b/pkg/util/filters_test.go index 7c4c472a08..5c7168b298 100644 --- a/pkg/util/filters_test.go +++ b/pkg/util/filters_test.go @@ -96,6 +96,19 @@ func TestNormalizeVolumePruneFilters(t *testing.T) { t.Fatalf("got %#v, want no all/anonymous keys", got) } }) + t.Run("all true keeps label filters", func(t *testing.T) { + t.Parallel() + got := NormalizeVolumePruneFilters(url.Values{"all": {"true"}, "label": {"k=v"}, "label!": {"x"}}) + if got.Has("all") || got.Has("anonymous") { + t.Fatalf("got %#v, want no all/anonymous keys", got) + } + if got.Get("label") != "k=v" { + t.Fatalf("label dropped alongside all: got %#v", got) + } + if got.Get("label!") != "x" { + t.Fatalf("label! dropped alongside all: got %#v", got) + } + }) t.Run("label without anonymous key does not inject anonymous", func(t *testing.T) { t.Parallel() in := url.Values{"label": {"k=v"}} diff --git a/test/e2e/volume_prune_test.go b/test/e2e/volume_prune_test.go index 7622b22287..f9bf104107 100644 --- a/test/e2e/volume_prune_test.go +++ b/test/e2e/volume_prune_test.go @@ -110,6 +110,24 @@ var _ = Describe("Podman volume prune", func() { podmanTest.PodmanExitCleanly("volume", "prune", "--force", "--filter", "label!=testlabel") }) + It("podman volume prune --all keeps the label filter", func() { + // Regression: the "all" flag must not discard a label/label! filter. + // With "--all --filter label!=keep" only unlabeled unused volumes are + // pruned; the labeled one survives. Before the fix, "all" cleared the + // filter and both volumes were removed. + podmanTest.PodmanExitCleanly("volume", "create", "--label", "keep=yes", "keepvol") + podmanTest.PodmanExitCleanly("volume", "create", "dropvol") + + session := podmanTest.PodmanExitCleanly("volume", "ls", "-q") + Expect(session.OutputToStringArray()).To(HaveLen(2)) + + podmanTest.PodmanExitCleanly("volume", "prune", "--all", "--force", "--filter", "label!=keep") + + session = podmanTest.PodmanExitCleanly("volume", "ls", "-q") + Expect(session.OutputToStringArray()).To(HaveLen(1)) + Expect(session.OutputToString()).To(Equal("keepvol")) + }) + It("podman system prune --volume", func() { useCustomNetworkDir(podmanTest, tempdir) podmanTest.PodmanExitCleanly("volume", "create")