From f460bc0ae5a1efdd3f7e85a0bfc25886fae54c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Rod=C3=A1k?= Date: Thu, 30 Apr 2026 14:32:00 +0200 Subject: [PATCH] ps: format labels as comma separated key=value for Docker compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/containers/podman/issues/21847 Signed-off-by: Jan Rodák --- cmd/podman/common/format.go | 21 ++++++++++++ cmd/podman/containers/ps.go | 8 ++++- cmd/podman/pods/ps.go | 7 ++-- docs/source/markdown/podman-pod-ps.1.md.in | 2 +- docs/source/markdown/podman-ps.1.md.in | 2 +- test/system/040-ps.bats | 37 ++++++++++++++++++++++ test/system/200-pod.bats | 2 +- test/upgrade/test-upgrade.bats | 6 ++-- 8 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 cmd/podman/common/format.go diff --git a/cmd/podman/common/format.go b/cmd/podman/common/format.go new file mode 100644 index 0000000000..3a8ec86830 --- /dev/null +++ b/cmd/podman/common/format.go @@ -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, ",") +} diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index fe2264d643..d60e0c33a2 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -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] } diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go index 9a2b2a2fff..59ded1261b 100644 --- a/cmd/podman/pods/ps.go +++ b/cmd/podman/pods/ps.go @@ -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 diff --git a/docs/source/markdown/podman-pod-ps.1.md.in b/docs/source/markdown/podman-pod-ps.1.md.in index d7281b2bde..d64dd8667d 100644 --- a/docs/source/markdown/podman-pod-ps.1.md.in +++ b/docs/source/markdown/podman-pod-ps.1.md.in @@ -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 | diff --git a/docs/source/markdown/podman-ps.1.md.in b/docs/source/markdown/podman-ps.1.md.in index 318a3a3050..2b5f69ed9b 100644 --- a/docs/source/markdown/podman-ps.1.md.in +++ b/docs/source/markdown/podman-ps.1.md.in @@ -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 | diff --git a/test/system/040-ps.bats b/test/system/040-ps.bats index 51b0f6cb97..493ee8273b 100644 --- a/test/system/040-ps.bats +++ b/test/system/040-ps.bats @@ -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) diff --git a/test/system/200-pod.bats b/test/system/200-pod.bats index 4f957f49e5..5018538069 100644 --- a/test/system/200-pod.bats +++ b/test/system/200-pod.bats @@ -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=..." diff --git a/test/upgrade/test-upgrade.bats b/test/upgrade/test-upgrade.bats index d352c36f5e..1eb25bf59e 100644 --- a/test/upgrade/test-upgrade.bats +++ b/test/upgrade/test-upgrade.bats @@ -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\).*----" "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----" "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