From d2b850286979ca2cfdb25b7d2ce69982eaea4915 Mon Sep 17 00:00:00 2001 From: Garv Changrani <154041471+Garvity@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:10:44 +0000 Subject: [PATCH] ps: fix label lookup in container and pod table headings Table headings for `podman ps` and `podman pod ps` use a plain `map[string]string`. A table template that calls `.Label` with an argument fails while rendering the headings, before listing rows are printed. For example: `podman ps --format 'table {{.Label "app"}}'` Add a shared `PsHeader` type in `cmd/podman/common` with a `Label` method that returns the requested label name as the column heading. Use this type for both container and pod listings. Add container unit tests and extend the existing container and pod e2e tests to check label headings, label values, missing labels, unlabeled objects, and `--noheading` output. Fixes: #29831 Signed-off-by: Garv Changrani <154041471+Garvity@users.noreply.github.com> --- cmd/podman/common/format.go | 8 +++++ cmd/podman/containers/ps.go | 4 +-- cmd/podman/containers/ps_test.go | 58 ++++++++++++++++++++++++++++++++ cmd/podman/pods/ps.go | 2 +- test/e2e/pod_ps_test.go | 23 ++++++++++--- test/e2e/ps_test.go | 18 ++++++++-- 6 files changed, 102 insertions(+), 11 deletions(-) diff --git a/cmd/podman/common/format.go b/cmd/podman/common/format.go index 3a8ec86830..94c4f234a5 100644 --- a/cmd/podman/common/format.go +++ b/cmd/podman/common/format.go @@ -19,3 +19,11 @@ func FormatLabels(labels map[string]string) string { } return strings.Join(list, ",") } + +// PsHeader provides column headings for container and pod listings. +type PsHeader map[string]string + +// Label uses the requested label name as the column heading. +func (h PsHeader) Label(name string) string { + return name +} diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index 129c091039..dca7deefea 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -295,7 +295,7 @@ func ps(cmd *cobra.Command, _ []string) error { } // cannot use report.Headers() as it doesn't support structures as fields -func createPsOut() ([]map[string]string, string) { +func createPsOut() ([]common.PsHeader, string) { hdrs := report.Headers(psReporter{}, map[string]string{ "Cgroup": "cgroupns", "CreatedHuman": "created", @@ -327,7 +327,7 @@ func createPsOut() ([]map[string]string, string) { row += "\t{{.Size}}" } } - return hdrs, "{{range .}}" + row + "\n{{end -}}" + return []common.PsHeader{common.PsHeader(hdrs[0])}, "{{range .}}" + row + "\n{{end -}}" } type psReporter struct { diff --git a/cmd/podman/containers/ps_test.go b/cmd/podman/containers/ps_test.go index 9d4073e275..4af84d3434 100644 --- a/cmd/podman/containers/ps_test.go +++ b/cmd/podman/containers/ps_test.go @@ -1,12 +1,70 @@ package containers import ( + "bytes" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.podman.io/common/libnetwork/types" + "go.podman.io/common/pkg/report" + "go.podman.io/podman/v6/pkg/domain/entities" ) +func TestPsLabelFormat(t *testing.T) { + for _, tt := range []struct { + name string + format string + want string + }{ + { + name: "table labels", + format: `table {{.Names}}|{{.Label "app"}}|{{.Label "com.example.project"}}|{{.Label "missing"}}`, + want: "NAMES|app|com.example.project|missing\nwith-label|my-app|my-project|\nwithout-label|||\n", + }, + { + name: "labels without headings", + format: `{{.Names}}|{{.Label "app"}}`, + want: "with-label|my-app\nwithout-label|\n", + }, + { + name: "ordinary headings", + format: `table {{.Names}}|{{.ID}}`, + want: "NAMES|CONTAINER ID\nwith-label|123456789abc\nwithout-label|abcdef123456\n", + }, + } { + t.Run(tt.name, func(t *testing.T) { + var output bytes.Buffer + rpt, err := report.New(&output, "ps").Parse(report.OriginUser, tt.format) + require.NoError(t, err) + if rpt.RenderHeaders { + headers, _ := createPsOut() + require.NoError(t, rpt.Execute(headers)) + } + require.NoError(t, rpt.Execute([]psReporter{ + { + ListContainer: entities.ListContainer{ + ID: "123456789abc", + Names: []string{"with-label"}, + Labels: map[string]string{ + "app": "my-app", + "com.example.project": "my-project", + }, + }, + }, + { + ListContainer: entities.ListContainer{ + ID: "abcdef123456", + Names: []string{"without-label"}, + }, + }, + })) + require.NoError(t, rpt.Flush()) + assert.Equal(t, tt.want, output.String()) + }) + } +} + func Test_portsToString(t *testing.T) { tests := []struct { name string diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go index 59ded1261b..278dd0feb3 100644 --- a/cmd/podman/pods/ps.go +++ b/cmd/podman/pods/ps.go @@ -151,7 +151,7 @@ func pods(cmd *cobra.Command, _ []string) error { "Restarts": "RESTARTS", }) - if err := rpt.Execute(headers); err != nil { + if err := rpt.Execute([]common.PsHeader{common.PsHeader(headers[0])}); err != nil { return err } } diff --git a/test/e2e/pod_ps_test.go b/test/e2e/pod_ps_test.go index ece8215276..bddb0533e2 100644 --- a/test/e2e/pod_ps_test.go +++ b/test/e2e/pod_ps_test.go @@ -352,19 +352,32 @@ var _ = Describe("Podman ps", func() { }) It("podman pod ps format with labels", func() { - _, ec, _ := podmanTest.CreatePod(nil) + _, ec, _ := podmanTest.CreatePod(map[string][]string{"--name": {"without-label"}}) Expect(ec).To(Equal(0)) - _, ec1, _ := podmanTest.CreatePod(map[string][]string{"--label": { - "io.podman.test.label=value1", - "io.podman.test.key=irrelevant-value", - }}) + _, ec1, _ := podmanTest.CreatePod(map[string][]string{ + "--name": {"with-label"}, + "--label": { + "io.podman.test.label=value1", + "io.podman.test.key=irrelevant-value", + }, + }) Expect(ec1).To(Equal(0)) session := podmanTest.Podman([]string{"pod", "ps", "--format", "{{.Labels}}"}) session.WaitWithDefaultTimeout() Expect(session).Should(ExitCleanly()) Expect(session.OutputToString()).To(ContainSubstring("value1")) + + format := `table {{.Name}}|{{.Label "io.podman.test.label"}}|{{.Label "missing"}}` + session = podmanTest.PodmanExitCleanly("pod", "ps", "--format", format) + actual := session.OutputToStringArray() + Expect(actual).To(HaveLen(3)) + Expect(actual[0]).To(Equal("NAME|io.podman.test.label|missing")) + Expect(actual[1:]).To(ConsistOf("with-label|value1|", "without-label||")) + + session = podmanTest.PodmanExitCleanly("pod", "ps", "--noheading", "--format", format) + Expect(session.OutputToStringArray()).To(ConsistOf("with-label|value1|", "without-label||")) }) It("podman pod ps headers", func() { diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go index d411b91998..bb45476362 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -298,10 +298,11 @@ var _ = Describe("Podman ps", func() { }) It("podman ps namespace flag with go template format", func() { - _, ec, _ := podmanTest.RunLsContainer("test1") - Expect(ec).To(Equal(0)) + result := podmanTest.Podman([]string{"run", "--name", "test1", "--network=none", "--label", "app=my-app", ALPINE, "ls"}) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) - result := podmanTest.Podman([]string{"ps", "-a", "--format", "table {{.ID}} {{.Image}} {{.ImageID}} {{.Labels}}"}) + result = podmanTest.Podman([]string{"ps", "-a", "--format", "table {{.ID}} {{.Image}} {{.ImageID}} {{.Labels}}"}) result.WaitWithDefaultTimeout() Expect(result).Should(ExitCleanly()) @@ -311,6 +312,17 @@ var _ = Describe("Podman ps", func() { Expect(actual[0]).To(ContainSubstring("CONTAINER ID")) Expect(actual[0]).ToNot(ContainSubstring("ImageID")) Expect(actual[1]).To(ContainSubstring("alpine:latest")) + + podmanTest.PodmanExitCleanly("create", "--name", "test2", ALPINE, "ls") + format := `table {{.Names}}|{{.Label "app"}}|{{.Label "missing"}}` + result = podmanTest.PodmanExitCleanly("ps", "-a", "--format", format) + actual = result.OutputToStringArray() + Expect(actual).To(HaveLen(3)) + Expect(actual[0]).To(Equal("NAMES|app|missing")) + Expect(actual[1:]).To(ConsistOf("test1|my-app|", "test2||")) + + result = podmanTest.PodmanExitCleanly("ps", "-a", "--noheading", "--format", format) + Expect(result.OutputToStringArray()).To(ConsistOf("test1|my-app|", "test2||")) }) It("podman ps ancestor filter flag", func() {