From ec583fa17f10cbeae9692b7adc98f91914140e44 Mon Sep 17 00:00:00 2001 From: Jiwoo Ahn Date: Mon, 4 May 2026 16:01:44 +0900 Subject: [PATCH] filters: add annotation filter for containers Fixes: #28562 Signed-off-by: Jiwoo Ahn --- cmd/podman/common/completion.go | 16 +++++++----- .../markdown/options/filter.container.md | 2 ++ .../markdown/podman-container-prune.1.md | 11 ++++---- docs/source/markdown/podman-ps.1.md.in | 10 +++++++ pkg/api/server/register_containers.go | 12 ++++++--- pkg/domain/filters/containers.go | 20 ++++++++++++-- test/e2e/prune_test.go | 16 ++++++++++++ test/e2e/ps_test.go | 26 +++++++++++++++++++ 8 files changed, 96 insertions(+), 17 deletions(-) diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 0f167cfb23..e705e3d102 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -1786,11 +1786,12 @@ func AutocompletePsFilters(cmd *cobra.Command, _ []string, toComplete string) ([ define.HealthCheckUnhealthy, }, cobra.ShellCompDirectiveNoFileComp }, - "id=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeIDs) }, - "label=": nil, - "name=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeNames) }, - "network=": func(s string) ([]string, cobra.ShellCompDirective) { return getNetworks(cmd, s, completeDefault) }, - "pod=": func(s string) ([]string, cobra.ShellCompDirective) { return getPods(cmd, s, completeDefault) }, + "id=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeIDs) }, + "annotation=": nil, + "label=": nil, + "name=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeNames) }, + "network=": func(s string) ([]string, cobra.ShellCompDirective) { return getNetworks(cmd, s, completeDefault) }, + "pod=": func(s string) ([]string, cobra.ShellCompDirective) { return getPods(cmd, s, completeDefault) }, "restart-policy=": func(_ string) ([]string, cobra.ShellCompDirective) { return []string{ define.RestartPolicyAlways, @@ -1870,11 +1871,14 @@ func AutocompleteImageFilters(cmd *cobra.Command, _ []string, toComplete string) } // AutocompletePruneFilters - Autocomplete container/image prune --filter options. -func AutocompletePruneFilters(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func AutocompletePruneFilters(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { kv := keyValueCompletion{ "label=": nil, "until=": nil, } + if cmd.CommandPath() == "podman container prune" { + kv["annotation="] = nil + } return completeKeyValues(toComplete, kv) } diff --git a/docs/source/markdown/options/filter.container.md b/docs/source/markdown/options/filter.container.md index e578bacfa5..9254073ab2 100644 --- a/docs/source/markdown/options/filter.container.md +++ b/docs/source/markdown/options/filter.container.md @@ -15,6 +15,8 @@ Valid filters are listed below: |----------------------|-------------------------------------------------------------------------------------------------| | id | [ID] Container's ID (CID prefix match by default; accepts regex) | | name | [Name] Container's name (accepts regex) | +| annotation | [Key] or [Key=Value] Annotation assigned to a container | +| annotation! | [Key] or [Key=Value] Annotation NOT assigned to a container | | label | [Key] or [Key=Value] Label assigned to a container | | label! | [Key] or [Key=Value] Label NOT assigned to a container | | exited | [Int] Container's exit code | diff --git a/docs/source/markdown/podman-container-prune.1.md b/docs/source/markdown/podman-container-prune.1.md index f19b65928c..b7142f0258 100644 --- a/docs/source/markdown/podman-container-prune.1.md +++ b/docs/source/markdown/podman-container-prune.1.md @@ -18,12 +18,13 @@ If there is more than one filter, the `--filter` option should be passed multipl Supported filters: -| Filter | Description | -|:------:|------------------------------------------------------------------------------------------------------| -| label | Only remove containers, with (or without, in the case of label!=[...] is used) the specified labels. | -| until | Only remove containers created before given timestamp. | +| Filter | Description | +|:-----------:|----------------------------------------------------------------------------------------------------------------| +| annotation | Only remove containers, with (or without, in the case of annotation!=[...] is used) the specified annotations. | +| label | Only remove containers, with (or without, in the case of label!=[...] is used) the specified labels. | +| until | Only remove containers created before given timestamp. | -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. +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. The `annotation` and `annotation!` filters work the same way for container annotations. **NOTE:** `label!` filters are combined with **AND**, so that the behavior is consistent with `label`, while in Docker, they are combined with **OR**. diff --git a/docs/source/markdown/podman-ps.1.md.in b/docs/source/markdown/podman-ps.1.md.in index 2b5f69ed9b..11075665b4 100644 --- a/docs/source/markdown/podman-ps.1.md.in +++ b/docs/source/markdown/podman-ps.1.md.in @@ -36,6 +36,8 @@ Note: Podman shares containers storage with other tools such as Buildah and CRI- Display external containers that are not controlled by Podman but are stored in containers storage. These external containers are generally created via other container technology such as Buildah or CRI-O and may depend on the same container images that Podman is also using. External containers are denoted with either a 'buildah' or 'storage' in the COMMAND and STATUS column of the ps output. +The **restart-policy**, **volume**, **health**, and **annotation** filters are not applicable for external containers. + @@option filter.container #### **--format**=*format* @@ -249,6 +251,14 @@ $ podman ps --filter label=app=frontend CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ff660efda598 docker.io/library/nginx:latest nginx -g daemon o... 3 minutes ago Up 3 minutes 0.0.0.0:8080->80/tcp webserver ``` + +Filter containers by annotation. +``` +$ podman ps --filter annotation=io.podman.annotations.autoremove=false +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +5e3694604817 quay.io/centos/centos:latest sleep 300 3 minutes ago Up 3 minutes centos-test +``` + Filter containers by volume. ``` $ podman ps --filter volume=mydata diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 4f4d4e3860..5042721c79 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -75,9 +75,10 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // description: | // A JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: // - `ancestor`=(`[:]`, ``, or ``) + // - `annotation`=(`key` or `"key=value"`) of a container annotation // - `before`=(`` or ``) - // - `expose`=(`[/]` or `/[]`) // - `exited=` containers with exit code of `` + // - `expose`=(`[/]` or `/[]`) // - `health`=(`starting`, `healthy`, `unhealthy` or `none`) // - `id=` a container's ID // - `is-task`=(`true` or `false`) @@ -112,8 +113,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // type: string // description: | // Filters to process on the prune list, encoded as JSON (a `map[string][]string`). Available filters: - // - `until=` Prune containers created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. + // - `annotation` (`annotation=`, `annotation==`, `annotation!=`, or `annotation!==`) Prune containers with (or without, in case `annotation!=...` is used) the specified annotations. // - `label` (`label=`, `label==`, `label!=`, or `label!==`) Prune containers with (or without, in case `label!=...` is used) the specified labels. + // - `until=` Prune containers created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. // produces: // - application/json // responses: @@ -815,9 +817,10 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // description: | // A JSON encoded value of the filters (a `map[string][]string`) to process on the containers list. Available filters: // - `ancestor`=(`[:]`, ``, or ``) + // - `annotation`=(`key` or `"key=value"`) of a container annotation // - `before`=(`` or ``) - // - `expose`=(`[/]` or `/[]`) // - `exited=` containers with exit code of `` + // - `expose`=(`[/]` or `/[]`) // - `health`=(`starting`, `healthy`, `unhealthy` or `none`) // - `id=` a container's ID // - `is-task`=(`true` or `false`) @@ -851,8 +854,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // type: string // description: | // Filters to process on the prune list, encoded as JSON (a `map[string][]string`). Available filters: - // - `until=` Prune containers created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. + // - `annotation` (`annotation=`, `annotation==`, `annotation!=`, or `annotation!==`) Prune containers with (or without, in case `annotation!=...` is used) the specified annotations. // - `label` (`label=`, `label==`, `label!=`, or `label!==`) Prune containers with (or without, in case `label!=...` is used) the specified labels. + // - `until=` Prune containers created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. // produces: // - application/json // responses: diff --git a/pkg/domain/filters/containers.go b/pkg/domain/filters/containers.go index 47a738bde2..273ece6604 100644 --- a/pkg/domain/filters/containers.go +++ b/pkg/domain/filters/containers.go @@ -34,6 +34,14 @@ func GenerateContainerFilterFuncs(filter string, filterValues []string, r *libpo return func(c *libpod.Container) bool { return filters.MatchNegatedLabelFilters(filterValues, c.Labels()) }, nil + case "annotation": + return func(c *libpod.Container) bool { + return filters.MatchLabelFilters(filterValues, c.ConfigNoCopy().Spec.Annotations) + }, nil + case "annotation!": + return func(c *libpod.Container) bool { + return filters.MatchNegatedLabelFilters(filterValues, c.ConfigNoCopy().Spec.Annotations) + }, nil case "name": // we only have to match one name return func(c *libpod.Container) bool { @@ -311,6 +319,14 @@ func GeneratePruneContainerFilterFuncs(filter string, filterValues []string, _ * return func(c *libpod.Container) bool { return filters.MatchNegatedLabelFilters(filterValues, c.Labels()) }, nil + case "annotation": + return func(c *libpod.Container) bool { + return filters.MatchLabelFilters(filterValues, c.ConfigNoCopy().Spec.Annotations) + }, nil + case "annotation!": + return func(c *libpod.Container) bool { + return filters.MatchNegatedLabelFilters(filterValues, c.ConfigNoCopy().Spec.Annotations) + }, nil case "until": return prepareUntilFilterFunc(filterValues) } @@ -330,7 +346,7 @@ func prepareUntilFilterFunc(filterValues []string) (func(container *libpod.Conta }, nil } -// GenerateContainerFilterFuncs return ContainerFilter functions based of filter. +// GenerateExternalContainerFilterFuncs return ContainerFilter functions for external containers func GenerateExternalContainerFilterFuncs(filter string, filterValues []string, r *libpod.Runtime) (func(listContainer *types.ListContainer) bool, error) { switch filter { case "id": @@ -534,7 +550,7 @@ func GenerateExternalContainerFilterFuncs(filter string, filterValues []string, } return false }, nil - case "restart-policy", "volume", "health": + case "restart-policy", "volume", "health", "annotation", "annotation!": return nil, fmt.Errorf("filter %s is not applicable for external containers", filter) } diff --git a/test/e2e/prune_test.go b/test/e2e/prune_test.go index 2b80bb11ef..ba37341617 100644 --- a/test/e2e/prune_test.go +++ b/test/e2e/prune_test.go @@ -79,6 +79,22 @@ var _ = Describe("Podman prune", func() { Expect(podmanTest.NumberOfContainers()).To(Equal(0)) }) + It("podman container prune filters by annotation", func() { + podmanTest.PodmanExitCleanly("create", "--annotation", "prune=me", "--name", "prune-me", BB) + podmanTest.PodmanExitCleanly("create", "--annotation", "prune=keep", "--name", "keep-me", BB) + podmanTest.PodmanExitCleanly("container", "prune", "-f", "--filter", "annotation=prune=me") + session := podmanTest.PodmanExitCleanly("ps", "-a", "--format", "{{.Names}}") + Expect(session.OutputToStringArray()).To(Equal([]string{"keep-me"})) + }) + + It("podman container prune filters by negated annotation", func() { + podmanTest.PodmanExitCleanly("create", "--annotation", "prune=me", "--name", "prune-me", BB) + podmanTest.PodmanExitCleanly("create", "--annotation", "prune=keep", "--name", "keep-me", BB) + podmanTest.PodmanExitCleanly("container", "prune", "-f", "--filter", "annotation!=prune=me") + session := podmanTest.PodmanExitCleanly("ps", "-a", "--format", "{{.Names}}") + Expect(session.OutputToStringArray()).To(Equal([]string{"prune-me"})) + }) + It("podman image prune - remove only dangling images", func() { session := podmanTest.Podman([]string{"images", "-a"}) session.WaitWithDefaultTimeout() diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go index 92ae13d202..6d4fd3f02b 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -211,6 +211,20 @@ var _ = Describe("Podman ps", func() { Expect(actual).ToNot(ContainSubstring("table")) }) + It("podman ps --filter annotation=test=with,comma", func() { + ctrAlpha := "first" + podmanTest.PodmanExitCleanly("create", "--annotation", "test=with,comma", "--name", ctrAlpha, ALPINE, "top") + + ctrBravo := "second" + podmanTest.PodmanExitCleanly("create", "--name", ctrBravo, ALPINE, "top") + + result := podmanTest.PodmanExitCleanly("ps", "-a", "--format", "{{.Names}}", "--filter", "annotation=test=with,comma") + Expect(result.OutputToStringArray()).To(Equal([]string{ctrAlpha})) + + result = podmanTest.PodmanExitCleanly("ps", "-a", "--format", "{{.Names}}", "--filter", "annotation!=test=with,comma") + Expect(result.OutputToStringArray()).To(Equal([]string{ctrBravo})) + }) + It("podman ps namespace flag", func() { _, ec, _ := podmanTest.RunLsContainer("") Expect(ec).To(Equal(0)) @@ -1014,6 +1028,18 @@ var _ = Describe("Podman ps", func() { Expect(output).To(HaveLen(1)) }) + It("podman ps rejects annotation filters for external containers", func() { + podmanTest.PodmanExitCleanly("create", "--name", "test", BB) + + session := podmanTest.Podman([]string{"ps", "-a", "--external", "--filter", "annotation=test=value"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitWithError(125, "filter annotation is not applicable for external containers")) + + session = podmanTest.Podman([]string{"ps", "-a", "--external", "--filter", "annotation!=test=value"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitWithError(125, "filter annotation! is not applicable for external containers")) + }) + // This test checks ps filtering of external container created earlier than a given It("podman ps filter external by container created earlier than a given", func() { early := podmanTest.Podman([]string{"create", "--name", "early", BB})