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