From 7aa6c79c0a33ef995a9c5a804f92bb55c4908066 Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Thu, 6 Feb 2025 15:11:27 -0600 Subject: [PATCH 1/2] docker ps: add "Platform" as formatting option docker ps --format 'table {{.ID}}\t{{.Image}}{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}\t{{.Platform}}' CONTAINER ID IMAGECOMMAND CREATED STATUS PORTS NAMES PLATFORM e422855eac55 docker-cli-dev"/bin/bash" 12 minutes ago Up 12 minutes strange_jennings linux/arm64 Signed-off-by: Jonathan A. Sternberg Signed-off-by: Sebastiaan van Stijn --- cli/command/formatter/container.go | 12 ++++++ cli/command/formatter/container_test.go | 51 +++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 7e10091a8d..0e7f8e0f32 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/containerd/platforms" "github.com/distribution/reference" "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/stringid" @@ -26,6 +27,7 @@ const ( mountsHeader = "MOUNTS" localVolumes = "LOCAL VOLUMES" networksHeader = "NETWORKS" + platformHeader = "PLATFORM" ) // NewContainerFormat returns a Format for rendering using a Context @@ -109,6 +111,7 @@ func NewContainerContext() *ContainerContext { "Mounts": mountsHeader, "LocalVolumes": localVolumes, "Networks": networksHeader, + "Platform": platformHeader, } return &containerCtx } @@ -208,6 +211,15 @@ func (c *ContainerContext) RunningFor() string { return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" } +// Platform returns a human-readable representation of the container's +// platform if it is available. +func (c *ContainerContext) Platform() string { + if c.c.ImageManifestDescriptor != nil && c.c.ImageManifestDescriptor.Platform != nil { + return platforms.FormatAll(*c.c.ImageManifestDescriptor.Platform) + } + return "" +} + // Ports returns a comma-separated string representing open ports of the container // e.g. "0.0.0.0:80->9090/tcp, 9988/tcp" // it's used by command 'docker ps' diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 584e29def8..003ce87c2a 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -14,6 +14,7 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/stringid" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -425,13 +426,36 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) { func TestContainerContextWriteJSON(t *testing.T) { unix := time.Now().Add(-65 * time.Second).Unix() containers := []container.Summary{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix, State: container.StateRunning}, - {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix, State: container.StateRunning}, + { + ID: "containerID1", + Names: []string{"/foobar_baz"}, + Image: "ubuntu", + Created: unix, + State: container.StateRunning, + }, + { + ID: "containerID2", + Names: []string{"/foobar_bar"}, + Image: "ubuntu", + Created: unix, + State: container.StateRunning, + + ImageManifestDescriptor: &ocispec.Descriptor{Platform: &ocispec.Platform{Architecture: "amd64", OS: "linux"}}, + }, + { + ID: "containerID3", + Names: []string{"/foobar_bar"}, + Image: "ubuntu", + Created: unix, + State: container.StateRunning, + + ImageManifestDescriptor: &ocispec.Descriptor{Platform: &ocispec.Platform{}}, + }, } expectedCreated := time.Unix(unix, 0).String() expectedJSONs := []map[string]any{ { - "Command": "\"\"", + "Command": `""`, "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", @@ -440,6 +464,7 @@ func TestContainerContextWriteJSON(t *testing.T) { "Mounts": "", "Names": "foobar_baz", "Networks": "", + "Platform": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", @@ -447,7 +472,7 @@ func TestContainerContextWriteJSON(t *testing.T) { "Status": "", }, { - "Command": "\"\"", + "Command": `""`, "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", @@ -456,6 +481,24 @@ func TestContainerContextWriteJSON(t *testing.T) { "Mounts": "", "Names": "foobar_bar", "Networks": "", + "Platform": "linux/amd64", + "Ports": "", + "RunningFor": "About a minute ago", + "Size": "0B", + "State": "running", + "Status": "", + }, + { + "Command": `""`, + "CreatedAt": expectedCreated, + "ID": "containerID3", + "Image": "ubuntu", + "Labels": "", + "LocalVolumes": "0", + "Mounts": "", + "Names": "foobar_bar", + "Networks": "", + "Platform": "unknown", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", From 67c0be4b0591f98c50393ca847027cce26d61ed6 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 13 May 2025 12:53:14 +0200 Subject: [PATCH 2/2] docker ps: allow formatting as JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this patch: docker ps --format 'table {{.Names}}\t{{.Platform}}' NAMES PLATFORM optimistic_nightingale linux/arm64 mycontainer linux/arm64/v8 trusting_goldstine linux/arm64 docker ps --format '{{.Platform}}' linux/arm64 linux/arm64/v8 linux/arm64 docker ps --format '{{json .Platform}}' {"architecture":"arm64","os":"linux"} {"architecture":"arm64","os":"linux","variant":"v8"} {"architecture":"arm64","os":"linux"} docker ps --format 'json' {"Command":"\"/bin/bash\"","CreatedAt":"2025-05-13 10:12:19 +0000 UTC","ID":"e8b3b2d604f1","Image":"docker-cli-dev","Labels":"desktop.docker.io/binds/0/Source=/Users/thajeztah/go/src/github.com/docker/cli,desktop.docker.io/binds/0/SourceKind=hostFile,desktop.docker.io/binds/0/Target=/go/src/github.com/docker/cli,desktop.docker.io/mounts/0/Source=/var/run/docker.sock,desktop.docker.io/mounts/0/SourceKind=dockerSocketProxied,desktop.docker.io/mounts/0/Target=/var/run/docker.sock,desktop.docker.io/ports.scheme=v2","LocalVolumes":"1","Mounts":"/host_mnt/User…,docker-cli-dev…,/run/host-serv…","Names":"optimistic_nightingale","Networks":"bridge","Platform":{"architecture":"arm64","os":"linux"},"Ports":"","RunningFor":"38 minutes ago","Size":"0B","State":"running","Status":"Up 38 minutes"} {"Command":"\"/docker-entrypoint.…\"","CreatedAt":"2025-05-13 09:58:01 +0000 UTC","ID":"c93b808dd54e","Image":"nginx:alpine","Labels":"desktop.docker.io/ports.scheme=v2,maintainer=NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e","LocalVolumes":"0","Mounts":"","Names":"mycontainer","Networks":"bridge","Platform":{"architecture":"arm64","os":"linux","variant":"v8"},"Ports":"80/tcp","RunningFor":"53 minutes ago","Size":"0B","State":"running","Status":"Up 53 minutes"} {"Command":"\"/usr/bin/gotty --ti…\"","CreatedAt":"2025-05-13 07:31:18 +0000 UTC","ID":"cbb981b06e46","Image":"thajeztah/dockershell:latest","Labels":"desktop.docker.io/ports.scheme=v2,com.thajeztah.docker-shell=1","LocalVolumes":"0","Mounts":"","Names":"trusting_goldstine","Networks":"bridge","Platform":{"architecture":"arm64","os":"linux"},"Ports":"0.0.0.0:55000-\u003e8080/tcp","RunningFor":"3 hours ago","Size":"0B","State":"running","Status":"Up 3 hours"} Signed-off-by: Sebastiaan van Stijn --- cli/command/formatter/container.go | 19 +++++++++++++++---- cli/command/formatter/container_test.go | 6 +++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 0e7f8e0f32..4480221fbf 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -16,6 +16,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/stringid" "github.com/docker/go-units" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) const ( @@ -30,6 +31,15 @@ const ( platformHeader = "PLATFORM" ) +// Platform wraps a [ocispec.Platform] to implement the stringer interface. +type Platform struct { + ocispec.Platform +} + +func (p Platform) String() string { + return platforms.FormatAll(p.Platform) +} + // NewContainerFormat returns a Format for rendering using a Context func NewContainerFormat(source string, quiet bool, size bool) Format { switch source { @@ -213,11 +223,12 @@ func (c *ContainerContext) RunningFor() string { // Platform returns a human-readable representation of the container's // platform if it is available. -func (c *ContainerContext) Platform() string { - if c.c.ImageManifestDescriptor != nil && c.c.ImageManifestDescriptor.Platform != nil { - return platforms.FormatAll(*c.c.ImageManifestDescriptor.Platform) +func (c *ContainerContext) Platform() *Platform { + p := c.c.ImageManifestDescriptor + if p == nil || p.Platform == nil { + return nil } - return "" + return &Platform{*p.Platform} } // Ports returns a comma-separated string representing open ports of the container diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 003ce87c2a..87928926a5 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -464,7 +464,7 @@ func TestContainerContextWriteJSON(t *testing.T) { "Mounts": "", "Names": "foobar_baz", "Networks": "", - "Platform": "", + "Platform": nil, "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", @@ -481,7 +481,7 @@ func TestContainerContextWriteJSON(t *testing.T) { "Mounts": "", "Names": "foobar_bar", "Networks": "", - "Platform": "linux/amd64", + "Platform": map[string]any{"architecture": "amd64", "os": "linux"}, "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", @@ -498,7 +498,7 @@ func TestContainerContextWriteJSON(t *testing.T) { "Mounts": "", "Names": "foobar_bar", "Networks": "", - "Platform": "unknown", + "Platform": map[string]any{"architecture": "", "os": ""}, "Ports": "", "RunningFor": "About a minute ago", "Size": "0B",