From 0cfea7d40f6cab27098c770030f01675ecc50380 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 27 Dec 2019 11:22:10 +0100 Subject: [PATCH 1/2] Add .IPAddresses as formatting option on docker ps This allows showing the IP address for each network that the container is attached to, for example: docker network create foo docker run -d --name foo nginx:alpine docker network connect foo foo container container ls --format 'table {{.ID}}\\t{{join .IPAddresses ", "}}' CONTAINER ID IP ADDRESSES 17e7d1910fc0 bridge:172.17.0.2, foo:172.19.0.2 container container ls --format='{{json .IPAddresses}}' | jq . [ "bridge:172.17.0.2", "foo:172.19.0.2" ] Signed-off-by: Sebastiaan van Stijn --- cli/command/formatter/container.go | 35 +++++++++++++---- cli/command/formatter/container_test.go | 33 +++++++++++++++- docs/reference/commandline/container_ls.md | 44 ++++++++++++++-------- 3 files changed, 88 insertions(+), 24 deletions(-) diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 979d3eb82d60..c844c5d1a3aa 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -21,13 +21,14 @@ import ( const ( defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" - namesHeader = "NAMES" - commandHeader = "COMMAND" - runningForHeader = "CREATED" - mountsHeader = "MOUNTS" - localVolumes = "LOCAL VOLUMES" - networksHeader = "NETWORKS" - platformHeader = "PLATFORM" + namesHeader = "NAMES" + commandHeader = "COMMAND" + runningForHeader = "CREATED" + mountsHeader = "MOUNTS" + localVolumes = "LOCAL VOLUMES" + networksHeader = "NETWORKS" + platformHeader = "PLATFORM" + ipAddressesHeader = "IP ADDRESSES" ) // Platform wraps a [ocispec.Platform] to implement the stringer interface. @@ -121,6 +122,7 @@ func NewContainerContext() *ContainerContext { "LocalVolumes": localVolumes, "Networks": networksHeader, "Platform": platformHeader, + "IPAddresses": ipAddressesHeader, } return &containerCtx } @@ -341,6 +343,25 @@ func (c *ContainerContext) Networks() string { return strings.Join(networks, ",") } +// IPAddresses returns the list of IP-addresses assigned to the container +// IP-addresses are prefixed with the name of the network, separated with a colon. +// For example: "bridge:192.168.1.10" +func (c *ContainerContext) IPAddresses() []string { + ipAddresses := []string{} + if c.c.NetworkSettings == nil { + return ipAddresses + } + for name, nw := range c.c.NetworkSettings.Networks { + if nw.IPAddress != "" { + ipAddresses = append(ipAddresses, name+":"+nw.IPAddress) + } + if nw.GlobalIPv6Address != "" { + ipAddresses = append(ipAddresses, name+":"+nw.GlobalIPv6Address) + } + } + return ipAddresses +} + // DisplayablePorts returns formatted string representing open ports of 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 a38e75e87d53..ae2b9dbeb420 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/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -390,7 +391,7 @@ size: 0B } containers := []container.Summary{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime, State: container.StateRunning}, + {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime, State: container.StateRunning, NetworkSettings: &container.NetworkSettingsSummary{}}, {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime, State: container.StateRunning}, } @@ -576,6 +577,36 @@ func TestContainerContextWriteJSONField(t *testing.T) { } } +func TestContainerContextIPAddresses(t *testing.T) { + containers := []container.Summary{ + { + ID: "containerID1", + NetworkSettings: &container.NetworkSettingsSummary{ + Networks: map[string]*network.EndpointSettings{ + "one": {IPAddress: "192.168.1.2"}, + "two": {IPAddress: "192.168.178.2"}, + }, + }, + }, + { + ID: "containerID2", + NetworkSettings: &container.NetworkSettingsSummary{ + Networks: map[string]*network.EndpointSettings{ + "one": {IPAddress: "192.168.1.3"}, + "two": {IPAddress: "192.168.178.3"}, + }, + }, + }, + } + + out := bytes.NewBufferString("") + err := ContainerWrite(Context{Format: "{{.IPAddresses}}", Output: out}, containers) + assert.NilError(t, err) + assert.Equal(t, out.String(), `[one:192.168.1.2 two:192.168.178.2] +[one:192.168.1.3 two:192.168.178.3] +`) +} + func TestContainerBackCompat(t *testing.T) { createdAtTime := time.Now().AddDate(-1, 0, 0) // 1 year ago diff --git a/docs/reference/commandline/container_ls.md b/docs/reference/commandline/container_ls.md index a19f7a5e0410..8bb74177fd47 100644 --- a/docs/reference/commandline/container_ls.md +++ b/docs/reference/commandline/container_ls.md @@ -395,22 +395,24 @@ template. Valid placeholders for the Go template are listed below: -| Placeholder | Description | -|:--------------|:------------------------------------------------------------------------------------------------| -| `.ID` | Container ID | -| `.Image` | Image ID | -| `.Command` | Quoted command | -| `.CreatedAt` | Time when the container was created. | -| `.RunningFor` | Elapsed time since the container was started. | -| `.Ports` | Exposed ports. | -| `.State` | Container status (for example; "created", "running", "exited"). | -| `.Status` | Container status with details about duration and health-status. | -| `.Size` | Container disk size. | -| `.Names` | Container names. | -| `.Labels` | All labels assigned to the container. | -| `.Label` | Value of a specific label for this container. For example `'{{.Label "com.docker.swarm.cpu"}}'` | -| `.Mounts` | Names of the volumes mounted in this container. | -| `.Networks` | Names of the networks attached to this container. | +| Placeholder | Description | +|:---------------|:------------------------------------------------------------------------------------------------| +| `.ID` | Container ID | +| `.Image` | Image ID | +| `.Command` | Quoted command | +| `.CreatedAt` | Time when the container was created. | +| `.RunningFor` | Elapsed time since the container was started. | +| `.Ports` | Exposed ports. | +| `.State` | Container status (for example; "created", "running", "exited"). | +| `.Status` | Container status with details about duration and health-status. | +| `.Size` | Container disk size. | +| `.Names` | Container names. | +| `.Labels` | All labels assigned to the container. | +| `.Label` | Value of a specific label for this container. For example `'{{.Label "com.docker.swarm.cpu"}}'` | +| `.Mounts` | Names of the volumes mounted in this container. | +| `.Networks` | Names of the networks attached to this container. | +| `.IPAddresses` | List of IP-Addresses for each network that the container is attached to. | + When using the `--format` option, the `ps` command will either output the data exactly as the template declares or, when using the `table` directive, includes @@ -446,3 +448,13 @@ To list all running containers in JSON format, use the `json` directive: $ docker ps --format json {"Command":"\"/docker-entrypoint.…\"","CreatedAt":"2021-03-10 00:15:05 +0100 CET","ID":"a762a2b37a1d","Image":"nginx","Labels":"maintainer=NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e","LocalVolumes":"0","Mounts":"","Names":"boring_keldysh","Networks":"bridge","Ports":"80/tcp","RunningFor":"4 seconds ago","Size":"0B","State":"running","Status":"Up 3 seconds"} ``` + +Show the IP-addresses that containers have: + +```console +$ docker ps --format "table {{.ID}}\\t{{join .IPAddresses \", \"}}" + +CONTAINER ID IP ADDRESSES +c0cf2877da71 bridge:172.17.0.3 +17e7d1910fc0 bridge:172.17.0.2, mynetwork:172.19.0.2 +``` From 2e0748f3768925401d97a222f8bb1090ded0b394 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 22 Sep 2020 16:03:21 +0200 Subject: [PATCH 2/2] WIP use structured type Still failing when joining; docker container ls --format 'table {{.ID}}\t{{join .IPAddresses ", "}}' CONTAINER ID IP ADDRESSES 245bd1d81375 bridge/172.17.0.2, foo/172.19.0.2 docker container ls --format '{{json .IPAddresses}}' [{"Network":"bridge","IP":"172.17.0.2"},{"Network":"foo","IP":"172.19.0.2"}] Signed-off-by: Sebastiaan van Stijn --- cli/command/formatter/container.go | 35 ++++++++++++++++++---- cli/command/formatter/container_test.go | 33 +++++++++++++++----- docs/reference/commandline/container_ls.md | 4 +-- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index c844c5d1a3aa..bc537566b75a 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -4,8 +4,10 @@ package formatter import ( + "cmp" "fmt" "net" + "slices" "sort" "strconv" "strings" @@ -40,6 +42,16 @@ func (p Platform) String() string { return platforms.FormatAll(p.Platform) } +// NetworkIP describes an IP-address and the network it's associated with. +type NetworkIP struct { + Network string `json:"Network,omitempty"` + IP string `json:"IP"` +} + +func (p NetworkIP) String() string { + return p.Network + "/" + p.IP +} + // NewContainerFormat returns a Format for rendering using a Context func NewContainerFormat(source string, quiet bool, size bool) Format { switch source { @@ -346,19 +358,30 @@ func (c *ContainerContext) Networks() string { // IPAddresses returns the list of IP-addresses assigned to the container // IP-addresses are prefixed with the name of the network, separated with a colon. // For example: "bridge:192.168.1.10" -func (c *ContainerContext) IPAddresses() []string { - ipAddresses := []string{} - if c.c.NetworkSettings == nil { - return ipAddresses +func (c *ContainerContext) IPAddresses() []NetworkIP { + if c.c.NetworkSettings == nil || len(c.c.NetworkSettings.Networks) == 0 { + return []NetworkIP{} } + ipAddresses := make([]NetworkIP, 0, len(c.c.NetworkSettings.Networks)) for name, nw := range c.c.NetworkSettings.Networks { if nw.IPAddress != "" { - ipAddresses = append(ipAddresses, name+":"+nw.IPAddress) + ipAddresses = append(ipAddresses, NetworkIP{ + Network: name, + IP: nw.IPAddress, + }) } if nw.GlobalIPv6Address != "" { - ipAddresses = append(ipAddresses, name+":"+nw.GlobalIPv6Address) + ipAddresses = append(ipAddresses, NetworkIP{ + Network: name, + IP: nw.GlobalIPv6Address, + }) } } + + slices.SortFunc(ipAddresses, func(a, b NetworkIP) int { + return cmp.Compare(a.String(), b.String()) + }) + return ipAddresses } diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index ae2b9dbeb420..1bc1a8e90101 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -477,7 +477,18 @@ func TestContainerContextWriteJSON(t *testing.T) { Image: "ubuntu", Created: unix, State: container.StateRunning, - + NetworkSettings: &container.NetworkSettingsSummary{ + Networks: map[string]*network.EndpointSettings{ + "bridge": { + IPAddress: "172.17.0.1", + GlobalIPv6Address: "ff02::1", + }, + "my-net": { + IPAddress: "172.18.0.1", + GlobalIPv6Address: "ff02::2", + }, + }, + }, ImageManifestDescriptor: &ocispec.Descriptor{Platform: &ocispec.Platform{Architecture: "amd64", OS: "linux"}}, }, { @@ -496,6 +507,7 @@ func TestContainerContextWriteJSON(t *testing.T) { "Command": `""`, "CreatedAt": expectedCreated, "ID": "containerID1", + "IPAddresses": []any{}, "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", @@ -510,15 +522,21 @@ func TestContainerContextWriteJSON(t *testing.T) { "Status": "", }, { - "Command": `""`, - "CreatedAt": expectedCreated, - "ID": "containerID2", + "Command": `""`, + "CreatedAt": expectedCreated, + "ID": "containerID2", + "IPAddresses": []any{ + map[string]any{"IP": "172.17.0.1", "Network": "bridge"}, + map[string]any{"IP": "ff02::1", "Network": "bridge"}, + map[string]any{"IP": "172.18.0.1", "Network": "my-net"}, + map[string]any{"IP": "ff02::2", "Network": "my-net"}, + }, "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", - "Networks": "", + "Networks": "bridge,my-net", "Platform": map[string]any{"architecture": "amd64", "os": "linux"}, "Ports": "", "RunningFor": "About a minute ago", @@ -530,6 +548,7 @@ func TestContainerContextWriteJSON(t *testing.T) { "Command": `""`, "CreatedAt": expectedCreated, "ID": "containerID3", + "IPAddresses": []any{}, "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", @@ -602,8 +621,8 @@ func TestContainerContextIPAddresses(t *testing.T) { out := bytes.NewBufferString("") err := ContainerWrite(Context{Format: "{{.IPAddresses}}", Output: out}, containers) assert.NilError(t, err) - assert.Equal(t, out.String(), `[one:192.168.1.2 two:192.168.178.2] -[one:192.168.1.3 two:192.168.178.3] + assert.Equal(t, out.String(), `[one/192.168.1.2 two/192.168.178.2] +[one/192.168.1.3 two/192.168.178.3] `) } diff --git a/docs/reference/commandline/container_ls.md b/docs/reference/commandline/container_ls.md index 8bb74177fd47..4632d0a01c52 100644 --- a/docs/reference/commandline/container_ls.md +++ b/docs/reference/commandline/container_ls.md @@ -455,6 +455,6 @@ Show the IP-addresses that containers have: $ docker ps --format "table {{.ID}}\\t{{join .IPAddresses \", \"}}" CONTAINER ID IP ADDRESSES -c0cf2877da71 bridge:172.17.0.3 -17e7d1910fc0 bridge:172.17.0.2, mynetwork:172.19.0.2 +c0cf2877da71 bridge/172.17.0.3 +17e7d1910fc0 bridge/172.17.0.2, mynetwork/172.19.0.2 ```