From fdd8cc17fa14d41d71588edd5b91fb9c6a51dfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brauner?= Date: Tue, 31 Mar 2026 15:40:52 +0200 Subject: [PATCH] Fix filtering with negated labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brauner --- docs/source/markdown/podman-container-prune.1.md | 2 ++ docs/source/markdown/podman-volume-prune.1.md | 2 ++ pkg/domain/filters/containers.go | 10 +++++++--- pkg/domain/filters/pods.go | 2 +- pkg/domain/filters/volumes.go | 4 ++-- test/e2e/volume_ls_test.go | 11 +++++++++-- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/source/markdown/podman-container-prune.1.md b/docs/source/markdown/podman-container-prune.1.md index 4db2e5f43a..f19b65928c 100644 --- a/docs/source/markdown/podman-container-prune.1.md +++ b/docs/source/markdown/podman-container-prune.1.md @@ -25,6 +25,8 @@ Supported filters: The `label` *filter* accepts two formats. One is the `label`=*key* or `label`=*key*=*value*, which removes containers with the specified labels. The other format is the `label!`=*key* or `label!`=*key*=*value*, which removes containers without the specified labels. +**NOTE:** `label!` filters are combined with **AND**, so that the behavior is consistent with `label`, while in Docker, they are combined with **OR**. + The `until` *filter* can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. 10m, 1h30m) computed relative to the machine’s time. #### **--force**, **-f** diff --git a/docs/source/markdown/podman-volume-prune.1.md b/docs/source/markdown/podman-volume-prune.1.md index f4d7bb563e..e41e1c8e26 100644 --- a/docs/source/markdown/podman-volume-prune.1.md +++ b/docs/source/markdown/podman-volume-prune.1.md @@ -43,6 +43,8 @@ Supported filters: The `label` *filter* accepts two formats. One is the `label`=*key* or `label`=*key*=*value*, which removes volumes with the specified labels. The other format is the `label!`=*key* or `label!`=*key*=*value*, which removes volumes without the specified labels. +**NOTE:** `label!` filters are combined with **AND**, so that the behavior is consistent with `label`, while in Docker, they are combined with **OR**. + The `until` *filter* can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. 10m, 1h30m) computed relative to the machine's time. #### **--force**, **-f** diff --git a/pkg/domain/filters/containers.go b/pkg/domain/filters/containers.go index 300a06f39a..47a738bde2 100644 --- a/pkg/domain/filters/containers.go +++ b/pkg/domain/filters/containers.go @@ -32,7 +32,7 @@ func GenerateContainerFilterFuncs(filter string, filterValues []string, r *libpo }, nil case "label!": return func(c *libpod.Container) bool { - return !filters.MatchLabelFilters(filterValues, c.Labels()) + return filters.MatchNegatedLabelFilters(filterValues, c.Labels()) }, nil case "name": // we only have to match one name @@ -309,7 +309,7 @@ func GeneratePruneContainerFilterFuncs(filter string, filterValues []string, _ * }, nil case "label!": return func(c *libpod.Container) bool { - return !filters.MatchLabelFilters(filterValues, c.Labels()) + return filters.MatchNegatedLabelFilters(filterValues, c.Labels()) }, nil case "until": return prepareUntilFilterFunc(filterValues) @@ -478,7 +478,11 @@ func GenerateExternalContainerFilterFuncs(filter string, filterValues []string, }, nil case "label": return func(listContainer *types.ListContainer) bool { - return !filters.MatchLabelFilters(filterValues, listContainer.Labels) + return filters.MatchLabelFilters(filterValues, listContainer.Labels) + }, nil + case "label!": + return func(listContainer *types.ListContainer) bool { + return filters.MatchNegatedLabelFilters(filterValues, listContainer.Labels) }, nil case "pod": var pods []*libpod.Pod diff --git a/pkg/domain/filters/pods.go b/pkg/domain/filters/pods.go index 8eafd5c196..4cd9cc6067 100644 --- a/pkg/domain/filters/pods.go +++ b/pkg/domain/filters/pods.go @@ -122,7 +122,7 @@ func GeneratePodFilterFunc(filter string, filterValues []string, r *libpod.Runti case "label!": return func(p *libpod.Pod) bool { labels := p.Labels() - return !filters.MatchLabelFilters(filterValues, labels) + return filters.MatchNegatedLabelFilters(filterValues, labels) }, nil case "until": return func(p *libpod.Pod) bool { diff --git a/pkg/domain/filters/volumes.go b/pkg/domain/filters/volumes.go index c8893e7a4c..7b859ed764 100644 --- a/pkg/domain/filters/volumes.go +++ b/pkg/domain/filters/volumes.go @@ -35,7 +35,7 @@ func GenerateVolumeFilters(filter string, filterValues []string, runtime *libpod }, nil case "label!": return func(v *libpod.Volume) bool { - return !filters.MatchLabelFilters(filterValues, v.Labels()) + return filters.MatchNegatedLabelFilters(filterValues, v.Labels()) }, nil case "opt": return func(v *libpod.Volume) bool { @@ -101,7 +101,7 @@ func GeneratePruneVolumeFilters(filter string, filterValues []string, runtime *l }, nil case "label!": return func(v *libpod.Volume) bool { - return !filters.MatchLabelFilters(filterValues, v.Labels()) + return filters.MatchNegatedLabelFilters(filterValues, v.Labels()) }, nil case "until": return createUntilFilterVolumeFunction(filterValues) diff --git a/test/e2e/volume_ls_test.go b/test/e2e/volume_ls_test.go index 1ede3c89b8..bd57494445 100644 --- a/test/e2e/volume_ls_test.go +++ b/test/e2e/volume_ls_test.go @@ -189,7 +189,7 @@ var _ = Describe("Podman volume ls", func() { vol1Name := session.OutputToString() - session = podmanTest.Podman([]string{"volume", "create", "--label", "b=c", "--label", "a=b", "vol2"}) + session = podmanTest.Podman([]string{"volume", "create", "--label", "a=b", "--label", "b=c", "--label", "d=e", "vol2"}) session.WaitWithDefaultTimeout() Expect(session).Should(ExitCleanly()) @@ -208,12 +208,19 @@ var _ = Describe("Podman volume ls", func() { Expect(session.OutputToStringArray()[0]).To(Equal(vol1Name)) Expect(session.OutputToStringArray()[1]).To(Equal(vol2Name)) - session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label=c=d", "--filter", "label=b=c"}) + session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label=b=c", "--filter", "label=c=d"}) session.WaitWithDefaultTimeout() Expect(session).Should(ExitCleanly()) Expect(session.OutputToStringArray()).To(HaveLen(1)) Expect(session.OutputToStringArray()[0]).To(Equal(vol3Name)) + // Filters with label! key + session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label!=c=d", "--filter", "label!=d=e"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToStringArray()).To(HaveLen(1)) + Expect(session.OutputToStringArray()[0]).To(Equal(vol1Name)) + // Filters with different keys session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label=b=c", "--filter", "name=vol1"}) session.WaitWithDefaultTimeout()