ps: format labels as comma separated key=value for Docker compatibility

Fixes: https://github.com/containers/podman/issues/21847

Signed-off-by: Jan Rodák <hony.com@seznam.cz>
This commit is contained in:
Jan Rodák
2026-04-30 14:32:00 +02:00
parent 1cae4a9d63
commit f460bc0ae5
8 changed files with 75 additions and 10 deletions

View File

@@ -0,0 +1,21 @@
package common
import (
"slices"
"strings"
)
// FormatLabels converts a map of labels to a sorted, comma-separated list
// of key=value pairs, matching Docker CLI output format.
func FormatLabels(labels map[string]string) string {
keys := make([]string, 0, len(labels))
for k := range labels {
keys = append(keys, k)
}
slices.Sort(keys)
list := make([]string, 0, len(keys))
for _, k := range keys {
list = append(list, k+"="+labels[k])
}
return strings.Join(list, ",")
}

View File

@@ -342,7 +342,13 @@ func (l psReporter) ImageID() string {
return l.ListContainer.ImageID
}
// Label returns a map of the pod's labels
// Labels returns the container's labels as a sorted, comma-separated list of
// key=value pairs, matching Docker CLI output format.
func (l psReporter) Labels() string {
return common.FormatLabels(l.ListContainer.Labels)
}
// Label returns the value of a single container label by name.
func (l psReporter) Label(name string) string {
return l.ListContainer.Labels[name]
}

View File

@@ -194,9 +194,10 @@ func (l ListPodReporter) Created() string {
return units.HumanDuration(time.Since(l.ListPodsReport.Created)) + " ago"
}
// Labels returns a map of the pod's labels
func (l ListPodReporter) Labels() map[string]string {
return l.ListPodsReport.Labels
// Labels returns the pod's labels as a sorted, comma-separated list of
// key=value pairs, matching Docker CLI output format.
func (l ListPodReporter) Labels() string {
return common.FormatLabels(l.ListPodsReport.Labels)
}
// Label returns a map of the pod's labels

View File

@@ -58,7 +58,7 @@ Valid placeholders for the Go template are listed below:
| .ID | Container ID |
| .InfraID | Pod infra container ID |
| .Label *string* | Specified label of the pod |
| .Labels ... | All the labels assigned to the pod |
| .Labels | All the labels assigned to the pod |
| .Name | Name of pod |
| .Networks | Show all networks connected to the infra container |
| .NumberOfContainers | Show the number of containers attached to pod |

View File

@@ -61,7 +61,7 @@ Valid placeholders for the Go template are listed below:
| .ImageID | Image ID |
| .IsInfra | "true" if infra container |
| .Label *string* | Specified label of the container |
| .Labels ... | All the labels assigned to the container |
| .Labels | All the labels assigned to the container |
| .Mounts | Volumes mounted in the container |
| .Names | Name of container |
| .Networks | Show all networks connected to the container |

View File

@@ -239,6 +239,43 @@ load helpers
run_podman pod rm -t 0 -f test
}
@test "podman ps --format json Labels is a string" {
rand_value=$(random_string 10)
run_podman run -d --label mylabel=$rand_value $IMAGE sleep inf
cid=$output
# {{json .Labels}} should produce a JSON string ("key=val,..."), not a JSON object
run_podman ps --format '{{json .Labels}}'
assert "$output" =~ "\".*mylabel=${rand_value}.*\"" "json .Labels should be a quoted string, not a JSON object"
assert "$output" !~ "{" "json .Labels should not contain braces (not a JSON object)"
# Plain {{.Labels}} should produce key=value format
run_podman ps --format '{{.Labels}}'
assert "$output" =~ "mylabel=${rand_value}" ".Labels should contain key=value pair"
assert "$output" !~ "map\[" ".Labels should not use Go map format"
run_podman rm -t 0 -f $cid
}
@test "podman pod ps --format json Labels is a string" {
rand_value=$(random_string 10)
run_podman pod create --label mylabel=${rand_value} test
# {{json .Labels}} should produce a JSON string, not a JSON object
run_podman pod ps --format '{{json .Labels}}'
assert "$output" =~ "\".*mylabel=${rand_value}.*\"" "json .Labels should be a quoted string, not a JSON object"
assert "$output" !~ "{" "json .Labels should not contain braces (not a JSON object)"
# Plain {{.Labels}} should produce key=value format
run_podman pod ps --format '{{.Labels}}'
assert "$output" =~ "mylabel=${rand_value}" ".Labels should contain key=value pair"
assert "$output" !~ "map\[" ".Labels should not use Go map format"
run_podman pod rm -t 0 -f test
}
@test "podman ps --format PodName" {
rand_value=$(random_string 10)

View File

@@ -309,7 +309,7 @@ EOF
# pod ps
run_podman pod ps --format '{{.ID}} {{.Name}} {{.Status}} {{.Labels}}'
assert "$output" =~ "${pod_id:0:12} $podname Running map\[${labelname}:${labelvalue}]" "pod ps"
assert "$output" =~ "${pod_id:0:12} $podname Running ${labelname}=${labelvalue}" "pod ps"
run_podman pod ps --no-trunc --filter "label=${labelname}=${labelvalue}" --format '{{.ID}}'
is "$output" "$pod_id" "pod ps --filter label=..."

View File

@@ -213,10 +213,10 @@ EOF
@test "ps -a : shows all containers" {
run_podman ps -a \
--format '{{.Names}}--{{.Status}}--{{.Ports}}--{{.Labels.mylabel}}' \
--format '{{.Names}}--{{.Status}}--{{.Ports}}--{{.Label "mylabel"}}' \
--sort=created
assert "${lines[0]}" == "mycreatedcontainer--Created----$LABEL_CREATED" "line 0, created"
assert "${lines[1]}" =~ "mydonecontainer--Exited \(0\).*----<no value>" "line 1, done"
assert "${lines[1]}" =~ "mydonecontainer--Exited \(0\).*----" "line 1, done"
assert "${lines[2]}" =~ "myfailedcontainer--Exited \(17\) .*----$LABEL_FAILED" "line 2, fail"
# Port order is not guaranteed
@@ -224,7 +224,7 @@ EOF
assert "${lines[3]}" =~ ".*--.*0\.0\.0\.0:$HOST_PORT->80\/tcp.*--.*" "line 3, first port forward"
assert "${lines[3]}" =~ ".*--.*127\.0\.0\.1\:9090-9092->8080-8082\/tcp.*--.*" "line 3, second port forward"
assert "${lines[4]}" =~ ".*-infra--Created----<no value>" "line 4, infra container"
assert "${lines[4]}" =~ ".*-infra--Created----" "line 4, infra container"
# For debugging: dump containers and IDs
if [[ -n "$PODMAN_UPGRADE_TEST_DEBUG" ]]; then