github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/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 { 37 types.Container{ 38 Image: "a5a665ff33eced1e0803148700880edab4", 39 ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", 40 }, 41 true, 42 "a5a665ff33ec", 43 ctx.Image, 44 }, 45 { 46 types.Container{ 47 Image: "a5a665ff33eced1e0803148700880edab4", 48 ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", 49 }, 50 false, 51 "a5a665ff33eced1e0803148700880edab4", 52 ctx.Image, 53 }, 54 {types.Container{Image: ""}, true, "<no image>", ctx.Image}, 55 {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command}, 56 {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt}, 57 {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports}, 58 {types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status}, 59 {types.Container{SizeRw: 10}, true, "10B", ctx.Size}, 60 {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size}, 61 {types.Container{}, true, "", ctx.Labels}, 62 {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels}, 63 {types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor}, 64 {types.Container{ 65 Mounts: []types.MountPoint{ 66 { 67 Name: "this-is-a-long-volume-name-and-will-be-truncated-if-trunc-is-set", 68 Driver: "local", 69 Source: "/a/path", 70 }, 71 }, 72 }, true, "this-is-a-long…", ctx.Mounts}, 73 {types.Container{ 74 Mounts: []types.MountPoint{ 75 { 76 Driver: "local", 77 Source: "/a/path", 78 }, 79 }, 80 }, false, "/a/path", ctx.Mounts}, 81 {types.Container{ 82 Mounts: []types.MountPoint{ 83 { 84 Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", 85 Driver: "local", 86 Source: "/a/path", 87 }, 88 }, 89 }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts}, 90 } 91 92 for _, c := range cases { 93 ctx = ContainerContext{c: c.container, trunc: c.trunc} 94 v := c.call() 95 if strings.Contains(v, ",") { 96 test.CompareMultipleValues(t, v, c.expValue) 97 } else if v != c.expValue { 98 t.Fatalf("Expected %s, was %s\n", c.expValue, v) 99 } 100 } 101 102 c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}} 103 ctx = ContainerContext{c: c1, trunc: true} 104 105 sid := ctx.Label("com.docker.swarm.swarm-id") 106 node := ctx.Label("com.docker.swarm.node_name") 107 if sid != "33" { 108 t.Fatalf("Expected 33, was %s\n", sid) 109 } 110 111 if node != "ubuntu" { 112 t.Fatalf("Expected ubuntu, was %s\n", node) 113 } 114 115 c2 := types.Container{} 116 ctx = ContainerContext{c: c2, trunc: true} 117 118 label := ctx.Label("anything.really") 119 if label != "" { 120 t.Fatalf("Expected an empty string, was %s", label) 121 } 122 } 123 124 func TestContainerContextWrite(t *testing.T) { 125 unixTime := time.Now().AddDate(0, 0, -1).Unix() 126 expectedTime := time.Unix(unixTime, 0).String() 127 128 cases := []struct { 129 context Context 130 expected string 131 }{ 132 // Errors 133 { 134 Context{Format: "{{InvalidFunction}}"}, 135 `template parsing error: template: :1: function "InvalidFunction" not defined`, 136 }, 137 { 138 Context{Format: "{{nil}}"}, 139 `template parsing error: template: :1:2: executing "" at <nil>: nil is not a command`, 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 { 454 []types.Port{ 455 { 456 PrivatePort: 9988, 457 Type: "udp", 458 }, 459 }, 460 "9988/udp", 461 }, 462 { 463 []types.Port{ 464 { 465 IP: "0.0.0.0", 466 PrivatePort: 9988, 467 Type: "tcp", 468 }, 469 }, 470 "0.0.0.0:0->9988/tcp", 471 }, 472 { 473 []types.Port{ 474 { 475 PrivatePort: 9988, 476 PublicPort: 8899, 477 Type: "tcp", 478 }, 479 }, 480 "9988/tcp", 481 }, 482 { 483 []types.Port{ 484 { 485 IP: "4.3.2.1", 486 PrivatePort: 9988, 487 PublicPort: 8899, 488 Type: "tcp", 489 }, 490 }, 491 "4.3.2.1:8899->9988/tcp", 492 }, 493 { 494 []types.Port{ 495 { 496 IP: "4.3.2.1", 497 PrivatePort: 9988, 498 PublicPort: 9988, 499 Type: "tcp", 500 }, 501 }, 502 "4.3.2.1:9988->9988/tcp", 503 }, 504 { 505 []types.Port{ 506 { 507 PrivatePort: 9988, 508 Type: "udp", 509 }, { 510 PrivatePort: 9988, 511 Type: "udp", 512 }, 513 }, 514 "9988/udp, 9988/udp", 515 }, 516 { 517 []types.Port{ 518 { 519 IP: "1.2.3.4", 520 PublicPort: 9998, 521 PrivatePort: 9998, 522 Type: "udp", 523 }, { 524 IP: "1.2.3.4", 525 PublicPort: 9999, 526 PrivatePort: 9999, 527 Type: "udp", 528 }, 529 }, 530 "1.2.3.4:9998-9999->9998-9999/udp", 531 }, 532 { 533 []types.Port{ 534 { 535 IP: "1.2.3.4", 536 PublicPort: 8887, 537 PrivatePort: 9998, 538 Type: "udp", 539 }, { 540 IP: "1.2.3.4", 541 PublicPort: 8888, 542 PrivatePort: 9999, 543 Type: "udp", 544 }, 545 }, 546 "1.2.3.4:8887->9998/udp, 1.2.3.4:8888->9999/udp", 547 }, 548 { 549 []types.Port{ 550 { 551 PrivatePort: 9998, 552 Type: "udp", 553 }, { 554 PrivatePort: 9999, 555 Type: "udp", 556 }, 557 }, 558 "9998-9999/udp", 559 }, 560 { 561 []types.Port{ 562 { 563 IP: "1.2.3.4", 564 PrivatePort: 6677, 565 PublicPort: 7766, 566 Type: "tcp", 567 }, { 568 PrivatePort: 9988, 569 PublicPort: 8899, 570 Type: "udp", 571 }, 572 }, 573 "9988/udp, 1.2.3.4:7766->6677/tcp", 574 }, 575 { 576 []types.Port{ 577 { 578 IP: "1.2.3.4", 579 PrivatePort: 9988, 580 PublicPort: 8899, 581 Type: "udp", 582 }, { 583 IP: "1.2.3.4", 584 PrivatePort: 9988, 585 PublicPort: 8899, 586 Type: "tcp", 587 }, { 588 IP: "4.3.2.1", 589 PrivatePort: 2233, 590 PublicPort: 3322, 591 Type: "tcp", 592 }, 593 }, 594 "4.3.2.1:3322->2233/tcp, 1.2.3.4:8899->9988/tcp, 1.2.3.4:8899->9988/udp", 595 }, 596 { 597 []types.Port{ 598 { 599 PrivatePort: 9988, 600 PublicPort: 8899, 601 Type: "udp", 602 }, { 603 IP: "1.2.3.4", 604 PrivatePort: 6677, 605 PublicPort: 7766, 606 Type: "tcp", 607 }, { 608 IP: "4.3.2.1", 609 PrivatePort: 2233, 610 PublicPort: 3322, 611 Type: "tcp", 612 }, 613 }, 614 "9988/udp, 4.3.2.1:3322->2233/tcp, 1.2.3.4:7766->6677/tcp", 615 }, 616 { 617 []types.Port{ 618 { 619 PrivatePort: 80, 620 Type: "tcp", 621 }, { 622 PrivatePort: 1024, 623 Type: "tcp", 624 }, { 625 PrivatePort: 80, 626 Type: "udp", 627 }, { 628 PrivatePort: 1024, 629 Type: "udp", 630 }, { 631 IP: "1.1.1.1", 632 PublicPort: 80, 633 PrivatePort: 1024, 634 Type: "tcp", 635 }, { 636 IP: "1.1.1.1", 637 PublicPort: 80, 638 PrivatePort: 1024, 639 Type: "udp", 640 }, { 641 IP: "1.1.1.1", 642 PublicPort: 1024, 643 PrivatePort: 80, 644 Type: "tcp", 645 }, { 646 IP: "1.1.1.1", 647 PublicPort: 1024, 648 PrivatePort: 80, 649 Type: "udp", 650 }, { 651 IP: "2.1.1.1", 652 PublicPort: 80, 653 PrivatePort: 1024, 654 Type: "tcp", 655 }, { 656 IP: "2.1.1.1", 657 PublicPort: 80, 658 PrivatePort: 1024, 659 Type: "udp", 660 }, { 661 IP: "2.1.1.1", 662 PublicPort: 1024, 663 PrivatePort: 80, 664 Type: "tcp", 665 }, { 666 IP: "2.1.1.1", 667 PublicPort: 1024, 668 PrivatePort: 80, 669 Type: "udp", 670 }, { 671 PrivatePort: 12345, 672 Type: "sctp", 673 }, 674 }, 675 "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", 676 }, 677 } 678 679 for _, port := range cases { 680 actual := DisplayablePorts(port.ports) 681 assert.Check(t, is.Equal(port.expected, actual)) 682 } 683 }