From b4a1f619aeb3790302831e365a670039a5d387a1 Mon Sep 17 00:00:00 2001 From: Nick Sieger Date: Tue, 21 Jul 2026 16:07:40 -0500 Subject: [PATCH] feat(ps): add ENGINE column gated on label presence Show an ENGINE column in `compose ps` default table output when the com.docker.compose.engine label is present. Expose {{.Engine}} for custom --format templates. Rename api.EngineLabel to api.ContainerEngineLabel to avoid the name collision with desktop.EngineLabel (com.docker.desktop.address). Signed-off-by: Nick Sieger --- cmd/compose/ps.go | 6 ++- cmd/formatter/container.go | 16 ++++++- cmd/formatter/container_test.go | 78 +++++++++++++++++++++++++++++++++ pkg/api/labels.go | 2 + 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 cmd/formatter/container_test.go diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go index 2528fccacfb..2d8194db027 100644 --- a/cmd/compose/ps.go +++ b/cmd/compose/ps.go @@ -156,9 +156,13 @@ func runPs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOp opts.Format = dockerCli.ConfigFile().PsFormat } + showEngine := slices.ContainsFunc(containers, func(c api.ContainerSummary) bool { + return c.Labels[api.ContainerEngineLabel] != "" + }) + containerCtx := cliformatter.Context{ Output: dockerCli.Out(), - Format: formatter.NewContainerFormat(opts.Format, opts.Quiet, false), + Format: formatter.NewContainerFormat(opts.Format, opts.Quiet, false, showEngine), Trunc: !opts.noTrunc, } return formatter.ContainerWrite(containerCtx, containers) diff --git a/cmd/formatter/container.go b/cmd/formatter/container.go index 546def442bb..860e7a60718 100644 --- a/cmd/formatter/container.go +++ b/cmd/formatter/container.go @@ -42,16 +42,20 @@ const ( mountsHeader = "MOUNTS" localVolumes = "LOCAL VOLUMES" networksHeader = "NETWORKS" + engineHeader = "ENGINE" ) // NewContainerFormat returns a Format for rendering using a Context -func NewContainerFormat(source string, quiet bool, size bool) formatter.Format { +func NewContainerFormat(source string, quiet bool, size bool, engine bool) formatter.Format { switch source { case formatter.TableFormatKey, "": // table formatting is the default if none is set. if quiet { return formatter.DefaultQuietFormat } format := defaultContainerTableFormat + if engine { + format += `\t{{.Engine}}` + } if size { format += `\t{{.Size}}` } @@ -126,6 +130,7 @@ func NewContainerContext() *ContainerContext { "Status": formatter.StatusHeader, "Size": formatter.SizeHeader, "Labels": formatter.LabelsHeader, + "Engine": engineHeader, } return &containerCtx } @@ -245,6 +250,15 @@ func (c *ContainerContext) Labels() string { return strings.Join(joinLabels, ",") } +// Engine returns the name of the engine that runs the container, as stored in +// the com.docker.compose.engine label, or an empty string if unset. +func (c *ContainerContext) Engine() string { + if c.c.Labels == nil { + return "" + } + return c.c.Labels[api.ContainerEngineLabel] +} + // Label returns the value of the label with the given name or an empty string // if the given label does not exist. func (c *ContainerContext) Label(name string) string { diff --git a/cmd/formatter/container_test.go b/cmd/formatter/container_test.go new file mode 100644 index 00000000000..4996fd85434 --- /dev/null +++ b/cmd/formatter/container_test.go @@ -0,0 +1,78 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package formatter + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/cli/cli/command/formatter" + "gotest.tools/v3/assert" + + "github.com/docker/compose/v5/pkg/api" +) + +func TestContainerContextEngine(t *testing.T) { + withLabel := ContainerContext{c: api.ContainerSummary{ + Labels: map[string]string{api.ContainerEngineLabel: "moby"}, + }} + assert.Equal(t, withLabel.Engine(), "moby") + + noLabels := ContainerContext{c: api.ContainerSummary{}} + assert.Equal(t, noLabels.Engine(), "") + + otherLabel := ContainerContext{c: api.ContainerSummary{ + Labels: map[string]string{api.ProjectLabel: "test"}, + }} + assert.Equal(t, otherLabel.Engine(), "") +} + +func TestContainerWriteEngineColumn(t *testing.T) { + containers := []api.ContainerSummary{ + { + Name: "with-engine", + Service: "svc1", + Labels: map[string]string{api.ContainerEngineLabel: "moby"}, + }, + { + Name: "without-engine", + Service: "svc2", + }, + } + + t.Run("engine column shown when label present", func(t *testing.T) { + var out bytes.Buffer + ctx := formatter.Context{ + Output: &out, + Format: NewContainerFormat("table", false, false, true), + } + assert.NilError(t, ContainerWrite(ctx, containers)) + assert.Assert(t, strings.Contains(out.String(), engineHeader), out.String()) + assert.Assert(t, strings.Contains(out.String(), "moby"), out.String()) + }) + + t.Run("engine column hidden when not requested", func(t *testing.T) { + var out bytes.Buffer + ctx := formatter.Context{ + Output: &out, + Format: NewContainerFormat("table", false, false, false), + } + assert.NilError(t, ContainerWrite(ctx, containers)) + assert.Assert(t, !strings.Contains(out.String(), engineHeader), out.String()) + }) +} diff --git a/pkg/api/labels.go b/pkg/api/labels.go index bc9d1f4a1b9..793973813c4 100644 --- a/pkg/api/labels.go +++ b/pkg/api/labels.go @@ -60,6 +60,8 @@ const ( ImageBuilderLabel = "com.docker.compose.image.builder" // ContainerReplaceLabel is set when container is created to replace another container (recreated) ContainerReplaceLabel = "com.docker.compose.replace" + // ContainerEngineLabel stores the name of the engine that runs the container + ContainerEngineLabel = "com.docker.compose.engine" ) // ComposeVersion is the compose tool version as declared by label VersionLabel