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>
This commit is contained in:
Garv Changrani 2026-09-29 11:10:44 +00:00
parent a1ce95d761
commit d2b8502869
6 changed files with 102 additions and 11 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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