github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/node/formatter_test.go (about) 1 // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: 2 //go:build go1.19 3 4 package node 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "strings" 11 "testing" 12 13 "github.com/docker/cli/cli/command/formatter" 14 "github.com/docker/cli/internal/test" 15 "github.com/docker/docker/api/types/swarm" 16 "github.com/docker/docker/api/types/system" 17 "github.com/docker/docker/pkg/stringid" 18 "gotest.tools/v3/assert" 19 is "gotest.tools/v3/assert/cmp" 20 ) 21 22 func TestNodeContext(t *testing.T) { 23 nodeID := stringid.GenerateRandomID() 24 25 var ctx nodeContext 26 cases := []struct { 27 nodeCtx nodeContext 28 expValue string 29 call func() string 30 }{ 31 {nodeContext{ 32 n: swarm.Node{ID: nodeID}, 33 }, nodeID, ctx.ID}, 34 {nodeContext{ 35 n: swarm.Node{Description: swarm.NodeDescription{Hostname: "node_hostname"}}, 36 }, "node_hostname", ctx.Hostname}, 37 {nodeContext{ 38 n: swarm.Node{Status: swarm.NodeStatus{State: swarm.NodeState("foo")}}, 39 }, "Foo", ctx.Status}, 40 {nodeContext{ 41 n: swarm.Node{Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}}, 42 }, "Drain", ctx.Availability}, 43 {nodeContext{ 44 n: swarm.Node{ManagerStatus: &swarm.ManagerStatus{Leader: true}}, 45 }, "Leader", ctx.ManagerStatus}, 46 } 47 48 for _, c := range cases { 49 ctx = c.nodeCtx 50 v := c.call() 51 if strings.Contains(v, ",") { 52 test.CompareMultipleValues(t, v, c.expValue) 53 } else if v != c.expValue { 54 t.Fatalf("Expected %s, was %s\n", c.expValue, v) 55 } 56 } 57 } 58 59 func TestNodeContextWrite(t *testing.T) { 60 cases := []struct { 61 context formatter.Context 62 expected string 63 clusterInfo swarm.ClusterInfo 64 }{ 65 // Errors 66 { 67 context: formatter.Context{Format: "{{InvalidFunction}}"}, 68 expected: `template parsing error: template: :1: function "InvalidFunction" not defined`, 69 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 70 }, 71 { 72 context: formatter.Context{Format: "{{nil}}"}, 73 expected: `template parsing error: template: :1:2: executing "" at <nil>: nil is not a command`, 74 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 75 }, 76 // Table format 77 { 78 context: formatter.Context{Format: NewFormat("table", false)}, 79 expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION 80 nodeID1 foobar_baz Foo Drain Leader 18.03.0-ce 81 nodeID2 foobar_bar Bar Active Reachable 1.2.3 82 nodeID3 foobar_boo Boo Active ` + "\n", // (to preserve whitespace) 83 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 84 }, 85 { 86 context: formatter.Context{Format: NewFormat("table", true)}, 87 expected: `nodeID1 88 nodeID2 89 nodeID3 90 `, 91 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 92 }, 93 { 94 context: formatter.Context{Format: NewFormat("table {{.Hostname}}", false)}, 95 expected: `HOSTNAME 96 foobar_baz 97 foobar_bar 98 foobar_boo 99 `, 100 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 101 }, 102 { 103 context: formatter.Context{Format: NewFormat("table {{.Hostname}}", true)}, 104 expected: `HOSTNAME 105 foobar_baz 106 foobar_bar 107 foobar_boo 108 `, 109 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 110 }, 111 { 112 context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, 113 expected: `ID HOSTNAME TLS STATUS 114 nodeID1 foobar_baz Needs Rotation 115 nodeID2 foobar_bar Ready 116 nodeID3 foobar_boo Unknown 117 `, 118 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 119 }, 120 { // no cluster TLS status info, TLS status for all nodes is unknown 121 context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, 122 expected: `ID HOSTNAME TLS STATUS 123 nodeID1 foobar_baz Unknown 124 nodeID2 foobar_bar Unknown 125 nodeID3 foobar_boo Unknown 126 `, 127 clusterInfo: swarm.ClusterInfo{}, 128 }, 129 // Raw Format 130 { 131 context: formatter.Context{Format: NewFormat("raw", false)}, 132 expected: `node_id: nodeID1 133 hostname: foobar_baz 134 status: Foo 135 availability: Drain 136 manager_status: Leader 137 138 node_id: nodeID2 139 hostname: foobar_bar 140 status: Bar 141 availability: Active 142 manager_status: Reachable 143 144 node_id: nodeID3 145 hostname: foobar_boo 146 status: Boo 147 availability: Active 148 manager_status: ` + "\n\n", // to preserve whitespace 149 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 150 }, 151 { 152 context: formatter.Context{Format: NewFormat("raw", true)}, 153 expected: `node_id: nodeID1 154 node_id: nodeID2 155 node_id: nodeID3 156 `, 157 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 158 }, 159 // Custom Format 160 { 161 context: formatter.Context{Format: NewFormat("{{.Hostname}} {{.TLSStatus}}", false)}, 162 expected: `foobar_baz Needs Rotation 163 foobar_bar Ready 164 foobar_boo Unknown 165 `, 166 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, 167 }, 168 } 169 170 nodes := []swarm.Node{ 171 { 172 ID: "nodeID1", 173 Description: swarm.NodeDescription{ 174 Hostname: "foobar_baz", 175 TLSInfo: swarm.TLSInfo{TrustRoot: "no"}, 176 Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}, 177 }, 178 Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, 179 Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, 180 ManagerStatus: &swarm.ManagerStatus{Leader: true}, 181 }, 182 { 183 ID: "nodeID2", 184 Description: swarm.NodeDescription{ 185 Hostname: "foobar_bar", 186 TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, 187 Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}, 188 }, 189 Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, 190 Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, 191 ManagerStatus: &swarm.ManagerStatus{ 192 Leader: false, 193 Reachability: swarm.Reachability("Reachable"), 194 }, 195 }, 196 { 197 ID: "nodeID3", 198 Description: swarm.NodeDescription{Hostname: "foobar_boo"}, 199 Status: swarm.NodeStatus{State: swarm.NodeState("boo")}, 200 Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, 201 }, 202 } 203 204 for _, tc := range cases { 205 tc := tc 206 t.Run(string(tc.context.Format), func(t *testing.T) { 207 var out bytes.Buffer 208 tc.context.Output = &out 209 210 err := FormatWrite(tc.context, nodes, system.Info{Swarm: swarm.Info{Cluster: &tc.clusterInfo}}) 211 if err != nil { 212 assert.Error(t, err, tc.expected) 213 } else { 214 assert.Equal(t, out.String(), tc.expected) 215 } 216 }) 217 } 218 } 219 220 func TestNodeContextWriteJSON(t *testing.T) { 221 cases := []struct { 222 expected []map[string]any 223 info system.Info 224 }{ 225 { 226 expected: []map[string]any{ 227 {"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "1.2.3"}, 228 {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": ""}, 229 {"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"}, 230 }, 231 info: system.Info{}, 232 }, 233 { 234 expected: []map[string]any{ 235 {"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Ready", "EngineVersion": "1.2.3"}, 236 {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Needs Rotation", "EngineVersion": ""}, 237 {"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"}, 238 }, 239 info: system.Info{ 240 Swarm: swarm.Info{ 241 Cluster: &swarm.ClusterInfo{ 242 TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, 243 RootRotationInProgress: true, 244 }, 245 }, 246 }, 247 }, 248 } 249 250 for _, testcase := range cases { 251 nodes := []swarm.Node{ 252 {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}}}, 253 {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar", TLSInfo: swarm.TLSInfo{TrustRoot: "no"}}}, 254 {ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}}, 255 } 256 out := bytes.NewBufferString("") 257 err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, nodes, testcase.info) 258 if err != nil { 259 t.Fatal(err) 260 } 261 for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { 262 msg := fmt.Sprintf("Output: line %d: %s", i, line) 263 var m map[string]any 264 err := json.Unmarshal([]byte(line), &m) 265 assert.NilError(t, err, msg) 266 assert.Check(t, is.DeepEqual(testcase.expected[i], m), msg) 267 } 268 } 269 } 270 271 func TestNodeContextWriteJSONField(t *testing.T) { 272 nodes := []swarm.Node{ 273 {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, 274 {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, 275 } 276 out := bytes.NewBufferString("") 277 err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, nodes, system.Info{}) 278 if err != nil { 279 t.Fatal(err) 280 } 281 for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { 282 msg := fmt.Sprintf("Output: line %d: %s", i, line) 283 var s string 284 err := json.Unmarshal([]byte(line), &s) 285 assert.NilError(t, err, msg) 286 assert.Check(t, is.Equal(nodes[i].ID, s), msg) 287 } 288 } 289 290 func TestNodeInspectWriteContext(t *testing.T) { 291 node := swarm.Node{ 292 ID: "nodeID1", 293 Description: swarm.NodeDescription{ 294 Hostname: "foobar_baz", 295 TLSInfo: swarm.TLSInfo{ 296 TrustRoot: "-----BEGIN CERTIFICATE-----\ndata\n-----END CERTIFICATE-----\n", 297 CertIssuerPublicKey: []byte("pubKey"), 298 CertIssuerSubject: []byte("subject"), 299 }, 300 Platform: swarm.Platform{ 301 OS: "linux", 302 Architecture: "amd64", 303 }, 304 Resources: swarm.Resources{ 305 MemoryBytes: 1, 306 }, 307 Engine: swarm.EngineDescription{ 308 EngineVersion: "0.1.1", 309 }, 310 }, 311 Status: swarm.NodeStatus{ 312 State: swarm.NodeState("ready"), 313 Addr: "1.1.1.1", 314 }, 315 Spec: swarm.NodeSpec{ 316 Availability: swarm.NodeAvailability("drain"), 317 Role: swarm.NodeRole("manager"), 318 }, 319 } 320 out := bytes.NewBufferString("") 321 context := formatter.Context{ 322 Format: NewFormat("pretty", false), 323 Output: out, 324 } 325 err := InspectFormatWrite(context, []string{"nodeID1"}, func(string) (any, []byte, error) { 326 return node, nil, nil 327 }) 328 if err != nil { 329 t.Fatal(err) 330 } 331 expected := `ID: nodeID1 332 Hostname: foobar_baz 333 Joined at: 0001-01-01 00:00:00 +0000 utc 334 Status: 335 State: Ready 336 Availability: Drain 337 Address: 1.1.1.1 338 Platform: 339 Operating System: linux 340 Architecture: amd64 341 Resources: 342 CPUs: 0 343 Memory: 1B 344 Engine Version: 0.1.1 345 TLS Info: 346 TrustRoot: 347 -----BEGIN CERTIFICATE----- 348 data 349 -----END CERTIFICATE----- 350 351 Issuer Subject: c3ViamVjdA== 352 Issuer Public Key: cHViS2V5 353 ` 354 assert.Check(t, is.Equal(expected, out.String())) 355 }