github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/command/formatter/container_test.go (about) 1 package formatter 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/docker/cli/internal/test" 12 "github.com/docker/docker/api/types" 13 "github.com/docker/docker/pkg/stringid" 14 "gotest.tools/v3/assert" 15 is "gotest.tools/v3/assert/cmp" 16 "gotest.tools/v3/golden" 17 ) 18 19 func TestContainerPsContext(t *testing.T) { 20 containerID := stringid.GenerateRandomID() 21 unix := time.Now().Add(-65 * time.Second).Unix() 22 23 var ctx ContainerContext 24 cases := []struct { 25 container types.Container 26 trunc bool 27 expValue string 28 call func() string 29 }{ 30 {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID}, 31 {types.Container{ID: containerID}, false, containerID, ctx.ID}, 32 {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names}, 33 {types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image}, 34 {types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image}, 35 {types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image}, 36 {types.Container{ 37 Image: "a5a665ff33eced1e0803148700880edab4", 38 ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", 39 }, 40 true, 41 "a5a665ff33ec", 42 ctx.Image, 43 }, 44 {types.Container{ 45 Image: "a5a665ff33eced1e0803148700880edab4", 46 ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", 47 }, 48 false, 49 "a5a665ff33eced1e0803148700880edab4", 50 ctx.Image, 51 }, 52 {types.Container{Image: ""}, true, "<no image>", ctx.Image}, 53 {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command}, 54 {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt}, 55 {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports}, 56 {types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status}, 57 {types.Container{SizeRw: 10}, true, "10B", ctx.Size}, 58 {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size}, 59 {types.Container{}, true, "", ctx.Labels}, 60 {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels}, 61 {types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor}, 62 {types.Container{ 63 Mounts: []types.MountPoint{ 64 { 65 Name: "this-is-a-long-volume-name-and-will-be-truncated-if-trunc-is-set", 66 Driver: "local", 67 Source: "/a/path", 68 }, 69 }, 70 }, true, "this-is-a-long…", ctx.Mounts}, 71 {types.Container{ 72 Mounts: []types.MountPoint{ 73 { 74 Driver: "local", 75 Source: "/a/path", 76 }, 77 }, 78 }, false, "/a/path", ctx.Mounts}, 79 {types.Container{ 80 Mounts: []types.MountPoint{ 81 { 82 Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", 83 Driver: "local", 84 Source: "/a/path", 85 }, 86 }, 87 }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts}, 88 } 89 90 for _, c := range cases { 91 ctx = ContainerContext{c: c.container, trunc: c.trunc} 92 v := c.call() 93 if strings.Contains(v, ",") { 94 test.CompareMultipleValues(t, v, c.expValue) 95 } else if v != c.expValue { 96 t.Fatalf("Expected %s, was %s\n", c.expValue, v) 97 } 98 } 99 100 c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}} 101 ctx = ContainerContext{c: c1, trunc: true} 102 103 sid := ctx.Label("com.docker.swarm.swarm-id") 104 node := ctx.Label("com.docker.swarm.node_name") 105 if sid != "33" { 106 t.Fatalf("Expected 33, was %s\n", sid) 107 } 108 109 if node != "ubuntu" { 110 t.Fatalf("Expected ubuntu, was %s\n", node) 111 } 112 113 c2 := types.Container{} 114 ctx = ContainerContext{c: c2, trunc: true} 115 116 label := ctx.Label("anything.really") 117 if label != "" { 118 t.Fatalf("Expected an empty string, was %s", label) 119 } 120 } 121 122 func TestContainerContextWrite(t *testing.T) { 123 unixTime := time.Now().AddDate(0, 0, -1).Unix() 124 expectedTime := time.Unix(unixTime, 0).String() 125 126 cases := []struct { 127 context Context 128 expected string 129 }{ 130 // Errors 131 { 132 Context{Format: "{{InvalidFunction}}"}, 133 `Template parsing error: template: :1: function "InvalidFunction" not defined 134 `, 135 }, 136 { 137 Context{Format: "{{nil}}"}, 138 `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command 139 `, 140 }, 141 // Table Format 142 { 143 Context{Format: NewContainerFormat("table", false, true)}, 144 `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE 145 containerID1 ubuntu "" 24 hours ago foobar_baz 0B 146 containerID2 ubuntu "" 24 hours ago foobar_bar 0B 147 `, 148 }, 149 { 150 Context{Format: NewContainerFormat("table", false, false)}, 151 `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 152 containerID1 ubuntu "" 24 hours ago foobar_baz 153 containerID2 ubuntu "" 24 hours ago foobar_bar 154 `, 155 }, 156 { 157 Context{Format: NewContainerFormat("table {{.Image}}", false, false)}, 158 "IMAGE\nubuntu\nubuntu\n", 159 }, 160 { 161 Context{Format: NewContainerFormat("table {{.Image}}", false, true)}, 162 "IMAGE\nubuntu\nubuntu\n", 163 }, 164 { 165 Context{Format: NewContainerFormat("table {{.Image}}", true, false)}, 166 "IMAGE\nubuntu\nubuntu\n", 167 }, 168 { 169 Context{Format: NewContainerFormat("table", true, false)}, 170 "containerID1\ncontainerID2\n", 171 }, 172 { 173 Context{Format: NewContainerFormat("table {{.State}}", false, true)}, 174 "STATE\nrunning\nrunning\n", 175 }, 176 // Raw Format 177 { 178 Context{Format: NewContainerFormat("raw", false, false)}, 179 fmt.Sprintf(`container_id: containerID1 180 image: ubuntu 181 command: "" 182 created_at: %s 183 state: running 184 status: 185 names: foobar_baz 186 labels: 187 ports: 188 189 container_id: containerID2 190 image: ubuntu 191 command: "" 192 created_at: %s 193 state: running 194 status: 195 names: foobar_bar 196 labels: 197 ports: 198 199 `, expectedTime, expectedTime), 200 }, 201 { 202 Context{Format: NewContainerFormat("raw", false, true)}, 203 fmt.Sprintf(`container_id: containerID1 204 image: ubuntu 205 command: "" 206 created_at: %s 207 state: running 208 status: 209 names: foobar_baz 210 labels: 211 ports: 212 size: 0B 213 214 container_id: containerID2 215 image: ubuntu 216 command: "" 217 created_at: %s 218 state: running 219 status: 220 names: foobar_bar 221 labels: 222 ports: 223 size: 0B 224 225 `, expectedTime, expectedTime), 226 }, 227 { 228 Context{Format: NewContainerFormat("raw", true, false)}, 229 "container_id: containerID1\ncontainer_id: containerID2\n", 230 }, 231 // Custom Format 232 { 233 Context{Format: "{{.Image}}"}, 234 "ubuntu\nubuntu\n", 235 }, 236 { 237 Context{Format: NewContainerFormat("{{.Image}}", false, true)}, 238 "ubuntu\nubuntu\n", 239 }, 240 // Special headers for customized table format 241 { 242 Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)}, 243 string(golden.Get(t, "container-context-write-special-headers.golden")), 244 }, 245 { 246 Context{Format: NewContainerFormat(`table {{split .Image ":"}}`, false, false)}, 247 "IMAGE\n[ubuntu]\n[ubuntu]\n", 248 }, 249 } 250 251 containers := []types.Container{ 252 {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime, State: "running"}, 253 {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime, State: "running"}, 254 } 255 256 for _, tc := range cases { 257 tc := tc 258 t.Run(string(tc.context.Format), func(t *testing.T) { 259 var out bytes.Buffer 260 tc.context.Output = &out 261 err := ContainerWrite(tc.context, containers) 262 if err != nil { 263 assert.Error(t, err, tc.expected) 264 } else { 265 assert.Equal(t, out.String(), tc.expected) 266 } 267 }) 268 269 } 270 } 271 272 func TestContainerContextWriteWithNoContainers(t *testing.T) { 273 out := bytes.NewBufferString("") 274 containers := []types.Container{} 275 276 cases := []struct { 277 context Context 278 expected string 279 }{ 280 { 281 Context{ 282 Format: "{{.Image}}", 283 Output: out, 284 }, 285 "", 286 }, 287 { 288 Context{ 289 Format: "table {{.Image}}", 290 Output: out, 291 }, 292 "IMAGE\n", 293 }, 294 { 295 Context{ 296 Format: NewContainerFormat("{{.Image}}", false, true), 297 Output: out, 298 }, 299 "", 300 }, 301 { 302 Context{ 303 Format: NewContainerFormat("table {{.Image}}", false, true), 304 Output: out, 305 }, 306 "IMAGE\n", 307 }, 308 { 309 Context{ 310 Format: "table {{.Image}}\t{{.Size}}", 311 Output: out, 312 }, 313 "IMAGE SIZE\n", 314 }, 315 { 316 Context{ 317 Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true), 318 Output: out, 319 }, 320 "IMAGE SIZE\n", 321 }, 322 } 323 324 for _, tc := range cases { 325 tc := tc 326 t.Run(string(tc.context.Format), func(t *testing.T) { 327 err := ContainerWrite(tc.context, containers) 328 assert.NilError(t, err) 329 assert.Equal(t, out.String(), tc.expected) 330 // Clean buffer 331 out.Reset() 332 }) 333 } 334 } 335 336 func TestContainerContextWriteJSON(t *testing.T) { 337 unix := time.Now().Add(-65 * time.Second).Unix() 338 containers := []types.Container{ 339 {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix, State: "running"}, 340 {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix, State: "running"}, 341 } 342 expectedCreated := time.Unix(unix, 0).String() 343 expectedJSONs := []map[string]interface{}{ 344 { 345 "Command": "\"\"", 346 "CreatedAt": expectedCreated, 347 "ID": "containerID1", 348 "Image": "ubuntu", 349 "Labels": "", 350 "LocalVolumes": "0", 351 "Mounts": "", 352 "Names": "foobar_baz", 353 "Networks": "", 354 "Ports": "", 355 "RunningFor": "About a minute ago", 356 "Size": "0B", 357 "State": "running", 358 "Status": "", 359 }, 360 { 361 "Command": "\"\"", 362 "CreatedAt": expectedCreated, 363 "ID": "containerID2", 364 "Image": "ubuntu", 365 "Labels": "", 366 "LocalVolumes": "0", 367 "Mounts": "", 368 "Names": "foobar_bar", 369 "Networks": "", 370 "Ports": "", 371 "RunningFor": "About a minute ago", 372 "Size": "0B", 373 "State": "running", 374 "Status": "", 375 }, 376 } 377 out := bytes.NewBufferString("") 378 err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers) 379 if err != nil { 380 t.Fatal(err) 381 } 382 for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { 383 msg := fmt.Sprintf("Output: line %d: %s", i, line) 384 var m map[string]interface{} 385 err := json.Unmarshal([]byte(line), &m) 386 assert.NilError(t, err, msg) 387 assert.Check(t, is.DeepEqual(expectedJSONs[i], m), msg) 388 } 389 } 390 391 func TestContainerContextWriteJSONField(t *testing.T) { 392 containers := []types.Container{ 393 {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu"}, 394 {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu"}, 395 } 396 out := bytes.NewBufferString("") 397 err := ContainerWrite(Context{Format: "{{json .ID}}", Output: out}, containers) 398 if err != nil { 399 t.Fatal(err) 400 } 401 for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { 402 msg := fmt.Sprintf("Output: line %d: %s", i, line) 403 var s string 404 err := json.Unmarshal([]byte(line), &s) 405 assert.NilError(t, err, msg) 406 assert.Check(t, is.Equal(containers[i].ID, s), msg) 407 } 408 } 409 410 func TestContainerBackCompat(t *testing.T) { 411 containers := []types.Container{{ID: "brewhaha"}} 412 cases := []string{ 413 "ID", 414 "Names", 415 "Image", 416 "Command", 417 "CreatedAt", 418 "RunningFor", 419 "Ports", 420 "Status", 421 "Size", 422 "Labels", 423 "Mounts", 424 } 425 buf := bytes.NewBuffer(nil) 426 for _, c := range cases { 427 ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", c)), Output: buf} 428 if err := ContainerWrite(ctx, containers); err != nil { 429 t.Logf("could not render template for field '%s': %v", c, err) 430 t.Fail() 431 } 432 buf.Reset() 433 } 434 } 435 436 type ports struct { 437 ports []types.Port 438 expected string 439 } 440 441 // nolint: lll 442 func TestDisplayablePorts(t *testing.T) { 443 cases := []ports{ 444 { 445 []types.Port{ 446 { 447 PrivatePort: 9988, 448 Type: "tcp", 449 }, 450 }, 451 "9988/tcp"}, 452 { 453 []types.Port{ 454 { 455 PrivatePort: 9988, 456 Type: "udp", 457 }, 458 }, 459 "9988/udp", 460 }, 461 { 462 []types.Port{ 463 { 464 IP: "0.0.0.0", 465 PrivatePort: 9988, 466 Type: "tcp", 467 }, 468 }, 469 "0.0.0.0:0->9988/tcp", 470 }, 471 { 472 []types.Port{ 473 { 474 PrivatePort: 9988, 475 PublicPort: 8899, 476 Type: "tcp", 477 }, 478 }, 479 "9988/tcp", 480 }, 481 { 482 []types.Port{ 483 { 484 IP: "4.3.2.1", 485 PrivatePort: 9988, 486 PublicPort: 8899, 487 Type: "tcp", 488 }, 489 }, 490 "4.3.2.1:8899->9988/tcp", 491 }, 492 { 493 []types.Port{ 494 { 495 IP: "4.3.2.1", 496 PrivatePort: 9988, 497 PublicPort: 9988, 498 Type: "tcp", 499 }, 500 }, 501 "4.3.2.1:9988->9988/tcp", 502 }, 503 { 504 []types.Port{ 505 { 506 PrivatePort: 9988, 507 Type: "udp", 508 }, { 509 PrivatePort: 9988, 510 Type: "udp", 511 }, 512 }, 513 "9988/udp, 9988/udp", 514 }, 515 { 516 []types.Port{ 517 { 518 IP: "1.2.3.4", 519 PublicPort: 9998, 520 PrivatePort: 9998, 521 Type: "udp", 522 }, { 523 IP: "1.2.3.4", 524 PublicPort: 9999, 525 PrivatePort: 9999, 526 Type: "udp", 527 }, 528 }, 529 "1.2.3.4:9998-9999->9998-9999/udp", 530 }, 531 { 532 []types.Port{ 533 { 534 IP: "1.2.3.4", 535 PublicPort: 8887, 536 PrivatePort: 9998, 537 Type: "udp", 538 }, { 539 IP: "1.2.3.4", 540 PublicPort: 8888, 541 PrivatePort: 9999, 542 Type: "udp", 543 }, 544 }, 545 "1.2.3.4:8887->9998/udp, 1.2.3.4:8888->9999/udp", 546 }, 547 { 548 []types.Port{ 549 { 550 PrivatePort: 9998, 551 Type: "udp", 552 }, { 553 PrivatePort: 9999, 554 Type: "udp", 555 }, 556 }, 557 "9998-9999/udp", 558 }, 559 { 560 []types.Port{ 561 { 562 IP: "1.2.3.4", 563 PrivatePort: 6677, 564 PublicPort: 7766, 565 Type: "tcp", 566 }, { 567 PrivatePort: 9988, 568 PublicPort: 8899, 569 Type: "udp", 570 }, 571 }, 572 "9988/udp, 1.2.3.4:7766->6677/tcp", 573 }, 574 { 575 []types.Port{ 576 { 577 IP: "1.2.3.4", 578 PrivatePort: 9988, 579 PublicPort: 8899, 580 Type: "udp", 581 }, { 582 IP: "1.2.3.4", 583 PrivatePort: 9988, 584 PublicPort: 8899, 585 Type: "tcp", 586 }, { 587 IP: "4.3.2.1", 588 PrivatePort: 2233, 589 PublicPort: 3322, 590 Type: "tcp", 591 }, 592 }, 593 "4.3.2.1:3322->2233/tcp, 1.2.3.4:8899->9988/tcp, 1.2.3.4:8899->9988/udp", 594 }, 595 { 596 []types.Port{ 597 { 598 PrivatePort: 9988, 599 PublicPort: 8899, 600 Type: "udp", 601 }, { 602 IP: "1.2.3.4", 603 PrivatePort: 6677, 604 PublicPort: 7766, 605 Type: "tcp", 606 }, { 607 IP: "4.3.2.1", 608 PrivatePort: 2233, 609 PublicPort: 3322, 610 Type: "tcp", 611 }, 612 }, 613 "9988/udp, 4.3.2.1:3322->2233/tcp, 1.2.3.4:7766->6677/tcp", 614 }, 615 { 616 []types.Port{ 617 { 618 PrivatePort: 80, 619 Type: "tcp", 620 }, { 621 PrivatePort: 1024, 622 Type: "tcp", 623 }, { 624 PrivatePort: 80, 625 Type: "udp", 626 }, { 627 PrivatePort: 1024, 628 Type: "udp", 629 }, { 630 IP: "1.1.1.1", 631 PublicPort: 80, 632 PrivatePort: 1024, 633 Type: "tcp", 634 }, { 635 IP: "1.1.1.1", 636 PublicPort: 80, 637 PrivatePort: 1024, 638 Type: "udp", 639 }, { 640 IP: "1.1.1.1", 641 PublicPort: 1024, 642 PrivatePort: 80, 643 Type: "tcp", 644 }, { 645 IP: "1.1.1.1", 646 PublicPort: 1024, 647 PrivatePort: 80, 648 Type: "udp", 649 }, { 650 IP: "2.1.1.1", 651 PublicPort: 80, 652 PrivatePort: 1024, 653 Type: "tcp", 654 }, { 655 IP: "2.1.1.1", 656 PublicPort: 80, 657 PrivatePort: 1024, 658 Type: "udp", 659 }, { 660 IP: "2.1.1.1", 661 PublicPort: 1024, 662 PrivatePort: 80, 663 Type: "tcp", 664 }, { 665 IP: "2.1.1.1", 666 PublicPort: 1024, 667 PrivatePort: 80, 668 Type: "udp", 669 }, { 670 PrivatePort: 12345, 671 Type: "sctp", 672 }, 673 }, 674 "80/tcp, 80/udp, 1024/tcp, 1024/udp, 12345/sctp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp", 675 }, 676 } 677 678 for _, port := range cases { 679 actual := DisplayablePorts(port.ports) 680 assert.Check(t, is.Equal(port.expected, actual)) 681 } 682 }