diff --git a/cli/command/formatter/node.go b/cli/command/formatter/node.go index 09bcd3f9f7..6cfc32380e 100644 --- a/cli/command/formatter/node.go +++ b/cli/command/formatter/node.go @@ -14,7 +14,7 @@ import ( ) const ( - defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}" + defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}\t{{.EngineVersion}}" nodeInspectPrettyTemplate Format = `ID: {{.ID}} {{- if .Name }} Name: {{.Name}} @@ -75,6 +75,7 @@ TLS Info: hostnameHeader = "HOSTNAME" availabilityHeader = "AVAILABILITY" managerStatusHeader = "MANAGER STATUS" + engineVersionHeader = "ENGINE VERSION" tlsStatusHeader = "TLS STATUS" ) @@ -115,6 +116,7 @@ func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error { "Status": statusHeader, "Availability": availabilityHeader, "ManagerStatus": managerStatusHeader, + "EngineVersion": engineVersionHeader, "TLSStatus": tlsStatusHeader, } nodeCtx := nodeContext{} @@ -176,6 +178,10 @@ func (c *nodeContext) TLSStatus() string { return "Needs Rotation" } +func (c *nodeContext) EngineVersion() string { + return c.n.Description.Engine.EngineVersion +} + // NodeInspectWrite renders the context for a list of nodes func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { if ctx.Format != nodeInspectPrettyTemplate { diff --git a/cli/command/formatter/node_test.go b/cli/command/formatter/node_test.go index 42fdf5138d..768f89f65b 100644 --- a/cli/command/formatter/node_test.go +++ b/cli/command/formatter/node_test.go @@ -74,10 +74,10 @@ func TestNodeContextWrite(t *testing.T) { // Table format { context: Context{Format: NewNodeFormat("table", false)}, - expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS -nodeID1 foobar_baz Foo Drain Leader -nodeID2 foobar_bar Bar Active Reachable -nodeID3 foobar_boo Boo Active ` + "\n", // (to preserve whitespace) + 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"}}, }, { @@ -172,6 +172,7 @@ foobar_boo Unknown 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")}, @@ -182,6 +183,7 @@ foobar_boo Unknown 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")}, @@ -215,17 +217,17 @@ func TestNodeContextWriteJSON(t *testing.T) { }{ { expected: []map[string]interface{}{ - {"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown"}, - {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown"}, - {"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown"}, + {"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"}, - {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Needs Rotation"}, - {"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown"}, + {"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{ @@ -240,9 +242,9 @@ func TestNodeContextWriteJSON(t *testing.T) { for _, testcase := range cases { nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}}, + {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"}}, + {ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}}, } out := bytes.NewBufferString("") err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, testcase.info) diff --git a/cli/command/node/list_test.go b/cli/command/node/list_test.go index b3a42df460..2cbe96e766 100644 --- a/cli/command/node/list_test.go +++ b/cli/command/node/list_test.go @@ -56,8 +56,8 @@ func TestNodeList(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ nodeListFunc: func() ([]swarm.Node, error) { return []swarm.Node{ - *Node(NodeID("nodeID1"), Hostname("node-2-foo"), Manager(Leader())), - *Node(NodeID("nodeID2"), Hostname("node-10-foo"), Manager()), + *Node(NodeID("nodeID1"), Hostname("node-2-foo"), Manager(Leader()), EngineVersion(".")), + *Node(NodeID("nodeID2"), Hostname("node-10-foo"), Manager(), EngineVersion("18.03.0-ce")), *Node(NodeID("nodeID3"), Hostname("node-1-foo")), }, nil }, diff --git a/cli/command/node/testdata/node-list-sort.golden b/cli/command/node/testdata/node-list-sort.golden index ad8c34a813..ffc09c9259 100644 --- a/cli/command/node/testdata/node-list-sort.golden +++ b/cli/command/node/testdata/node-list-sort.golden @@ -1,4 +1,4 @@ -ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS -nodeID3 node-1-foo Ready Active -nodeID1 * node-2-foo Ready Active Leader -nodeID2 node-10-foo Ready Active Reachable +ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION +nodeID3 node-1-foo Ready Active 1.13.0 +nodeID1 * node-2-foo Ready Active Leader . +nodeID2 node-10-foo Ready Active Reachable 18.03.0-ce diff --git a/docs/reference/commandline/node_ls.md b/docs/reference/commandline/node_ls.md index d4368df4a0..5033544921 100644 --- a/docs/reference/commandline/node_ls.md +++ b/docs/reference/commandline/node_ls.md @@ -146,6 +146,7 @@ Placeholder | Description `.Availability` | Node availability ("active", "pause", or "drain") `.ManagerStatus` | Manager status of the node `.TLSStatus` | TLS status of the node ("Ready", or "Needs Rotation" has TLS certificate signed by an old CA) +`.EngineVersion` | Engine version When using the `--format` option, the `node ls` command will either output the data exactly as the template declares or, when using the diff --git a/internal/test/builders/node.go b/internal/test/builders/node.go index 234d6de242..4e3eb06dd1 100644 --- a/internal/test/builders/node.go +++ b/internal/test/builders/node.go @@ -125,3 +125,10 @@ func ManagerStatus(managerStatusBuilders ...func(*swarm.ManagerStatus)) *swarm.M return managerStatus } + +// EngineVersion sets the node's engine version +func EngineVersion(version string) func(*swarm.Node) { + return func(node *swarm.Node) { + node.Description.Engine.EngineVersion = version + } +}