filters: add annotation filter for containers Fixes: #28562

Signed-off-by: Jiwoo Ahn <ikwydls1314@gmail.com>
This commit is contained in:
Jiwoo Ahn 2026-05-04 16:01:44 +09:00
parent a7ed92280a
commit ec583fa17f
8 changed files with 96 additions and 17 deletions

View file

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

View file

@ -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 |

View file

@ -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**.

View file

@ -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

View file

@ -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`=(`<image-name>[:<tag>]`, `<image id>`, or `<image@digest>`)
// - `annotation`=(`key` or `"key=value"`) of a container annotation
// - `before`=(`<container id>` or `<container name>`)
// - `expose`=(`<port>[/<proto>]` or `<startport-endport>/[<proto>]`)
// - `exited=<int>` containers with exit code of `<int>`
// - `expose`=(`<port>[/<proto>]` or `<startport-endport>/[<proto>]`)
// - `health`=(`starting`, `healthy`, `unhealthy` or `none`)
// - `id=<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=<timestamp>` Prune containers 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 machines time.
// - `annotation` (`annotation=<key>`, `annotation=<key>=<value>`, `annotation!=<key>`, or `annotation!=<key>=<value>`) Prune containers with (or without, in case `annotation!=...` is used) the specified annotations.
// - `label` (`label=<key>`, `label=<key>=<value>`, `label!=<key>`, or `label!=<key>=<value>`) Prune containers with (or without, in case `label!=...` is used) the specified labels.
// - `until=<timestamp>` Prune containers 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 machines 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`=(`<image-name>[:<tag>]`, `<image id>`, or `<image@digest>`)
// - `annotation`=(`key` or `"key=value"`) of a container annotation
// - `before`=(`<container id>` or `<container name>`)
// - `expose`=(`<port>[/<proto>]` or `<startport-endport>/[<proto>]`)
// - `exited=<int>` containers with exit code of `<int>`
// - `expose`=(`<port>[/<proto>]` or `<startport-endport>/[<proto>]`)
// - `health`=(`starting`, `healthy`, `unhealthy` or `none`)
// - `id=<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=<timestamp>` Prune containers 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 machines time.
// - `annotation` (`annotation=<key>`, `annotation=<key>=<value>`, `annotation!=<key>`, or `annotation!=<key>=<value>`) Prune containers with (or without, in case `annotation!=...` is used) the specified annotations.
// - `label` (`label=<key>`, `label=<key>=<value>`, `label!=<key>`, or `label!=<key>=<value>`) Prune containers with (or without, in case `label!=...` is used) the specified labels.
// - `until=<timestamp>` Prune containers 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 machines time.
// produces:
// - application/json
// responses:

View file

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

View file

@ -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()

View file

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