The tabwriter was configured to have a min-width for columns of 20 positions.
This seemed quite wide, and caused smaller columns to be printed with a large
gap between.
Before:
docker container stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
29184b3ae391 amazing_shirley 0.00% 800KiB / 1.944GiB 0.04% 1.44kB / 0B 0B / 0B 1
403c101bad56 agitated_swartz 0.15% 34.31MiB / 1.944GiB 1.72% 10.2MB / 206kB 0B / 0B 51
0dc4b7f6c6be container2 0.00% 1.012MiB / 1.944GiB 0.05% 12.9kB / 0B 0B / 0B 5
2d99abcc6f62 container99 0.00% 972KiB / 1.944GiB 0.05% 13kB / 0B 0B / 0B 5
9f9aa90173ac foo 0.00% 820KiB / 1.944GiB 0.04% 13kB / 0B 0B / 0B 5
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
29184b3ae391 docker-cli-dev "ash" 4 hours ago Up 4 hours amazing_shirley
403c101bad56 docker-dev:master "hack/dind bash" 3 days ago Up 3 days agitated_swartz
0dc4b7f6c6be nginx:alpine "/docker-entrypoint.…" 4 days ago Up 4 days 80/tcp container2
2d99abcc6f62 nginx:alpine "/docker-entrypoint.…" 4 days ago Up 4 days 80/tcp container99
9f9aa90173ac nginx:alpine "/docker-entrypoint.…" 4 days ago Up 4 days 80/tcp foo
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-cli-dev latest 5f603caa04aa 4 hours ago 610MB
docker-cli-native latest 9dd29f8d387b 4 hours ago 519MB
docker-dev master 8132bf7a199e 3 days ago 2.02GB
docker-dev improve-build-errors 69e208994b3f 11 days ago 2.01GB
docker-dev refactor-idtools 69e208994b3f 11 days ago 2.01GB
After:
docker container stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
29184b3ae391 amazing_shirley 0.14% 5.703MiB / 1.944GiB 0.29% 1.44kB / 0B 0B / 0B 10
403c101bad56 agitated_swartz 0.15% 56.97MiB / 1.944GiB 2.86% 10.2MB / 206kB 0B / 0B 51
0dc4b7f6c6be container2 0.00% 1016KiB / 1.944GiB 0.05% 12.9kB / 0B 0B / 0B 5
2d99abcc6f62 container99 0.00% 956KiB / 1.944GiB 0.05% 13kB / 0B 0B / 0B 5
9f9aa90173ac foo 0.00% 980KiB / 1.944GiB 0.05% 13kB / 0B 0B / 0B 5
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
29184b3ae391 docker-cli-dev "ash" 12 minutes ago Up 12 minutes amazing_shirley
403c101bad56 docker-dev:master "hack/dind bash" 3 days ago Up 3 days agitated_swartz
0dc4b7f6c6be nginx:alpine "/docker-entrypoint.…" 4 days ago Up 4 days 80/tcp container2
2d99abcc6f62 nginx:alpine "/docker-entrypoint.…" 4 days ago Up 4 days 80/tcp container99
9f9aa90173ac nginx:alpine "/docker-entrypoint.…" 4 days ago Up 4 days 80/tcp foo
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-cli-dev latest 5f603caa04aa 4 hours ago 610MB
docker-cli-native latest 9dd29f8d387b 4 hours ago 519MB
docker-dev master 8132bf7a199e 3 days ago 2.02GB
docker-dev improve-build-errors 69e208994b3f 11 days ago 2.01GB
docker-dev refactor-idtools 69e208994b3f 11 days ago 2.01GB
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
356 lines
11 KiB
Go
356 lines
11 KiB
Go
package node
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/docker/cli/cli/command/formatter"
|
|
"github.com/docker/cli/internal/test"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
func TestNodeContext(t *testing.T) {
|
|
nodeID := stringid.GenerateRandomID()
|
|
|
|
var ctx nodeContext
|
|
cases := []struct {
|
|
nodeCtx nodeContext
|
|
expValue string
|
|
call func() string
|
|
}{
|
|
{nodeContext{
|
|
n: swarm.Node{ID: nodeID},
|
|
}, nodeID, ctx.ID},
|
|
{nodeContext{
|
|
n: swarm.Node{Description: swarm.NodeDescription{Hostname: "node_hostname"}},
|
|
}, "node_hostname", ctx.Hostname},
|
|
{nodeContext{
|
|
n: swarm.Node{Status: swarm.NodeStatus{State: swarm.NodeState("foo")}},
|
|
}, "Foo", ctx.Status},
|
|
{nodeContext{
|
|
n: swarm.Node{Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}},
|
|
}, "Drain", ctx.Availability},
|
|
{nodeContext{
|
|
n: swarm.Node{ManagerStatus: &swarm.ManagerStatus{Leader: true}},
|
|
}, "Leader", ctx.ManagerStatus},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
ctx = c.nodeCtx
|
|
v := c.call()
|
|
if strings.Contains(v, ",") {
|
|
test.CompareMultipleValues(t, v, c.expValue)
|
|
} else if v != c.expValue {
|
|
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNodeContextWrite(t *testing.T) {
|
|
cases := []struct {
|
|
context formatter.Context
|
|
expected string
|
|
clusterInfo swarm.ClusterInfo
|
|
}{
|
|
|
|
// Errors
|
|
{
|
|
context: formatter.Context{Format: "{{InvalidFunction}}"},
|
|
expected: `Template parsing error: template: :1: function "InvalidFunction" not defined
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
{
|
|
context: formatter.Context{Format: "{{nil}}"},
|
|
expected: `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
// Table format
|
|
{
|
|
context: formatter.Context{Format: NewFormat("table", false)},
|
|
expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
|
|
nodeID1 foobar_baz Foo Drain Leader 18.03.0-ce
|
|
nodeID2 foobar_bar Bar Active Reachable 1.2.3
|
|
nodeID3 foobar_boo Boo Active ` + "\n", // (to preserve whitespace)
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
{
|
|
context: formatter.Context{Format: NewFormat("table", true)},
|
|
expected: `nodeID1
|
|
nodeID2
|
|
nodeID3
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
{
|
|
context: formatter.Context{Format: NewFormat("table {{.Hostname}}", false)},
|
|
expected: `HOSTNAME
|
|
foobar_baz
|
|
foobar_bar
|
|
foobar_boo
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
{
|
|
context: formatter.Context{Format: NewFormat("table {{.Hostname}}", true)},
|
|
expected: `HOSTNAME
|
|
foobar_baz
|
|
foobar_bar
|
|
foobar_boo
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
{
|
|
context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)},
|
|
expected: `ID HOSTNAME TLS STATUS
|
|
nodeID1 foobar_baz Needs Rotation
|
|
nodeID2 foobar_bar Ready
|
|
nodeID3 foobar_boo Unknown
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
{ // no cluster TLS status info, TLS status for all nodes is unknown
|
|
context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)},
|
|
expected: `ID HOSTNAME TLS STATUS
|
|
nodeID1 foobar_baz Unknown
|
|
nodeID2 foobar_bar Unknown
|
|
nodeID3 foobar_boo Unknown
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{},
|
|
},
|
|
// Raw Format
|
|
{
|
|
context: formatter.Context{Format: NewFormat("raw", false)},
|
|
expected: `node_id: nodeID1
|
|
hostname: foobar_baz
|
|
status: Foo
|
|
availability: Drain
|
|
manager_status: Leader
|
|
|
|
node_id: nodeID2
|
|
hostname: foobar_bar
|
|
status: Bar
|
|
availability: Active
|
|
manager_status: Reachable
|
|
|
|
node_id: nodeID3
|
|
hostname: foobar_boo
|
|
status: Boo
|
|
availability: Active
|
|
manager_status: ` + "\n\n", // to preserve whitespace
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
{
|
|
context: formatter.Context{Format: NewFormat("raw", true)},
|
|
expected: `node_id: nodeID1
|
|
node_id: nodeID2
|
|
node_id: nodeID3
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
// Custom Format
|
|
{
|
|
context: formatter.Context{Format: NewFormat("{{.Hostname}} {{.TLSStatus}}", false)},
|
|
expected: `foobar_baz Needs Rotation
|
|
foobar_bar Ready
|
|
foobar_boo Unknown
|
|
`,
|
|
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
|
|
},
|
|
}
|
|
|
|
nodes := []swarm.Node{
|
|
{
|
|
ID: "nodeID1",
|
|
Description: swarm.NodeDescription{
|
|
Hostname: "foobar_baz",
|
|
TLSInfo: swarm.TLSInfo{TrustRoot: "no"},
|
|
Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"},
|
|
},
|
|
Status: swarm.NodeStatus{State: swarm.NodeState("foo")},
|
|
Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")},
|
|
ManagerStatus: &swarm.ManagerStatus{Leader: true},
|
|
},
|
|
{
|
|
ID: "nodeID2",
|
|
Description: swarm.NodeDescription{
|
|
Hostname: "foobar_bar",
|
|
TLSInfo: swarm.TLSInfo{TrustRoot: "hi"},
|
|
Engine: swarm.EngineDescription{EngineVersion: "1.2.3"},
|
|
},
|
|
Status: swarm.NodeStatus{State: swarm.NodeState("bar")},
|
|
Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")},
|
|
ManagerStatus: &swarm.ManagerStatus{
|
|
Leader: false,
|
|
Reachability: swarm.Reachability("Reachable"),
|
|
},
|
|
},
|
|
{
|
|
ID: "nodeID3",
|
|
Description: swarm.NodeDescription{Hostname: "foobar_boo"},
|
|
Status: swarm.NodeStatus{State: swarm.NodeState("boo")},
|
|
Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
|
var out bytes.Buffer
|
|
tc.context.Output = &out
|
|
|
|
err := FormatWrite(tc.context, nodes, types.Info{Swarm: swarm.Info{Cluster: &tc.clusterInfo}})
|
|
if err != nil {
|
|
assert.Error(t, err, tc.expected)
|
|
} else {
|
|
assert.Equal(t, out.String(), tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNodeContextWriteJSON(t *testing.T) {
|
|
cases := []struct {
|
|
expected []map[string]interface{}
|
|
info types.Info
|
|
}{
|
|
{
|
|
expected: []map[string]interface{}{
|
|
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "1.2.3"},
|
|
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": ""},
|
|
{"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"},
|
|
},
|
|
info: types.Info{},
|
|
},
|
|
{
|
|
expected: []map[string]interface{}{
|
|
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Ready", "EngineVersion": "1.2.3"},
|
|
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Needs Rotation", "EngineVersion": ""},
|
|
{"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"},
|
|
},
|
|
info: types.Info{
|
|
Swarm: swarm.Info{
|
|
Cluster: &swarm.ClusterInfo{
|
|
TLSInfo: swarm.TLSInfo{TrustRoot: "hi"},
|
|
RootRotationInProgress: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testcase := range cases {
|
|
nodes := []swarm.Node{
|
|
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}}},
|
|
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar", TLSInfo: swarm.TLSInfo{TrustRoot: "no"}}},
|
|
{ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}},
|
|
}
|
|
out := bytes.NewBufferString("")
|
|
err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, nodes, testcase.info)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
|
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
|
var m map[string]interface{}
|
|
err := json.Unmarshal([]byte(line), &m)
|
|
assert.NilError(t, err, msg)
|
|
assert.Check(t, is.DeepEqual(testcase.expected[i], m), msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNodeContextWriteJSONField(t *testing.T) {
|
|
nodes := []swarm.Node{
|
|
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}},
|
|
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}},
|
|
}
|
|
out := bytes.NewBufferString("")
|
|
err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, nodes, types.Info{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
|
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
|
var s string
|
|
err := json.Unmarshal([]byte(line), &s)
|
|
assert.NilError(t, err, msg)
|
|
assert.Check(t, is.Equal(nodes[i].ID, s), msg)
|
|
}
|
|
}
|
|
|
|
func TestNodeInspectWriteContext(t *testing.T) {
|
|
node := swarm.Node{
|
|
ID: "nodeID1",
|
|
Description: swarm.NodeDescription{
|
|
Hostname: "foobar_baz",
|
|
TLSInfo: swarm.TLSInfo{
|
|
TrustRoot: "-----BEGIN CERTIFICATE-----\ndata\n-----END CERTIFICATE-----\n",
|
|
CertIssuerPublicKey: []byte("pubKey"),
|
|
CertIssuerSubject: []byte("subject"),
|
|
},
|
|
Platform: swarm.Platform{
|
|
OS: "linux",
|
|
Architecture: "amd64",
|
|
},
|
|
Resources: swarm.Resources{
|
|
MemoryBytes: 1,
|
|
},
|
|
Engine: swarm.EngineDescription{
|
|
EngineVersion: "0.1.1",
|
|
},
|
|
},
|
|
Status: swarm.NodeStatus{
|
|
State: swarm.NodeState("ready"),
|
|
Addr: "1.1.1.1",
|
|
},
|
|
Spec: swarm.NodeSpec{
|
|
Availability: swarm.NodeAvailability("drain"),
|
|
Role: swarm.NodeRole("manager"),
|
|
},
|
|
}
|
|
out := bytes.NewBufferString("")
|
|
context := formatter.Context{
|
|
Format: NewFormat("pretty", false),
|
|
Output: out,
|
|
}
|
|
err := InspectFormatWrite(context, []string{"nodeID1"}, func(string) (interface{}, []byte, error) {
|
|
return node, nil, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected := `ID: nodeID1
|
|
Hostname: foobar_baz
|
|
Joined at: 0001-01-01 00:00:00 +0000 utc
|
|
Status:
|
|
State: Ready
|
|
Availability: Drain
|
|
Address: 1.1.1.1
|
|
Platform:
|
|
Operating System: linux
|
|
Architecture: amd64
|
|
Resources:
|
|
CPUs: 0
|
|
Memory: 1B
|
|
Engine Version: 0.1.1
|
|
TLS Info:
|
|
TrustRoot:
|
|
-----BEGIN CERTIFICATE-----
|
|
data
|
|
-----END CERTIFICATE-----
|
|
|
|
Issuer Subject: c3ViamVjdA==
|
|
Issuer Public Key: cHViS2V5
|
|
`
|
|
assert.Check(t, is.Equal(expected, out.String()))
|
|
}
|