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