github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_list_linux_test.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "errors" 21 "fmt" 22 "os" 23 "strings" 24 "testing" 25 26 "github.com/containerd/nerdctl/pkg/formatter" 27 "github.com/containerd/nerdctl/pkg/strutil" 28 "github.com/containerd/nerdctl/pkg/tabutil" 29 "github.com/containerd/nerdctl/pkg/testutil" 30 "gotest.tools/v3/assert" 31 ) 32 33 type psTestContainer struct { 34 name string 35 labels map[string]string 36 volumes []string 37 network string 38 } 39 40 // When keepAlive is false, the container will exit immediately with status 1. 41 func preparePsTestContainer(t *testing.T, identity string, keepAlive bool) (*testutil.Base, psTestContainer) { 42 base := testutil.NewBase(t) 43 44 base.Cmd("pull", testutil.CommonImage).AssertOK() 45 46 testContainerName := testutil.Identifier(t) + identity 47 rwVolName := testContainerName + "-rw" 48 // A container can mount named and anonymous volumes 49 rwDir, err := os.MkdirTemp(t.TempDir(), "rw") 50 if err != nil { 51 t.Fatal(err) 52 } 53 base.Cmd("network", "create", testContainerName).AssertOK() 54 t.Cleanup(func() { 55 base.Cmd("rm", "-f", testContainerName).AssertOK() 56 base.Cmd("volume", "rm", "-f", rwVolName).Run() 57 base.Cmd("network", "rm", testContainerName).Run() 58 os.RemoveAll(rwDir) 59 }) 60 61 // A container can have multiple labels. 62 // Therefore, this test container has multiple labels to check it. 63 testLabels := make(map[string]string) 64 keys := []string{ 65 testutil.Identifier(t) + identity, 66 testutil.Identifier(t) + identity, 67 } 68 // fill the value of testLabels 69 for _, k := range keys { 70 testLabels[k] = k 71 } 72 base.Cmd("volume", "create", rwVolName).AssertOK() 73 mnt1 := fmt.Sprintf("%s:/%s_mnt1", rwDir, identity) 74 mnt2 := fmt.Sprintf("%s:/%s_mnt3", rwVolName, identity) 75 76 args := []string{ 77 "run", 78 "-d", 79 "--name", 80 testContainerName, 81 "--label", 82 formatter.FormatLabels(testLabels), 83 "-v", mnt1, 84 "-v", mnt2, 85 "--net", testContainerName, 86 } 87 if keepAlive { 88 args = append(args, testutil.CommonImage, "top") 89 } else { 90 args = append(args, "--restart=no", testutil.CommonImage, "false") 91 } 92 93 base.Cmd(args...).AssertOK() 94 if keepAlive { 95 base.EnsureContainerStarted(testContainerName) 96 } else { 97 base.EnsureContainerExited(testContainerName, 1) 98 } 99 100 // dd if=/dev/zero of=test_file bs=1M count=25 101 // let the container occupy 25MiB space. 102 if keepAlive { 103 base.Cmd("exec", testContainerName, "dd", "if=/dev/zero", "of=/test_file", "bs=1M", "count=25").AssertOK() 104 } 105 volumes := []string{} 106 volumes = append(volumes, strings.Split(mnt1, ":")...) 107 volumes = append(volumes, strings.Split(mnt2, ":")...) 108 109 return base, psTestContainer{ 110 name: testContainerName, 111 labels: testLabels, 112 volumes: volumes, 113 network: testContainerName, 114 } 115 } 116 117 func TestContainerList(t *testing.T) { 118 base, testContainer := preparePsTestContainer(t, "list", true) 119 120 // hope there are no tests running parallel 121 base.Cmd("ps", "-n", "1", "-s").AssertOutWithFunc(func(stdout string) error { 122 // An example of nerdctl/docker ps -n 1 -s 123 // CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE 124 // be8d386c991e docker.io/library/busybox:latest "top" 1 second ago Up c1 16.0 KiB (virtual 1.3 MiB) 125 126 lines := strings.Split(strings.TrimSpace(stdout), "\n") 127 if len(lines) < 2 { 128 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 129 } 130 131 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") 132 err := tab.ParseHeader(lines[0]) 133 if err != nil { 134 return fmt.Errorf("failed to parse header: %v", err) 135 } 136 137 container, _ := tab.ReadRow(lines[1], "NAMES") 138 assert.Equal(t, container, testContainer.name) 139 140 image, _ := tab.ReadRow(lines[1], "IMAGE") 141 assert.Equal(t, image, testutil.CommonImage) 142 143 size, _ := tab.ReadRow(lines[1], "SIZE") 144 145 // there is some difference between nerdctl and docker in calculating the size of the container 146 expectedSize := "26.2MB (virtual " 147 if base.Target != testutil.Docker { 148 expectedSize = "25.0 MiB (virtual " 149 } 150 151 if !strings.Contains(size, expectedSize) { 152 return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) 153 } 154 155 return nil 156 }) 157 } 158 159 func TestContainerListWideMode(t *testing.T) { 160 testutil.DockerIncompatible(t) 161 base, testContainer := preparePsTestContainer(t, "listWithMode", true) 162 163 // hope there are no tests running parallel 164 base.Cmd("ps", "-n", "1", "--format", "wide").AssertOutWithFunc(func(stdout string) error { 165 166 // An example of nerdctl ps --format wide 167 // CONTAINER ID IMAGE PLATFORM COMMAND CREATED STATUS PORTS NAMES RUNTIME SIZE 168 // 17181f208b61 docker.io/library/busybox:latest linux/amd64 "top" About an hour ago Up busybox-17181 io.containerd.runc.v2 16.0 KiB (virtual 1.3 MiB) 169 170 lines := strings.Split(strings.TrimSpace(stdout), "\n") 171 if len(lines) < 2 { 172 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 173 } 174 175 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE") 176 err := tab.ParseHeader(lines[0]) 177 if err != nil { 178 return fmt.Errorf("failed to parse header: %v", err) 179 } 180 181 container, _ := tab.ReadRow(lines[1], "NAMES") 182 assert.Equal(t, container, testContainer.name) 183 184 image, _ := tab.ReadRow(lines[1], "IMAGE") 185 assert.Equal(t, image, testutil.CommonImage) 186 187 runtime, _ := tab.ReadRow(lines[1], "RUNTIME") 188 assert.Equal(t, runtime, "io.containerd.runc.v2") 189 190 size, _ := tab.ReadRow(lines[1], "SIZE") 191 expectedSize := "25.0 MiB (virtual " 192 if !strings.Contains(size, expectedSize) { 193 return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) 194 } 195 return nil 196 }) 197 } 198 199 func TestContainerListWithLabels(t *testing.T) { 200 base, testContainer := preparePsTestContainer(t, "listWithLabels", true) 201 202 // hope there are no tests running parallel 203 base.Cmd("ps", "-n", "1", "--format", "{{.Labels}}").AssertOutWithFunc(func(stdout string) error { 204 205 // An example of nerdctl ps --format "{{.Labels}}" 206 // key1=value1,key2=value2,key3=value3 207 lines := strings.Split(strings.TrimSpace(stdout), "\n") 208 if len(lines) != 1 { 209 return fmt.Errorf("expected 1 line, got %d", len(lines)) 210 } 211 212 // check labels using map 213 // 1. the results has no guarantee to show the same order. 214 // 2. the results has no guarantee to show only configured labels. 215 labelsMap, err := strutil.ParseCSVMap(lines[0]) 216 if err != nil { 217 return fmt.Errorf("failed to parse labels: %v", err) 218 } 219 220 for i := range testContainer.labels { 221 if value, ok := labelsMap[i]; ok { 222 assert.Equal(t, value, testContainer.labels[i]) 223 } 224 } 225 return nil 226 }) 227 } 228 229 func TestContainerListWithNames(t *testing.T) { 230 base, testContainer := preparePsTestContainer(t, "listWithNames", true) 231 232 // hope there are no tests running parallel 233 base.Cmd("ps", "-n", "1", "--format", "{{.Names}}").AssertOutWithFunc(func(stdout string) error { 234 235 // An example of nerdctl ps --format "{{.Names}}" 236 lines := strings.Split(strings.TrimSpace(stdout), "\n") 237 if len(lines) != 1 { 238 return fmt.Errorf("expected 1 line, got %d", len(lines)) 239 } 240 241 assert.Equal(t, lines[0], testContainer.name) 242 243 return nil 244 }) 245 } 246 247 func TestContainerListWithFilter(t *testing.T) { 248 base, testContainerA := preparePsTestContainer(t, "listWithFilterA", true) 249 _, testContainerB := preparePsTestContainer(t, "listWithFilterB", true) 250 _, testContainerC := preparePsTestContainer(t, "listWithFilterC", false) 251 252 base.Cmd("ps", "--filter", "name="+testContainerA.name).AssertOutWithFunc(func(stdout string) error { 253 lines := strings.Split(strings.TrimSpace(stdout), "\n") 254 if len(lines) < 2 { 255 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 256 } 257 258 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 259 err := tab.ParseHeader(lines[0]) 260 if err != nil { 261 return fmt.Errorf("failed to parse header: %v", err) 262 } 263 264 containerName, _ := tab.ReadRow(lines[1], "NAMES") 265 assert.Equal(t, containerName, testContainerA.name) 266 id, _ := tab.ReadRow(lines[1], "CONTAINER ID") 267 base.Cmd("ps", "-q", "--filter", "id="+id).AssertOutWithFunc(func(stdout string) error { 268 lines := strings.Split(strings.TrimSpace(stdout), "\n") 269 if len(lines) != 1 { 270 return fmt.Errorf("expected 1 line, got %d", len(lines)) 271 } 272 if lines[0] != id { 273 return errors.New("failed to filter by id") 274 } 275 return nil 276 }) 277 base.Cmd("ps", "-q", "--filter", "id="+id+id).AssertOutWithFunc(func(stdout string) error { 278 lines := strings.Split(strings.TrimSpace(stdout), "\n") 279 if len(lines) > 0 { 280 for _, line := range lines { 281 if line != "" { 282 return fmt.Errorf("unexpected container found: %s", line) 283 } 284 } 285 } 286 return nil 287 }) 288 base.Cmd("ps", "-q", "--filter", "id=").AssertOutWithFunc(func(stdout string) error { 289 lines := strings.Split(strings.TrimSpace(stdout), "\n") 290 if len(lines) > 0 { 291 for _, line := range lines { 292 if line != "" { 293 return fmt.Errorf("unexpected container found: %s", line) 294 } 295 } 296 } 297 return nil 298 }) 299 return nil 300 }) 301 302 base.Cmd("ps", "-q", "--filter", "name="+testContainerA.name+testContainerA.name).AssertOutWithFunc(func(stdout string) error { 303 lines := strings.Split(strings.TrimSpace(stdout), "\n") 304 if len(lines) > 0 { 305 for _, line := range lines { 306 if line != "" { 307 return fmt.Errorf("unexpected container found: %s", line) 308 } 309 } 310 } 311 return nil 312 }) 313 314 base.Cmd("ps", "-q", "--filter", "name=").AssertOutWithFunc(func(stdout string) error { 315 lines := strings.Split(strings.TrimSpace(stdout), "\n") 316 if len(lines) == 0 { 317 return errors.New("expect at least 1 container, got 0") 318 } 319 return nil 320 }) 321 322 base.Cmd("ps", "--filter", "name=listWithFilter").AssertOutWithFunc(func(stdout string) error { 323 lines := strings.Split(strings.TrimSpace(stdout), "\n") 324 if len(lines) < 3 { 325 return fmt.Errorf("expected at least 3 lines, got %d", len(lines)) 326 } 327 328 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 329 err := tab.ParseHeader(lines[0]) 330 if err != nil { 331 return fmt.Errorf("failed to parse header: %v", err) 332 } 333 containerNames := map[string]struct{}{ 334 testContainerA.name: {}, testContainerB.name: {}, 335 } 336 for idx, line := range lines { 337 if idx == 0 { 338 continue 339 } 340 containerName, _ := tab.ReadRow(line, "NAMES") 341 if _, ok := containerNames[containerName]; !ok { 342 return fmt.Errorf("unexpected container %s found", containerName) 343 } 344 } 345 return nil 346 }) 347 348 // docker filter by id only support full ID no truncate 349 // https://github.com/docker/for-linux/issues/258 350 // yet nerdctl also support truncate ID 351 base.Cmd("ps", "--no-trunc", "--filter", "since="+testContainerA.name).AssertOutWithFunc(func(stdout string) error { 352 lines := strings.Split(strings.TrimSpace(stdout), "\n") 353 if len(lines) < 2 { 354 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 355 } 356 357 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 358 err := tab.ParseHeader(lines[0]) 359 if err != nil { 360 return fmt.Errorf("failed to parse header: %v", err) 361 } 362 var id string 363 for idx, line := range lines { 364 if idx == 0 { 365 continue 366 } 367 containerName, _ := tab.ReadRow(line, "NAMES") 368 if containerName != testContainerB.name { 369 return fmt.Errorf("unexpected container %s found", containerName) 370 } 371 id, _ = tab.ReadRow(line, "CONTAINER ID") 372 } 373 base.Cmd("ps", "--filter", "before="+id).AssertOutWithFunc(func(stdout string) error { 374 lines := strings.Split(strings.TrimSpace(stdout), "\n") 375 if len(lines) < 2 { 376 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 377 } 378 379 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 380 err := tab.ParseHeader(lines[0]) 381 if err != nil { 382 return fmt.Errorf("failed to parse header: %v", err) 383 } 384 foundA := false 385 for idx, line := range lines { 386 if idx == 0 { 387 continue 388 } 389 containerName, _ := tab.ReadRow(line, "NAMES") 390 if containerName == testContainerA.name { 391 foundA = true 392 break 393 } 394 } 395 // there are other containers such as **wordpress** could be listed since 396 // their created times are ahead of testContainerB too 397 if !foundA { 398 return fmt.Errorf("expected container %s not found", testContainerA.name) 399 } 400 return nil 401 }) 402 return nil 403 }) 404 405 // docker filter by id only support full ID no truncate 406 // https://github.com/docker/for-linux/issues/258 407 // yet nerdctl also support truncate ID 408 base.Cmd("ps", "--no-trunc", "--filter", "before="+testContainerB.name).AssertOutWithFunc(func(stdout string) error { 409 lines := strings.Split(strings.TrimSpace(stdout), "\n") 410 if len(lines) < 2 { 411 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 412 } 413 414 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 415 err := tab.ParseHeader(lines[0]) 416 if err != nil { 417 return fmt.Errorf("failed to parse header: %v", err) 418 } 419 foundA := false 420 var id string 421 for idx, line := range lines { 422 if idx == 0 { 423 continue 424 } 425 containerName, _ := tab.ReadRow(line, "NAMES") 426 if containerName == testContainerA.name { 427 foundA = true 428 id, _ = tab.ReadRow(line, "CONTAINER ID") 429 break 430 } 431 } 432 // there are other containers such as **wordpress** could be listed since 433 // their created times are ahead of testContainerB too 434 if !foundA { 435 return fmt.Errorf("expected container %s not found", testContainerA.name) 436 } 437 base.Cmd("ps", "--filter", "since="+id).AssertOutWithFunc(func(stdout string) error { 438 lines := strings.Split(strings.TrimSpace(stdout), "\n") 439 if len(lines) < 2 { 440 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 441 } 442 443 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 444 err := tab.ParseHeader(lines[0]) 445 if err != nil { 446 return fmt.Errorf("failed to parse header: %v", err) 447 } 448 for idx, line := range lines { 449 if idx == 0 { 450 continue 451 } 452 containerName, _ := tab.ReadRow(line, "NAMES") 453 if containerName != testContainerB.name { 454 return fmt.Errorf("unexpected container %s found", containerName) 455 } 456 } 457 return nil 458 }) 459 return nil 460 }) 461 462 for _, testContainer := range []psTestContainer{testContainerA, testContainerB} { 463 for _, volume := range testContainer.volumes { 464 base.Cmd("ps", "--filter", "volume="+volume).AssertOutWithFunc(func(stdout string) error { 465 lines := strings.Split(strings.TrimSpace(stdout), "\n") 466 if len(lines) < 2 { 467 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 468 } 469 470 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 471 err := tab.ParseHeader(lines[0]) 472 if err != nil { 473 return fmt.Errorf("failed to parse header: %v", err) 474 } 475 containerName, _ := tab.ReadRow(lines[1], "NAMES") 476 assert.Equal(t, containerName, testContainer.name) 477 return nil 478 }) 479 } 480 } 481 482 base.Cmd("ps", "--filter", "network="+testContainerA.network).AssertOutWithFunc(func(stdout string) error { 483 lines := strings.Split(strings.TrimSpace(stdout), "\n") 484 if len(lines) < 2 { 485 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 486 } 487 488 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 489 err := tab.ParseHeader(lines[0]) 490 if err != nil { 491 return fmt.Errorf("failed to parse header: %v", err) 492 } 493 containerName, _ := tab.ReadRow(lines[1], "NAMES") 494 assert.Equal(t, containerName, testContainerA.name) 495 return nil 496 }) 497 498 for key, value := range testContainerB.labels { 499 base.Cmd("ps", "--filter", "label="+key+"="+value).AssertOutWithFunc(func(stdout string) error { 500 lines := strings.Split(strings.TrimSpace(stdout), "\n") 501 if len(lines) < 2 { 502 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 503 } 504 505 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 506 err := tab.ParseHeader(lines[0]) 507 if err != nil { 508 return fmt.Errorf("failed to parse header: %v", err) 509 } 510 containerNames := map[string]struct{}{ 511 testContainerB.name: {}, 512 } 513 for idx, line := range lines { 514 if idx == 0 { 515 continue 516 } 517 containerName, _ := tab.ReadRow(line, "NAMES") 518 if _, ok := containerNames[containerName]; !ok { 519 return fmt.Errorf("unexpected container %s found", containerName) 520 } 521 } 522 return nil 523 }) 524 } 525 526 base.Cmd("ps", "-a", "--filter", "exited=1").AssertOutWithFunc(func(stdout string) error { 527 lines := strings.Split(strings.TrimSpace(stdout), "\n") 528 if len(lines) < 2 { 529 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 530 } 531 532 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 533 err := tab.ParseHeader(lines[0]) 534 if err != nil { 535 return fmt.Errorf("failed to parse header: %v", err) 536 } 537 containerNames := map[string]struct{}{ 538 testContainerC.name: {}, 539 } 540 for idx, line := range lines { 541 if idx == 0 { 542 continue 543 } 544 containerName, _ := tab.ReadRow(line, "NAMES") 545 if _, ok := containerNames[containerName]; !ok { 546 return fmt.Errorf("unexpected container %s found", containerName) 547 } 548 } 549 return nil 550 }) 551 552 base.Cmd("ps", "-a", "--filter", "status=exited").AssertOutWithFunc(func(stdout string) error { 553 lines := strings.Split(strings.TrimSpace(stdout), "\n") 554 if len(lines) < 2 { 555 return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) 556 } 557 558 tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 559 err := tab.ParseHeader(lines[0]) 560 if err != nil { 561 return fmt.Errorf("failed to parse header: %v", err) 562 } 563 containerNames := map[string]struct{}{ 564 testContainerC.name: {}, 565 } 566 for idx, line := range lines { 567 if idx == 0 { 568 continue 569 } 570 containerName, _ := tab.ReadRow(line, "NAMES") 571 if _, ok := containerNames[containerName]; !ok { 572 return fmt.Errorf("unexpected container %s found", containerName) 573 } 574 } 575 return nil 576 }) 577 }