Propagate the swarm cluster and node TLS info provided by the REST API

responses to the CLI. In `node ls`, display only whether the nodes' TLS
info matches the cluster's TLS info, or whether the node needs cert rotation.

Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
cyli 2017-05-08 10:48:24 -07:00 committed by Ying Li
parent 3574e6a674
commit b75858eb09
3 changed files with 233 additions and 49 deletions

View File

@ -1,7 +1,9 @@
package formatter package formatter
import ( import (
"encoding/base64"
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
@ -61,12 +63,20 @@ Engine Labels:
{{- range $k, $v := .EngineLabels }} {{- range $k, $v := .EngineLabels }}
- {{ $k }}{{if $v }}={{ $v }}{{ end }} - {{ $k }}{{if $v }}={{ $v }}{{ end }}
{{- end }}{{- end }} {{- end }}{{- end }}
{{- if .HasTLSInfo}}
TLS Info:
TrustRoot:
{{.TLSInfoTrustRoot}}
Issuer Subject: {{.TLSInfoCertIssuerSubject}}
Issuer Public Key: {{.TLSInfoCertIssuerPublicKey}}
{{- end}}
` `
nodeIDHeader = "ID" nodeIDHeader = "ID"
selfHeader = "" selfHeader = ""
hostnameHeader = "HOSTNAME" hostnameHeader = "HOSTNAME"
availabilityHeader = "AVAILABILITY" availabilityHeader = "AVAILABILITY"
managerStatusHeader = "MANAGER STATUS" managerStatusHeader = "MANAGER STATUS"
tlsStatusHeader = "TLS STATUS"
) )
// NewNodeFormat returns a Format for rendering using a node Context // NewNodeFormat returns a Format for rendering using a node Context
@ -99,15 +109,17 @@ func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error {
} }
return nil return nil
} }
nodeCtx := nodeContext{} header := nodeHeaderContext{
nodeCtx.header = nodeHeaderContext{
"ID": nodeIDHeader, "ID": nodeIDHeader,
"Self": selfHeader, "Self": selfHeader,
"Hostname": hostnameHeader, "Hostname": hostnameHeader,
"Status": statusHeader, "Status": statusHeader,
"Availability": availabilityHeader, "Availability": availabilityHeader,
"ManagerStatus": managerStatusHeader, "ManagerStatus": managerStatusHeader,
"TLSStatus": tlsStatusHeader,
} }
nodeCtx := nodeContext{}
nodeCtx.header = header
return ctx.Write(&nodeCtx, render) return ctx.Write(&nodeCtx, render)
} }
@ -155,6 +167,16 @@ func (c *nodeContext) ManagerStatus() string {
return command.PrettyPrint(reachability) return command.PrettyPrint(reachability)
} }
func (c *nodeContext) TLSStatus() string {
if c.info.Swarm.Cluster == nil || reflect.DeepEqual(c.info.Swarm.Cluster.TLSInfo, swarm.TLSInfo{}) || reflect.DeepEqual(c.n.Description.TLSInfo, swarm.TLSInfo{}) {
return "Unknown"
}
if reflect.DeepEqual(c.n.Description.TLSInfo, c.info.Swarm.Cluster.TLSInfo) {
return "Ready"
}
return "Needs Rotation"
}
// NodeInspectWrite renders the context for a list of services // NodeInspectWrite renders the context for a list of services
func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error {
if ctx.Format != nodeInspectPrettyTemplate { if ctx.Format != nodeInspectPrettyTemplate {
@ -290,3 +312,20 @@ func (ctx *nodeInspectContext) EngineLabels() map[string]string {
func (ctx *nodeInspectContext) EngineVersion() string { func (ctx *nodeInspectContext) EngineVersion() string {
return ctx.Node.Description.Engine.EngineVersion return ctx.Node.Description.Engine.EngineVersion
} }
func (ctx *nodeInspectContext) HasTLSInfo() bool {
tlsInfo := ctx.Node.Description.TLSInfo
return !reflect.DeepEqual(tlsInfo, swarm.TLSInfo{})
}
func (ctx *nodeInspectContext) TLSInfoTrustRoot() string {
return ctx.Node.Description.TLSInfo.TrustRoot
}
func (ctx *nodeInspectContext) TLSInfoCertIssuerPublicKey() string {
return base64.StdEncoding.EncodeToString(ctx.Node.Description.TLSInfo.CertIssuerPublicKey)
}
func (ctx *nodeInspectContext) TLSInfoCertIssuerSubject() string {
return base64.StdEncoding.EncodeToString(ctx.Node.Description.TLSInfo.CertIssuerSubject)
}

View File

@ -51,53 +51,81 @@ func TestNodeContext(t *testing.T) {
func TestNodeContextWrite(t *testing.T) { func TestNodeContextWrite(t *testing.T) {
cases := []struct { cases := []struct {
context Context context Context
expected string expected string
clusterInfo swarm.ClusterInfo
}{ }{
// Errors // Errors
{ {
Context{Format: "{{InvalidFunction}}"}, context: Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined expected: `Template parsing error: template: :1: function "InvalidFunction" not defined
`, `,
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
Context{Format: "{{nil}}"}, context: Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command 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 // Table format
{ {
Context{Format: NewNodeFormat("table", false)}, context: Context{Format: NewNodeFormat("table", false)},
`ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
nodeID1 foobar_baz Foo Drain Leader nodeID1 foobar_baz Foo Drain Leader
nodeID2 foobar_bar Bar Active Reachable nodeID2 foobar_bar Bar Active Reachable
`, nodeID3 foobar_boo Boo Active ` + "\n", // (to preserve whitespace)
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
Context{Format: NewNodeFormat("table", true)}, context: Context{Format: NewNodeFormat("table", true)},
`nodeID1 expected: `nodeID1
nodeID2 nodeID2
nodeID3
`, `,
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
Context{Format: NewNodeFormat("table {{.Hostname}}", false)}, context: Context{Format: NewNodeFormat("table {{.Hostname}}", false)},
`HOSTNAME expected: `HOSTNAME
foobar_baz foobar_baz
foobar_bar foobar_bar
foobar_boo
`, `,
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
{ {
Context{Format: NewNodeFormat("table {{.Hostname}}", true)}, context: Context{Format: NewNodeFormat("table {{.Hostname}}", true)},
`HOSTNAME expected: `HOSTNAME
foobar_baz foobar_baz
foobar_bar foobar_bar
foobar_boo
`, `,
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
},
{
context: Context{Format: NewNodeFormat("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: Context{Format: NewNodeFormat("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 // Raw Format
{ {
Context{Format: NewNodeFormat("raw", false)}, context: Context{Format: NewNodeFormat("raw", false)},
`node_id: nodeID1 expected: `node_id: nodeID1
hostname: foobar_baz hostname: foobar_baz
status: Foo status: Foo
availability: Drain availability: Drain
@ -109,46 +137,67 @@ status: Bar
availability: Active availability: Active
manager_status: Reachable 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{Format: NewNodeFormat("raw", true)}, context: Context{Format: NewNodeFormat("raw", true)},
`node_id: nodeID1 expected: `node_id: nodeID1
node_id: nodeID2 node_id: nodeID2
node_id: nodeID3
`, `,
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
// Custom Format // Custom Format
{ {
Context{Format: NewNodeFormat("{{.Hostname}}", false)}, context: Context{Format: NewNodeFormat("{{.Hostname}} {{.TLSStatus}}", false)},
`foobar_baz expected: `foobar_baz Needs Rotation
foobar_bar foobar_bar Ready
foobar_boo Unknown
`, `,
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
}, },
} }
for _, testcase := range cases { for _, testcase := range cases {
nodes := []swarm.Node{ nodes := []swarm.Node{
{ {
ID: "nodeID1", ID: "nodeID1",
Description: swarm.NodeDescription{Hostname: "foobar_baz"}, Description: swarm.NodeDescription{
Hostname: "foobar_baz",
TLSInfo: swarm.TLSInfo{TrustRoot: "no"},
},
Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, Status: swarm.NodeStatus{State: swarm.NodeState("foo")},
Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")},
ManagerStatus: &swarm.ManagerStatus{Leader: true}, ManagerStatus: &swarm.ManagerStatus{Leader: true},
}, },
{ {
ID: "nodeID2", ID: "nodeID2",
Description: swarm.NodeDescription{Hostname: "foobar_bar"}, Description: swarm.NodeDescription{
Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, Hostname: "foobar_bar",
Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, TLSInfo: swarm.TLSInfo{TrustRoot: "hi"},
},
Status: swarm.NodeStatus{State: swarm.NodeState("bar")},
Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")},
ManagerStatus: &swarm.ManagerStatus{ ManagerStatus: &swarm.ManagerStatus{
Leader: false, Leader: false,
Reachability: swarm.Reachability("Reachable"), 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")},
},
} }
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := NodeWrite(testcase.context, nodes, types.Info{}) err := NodeWrite(testcase.context, nodes, types.Info{Swarm: swarm.Info{Cluster: &testcase.clusterInfo}})
if err != nil { if err != nil {
assert.EqualError(t, err, testcase.expected) assert.EqualError(t, err, testcase.expected)
} else { } else {
@ -158,27 +207,54 @@ foobar_bar
} }
func TestNodeContextWriteJSON(t *testing.T) { func TestNodeContextWriteJSON(t *testing.T) {
nodes := []swarm.Node{ cases := []struct {
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, expected []map[string]interface{}
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, info types.Info
} }{
expectedJSONs := []map[string]interface{}{ {
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false}, expected: []map[string]interface{}{
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false}, {"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"},
},
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"},
},
info: types.Info{
Swarm: swarm.Info{
Cluster: &swarm.ClusterInfo{
TLSInfo: swarm.TLSInfo{TrustRoot: "hi"},
RootRotationInProgress: true,
},
},
},
},
} }
out := bytes.NewBufferString("") for _, testcase := range cases {
err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, types.Info{}) nodes := []swarm.Node{
if err != nil { {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}},
t.Fatal(err) {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar", TLSInfo: swarm.TLSInfo{TrustRoot: "no"}}},
} {ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo"}},
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { }
t.Logf("Output: line %d: %s", i, line) out := bytes.NewBufferString("")
var m map[string]interface{} err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, testcase.info)
if err := json.Unmarshal([]byte(line), &m); err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
assert.Equal(t, expectedJSONs[i], m) for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, testcase.expected[i], m)
}
} }
} }
@ -201,3 +277,71 @@ func TestNodeContextWriteJSONField(t *testing.T) {
assert.Equal(t, nodes[i].ID, s) assert.Equal(t, nodes[i].ID, s)
} }
} }
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 := Context{
Format: NewNodeFormat("pretty", false),
Output: out,
}
err := NodeInspectWrite(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.Equal(t, expected, out.String())
}

View File

@ -129,6 +129,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL) fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL)
} }
} }
fmt.Fprintf(dockerCli.Out(), " Root Rotation In Progress: %v\n", info.Swarm.Cluster.RootRotationInProgress)
} }
fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr) fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr)
managers := []string{} managers := []string{}