github.com/moby/docker@v26.1.3+incompatible/integration/container/create_test.go (about) 1 package container // import "github.com/docker/docker/integration/container" 2 3 import ( 4 "bufio" 5 "context" 6 "encoding/json" 7 "fmt" 8 "strconv" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/docker/docker/api/types/container" 14 "github.com/docker/docker/api/types/network" 15 "github.com/docker/docker/api/types/versions" 16 "github.com/docker/docker/client" 17 "github.com/docker/docker/errdefs" 18 ctr "github.com/docker/docker/integration/internal/container" 19 net "github.com/docker/docker/integration/internal/network" 20 "github.com/docker/docker/oci" 21 "github.com/docker/docker/testutil" 22 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 23 "gotest.tools/v3/assert" 24 is "gotest.tools/v3/assert/cmp" 25 "gotest.tools/v3/poll" 26 "gotest.tools/v3/skip" 27 ) 28 29 func TestCreateFailsWhenIdentifierDoesNotExist(t *testing.T) { 30 ctx := setupTest(t) 31 client := testEnv.APIClient() 32 33 testCases := []struct { 34 doc string 35 image string 36 expectedError string 37 }{ 38 { 39 doc: "image and tag", 40 image: "test456:v1", 41 expectedError: "No such image: test456:v1", 42 }, 43 { 44 doc: "image no tag", 45 image: "test456", 46 expectedError: "No such image: test456", 47 }, 48 { 49 doc: "digest", 50 image: "sha256:0cb40641836c461bc97c793971d84d758371ed682042457523e4ae701efeaaaa", 51 expectedError: "No such image: sha256:0cb40641836c461bc97c793971d84d758371ed682042457523e4ae701efeaaaa", 52 }, 53 } 54 55 for _, tc := range testCases { 56 tc := tc 57 t.Run(tc.doc, func(t *testing.T) { 58 t.Parallel() 59 ctx := testutil.StartSpan(ctx, t) 60 _, err := client.ContainerCreate(ctx, 61 &container.Config{Image: tc.image}, 62 &container.HostConfig{}, 63 &network.NetworkingConfig{}, 64 nil, 65 "", 66 ) 67 assert.Check(t, is.ErrorContains(err, tc.expectedError)) 68 assert.Check(t, errdefs.IsNotFound(err)) 69 }) 70 } 71 } 72 73 // TestCreateLinkToNonExistingContainer verifies that linking to a non-existing 74 // container returns an "invalid parameter" (400) status, and not the underlying 75 // "non exists" (404). 76 func TestCreateLinkToNonExistingContainer(t *testing.T) { 77 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "legacy links are not supported on windows") 78 ctx := setupTest(t) 79 c := testEnv.APIClient() 80 81 _, err := c.ContainerCreate(ctx, 82 &container.Config{ 83 Image: "busybox", 84 }, 85 &container.HostConfig{ 86 Links: []string{"no-such-container"}, 87 }, 88 &network.NetworkingConfig{}, 89 nil, 90 "", 91 ) 92 assert.Check(t, is.ErrorContains(err, "could not get container for no-such-container")) 93 assert.Check(t, errdefs.IsInvalidParameter(err)) 94 } 95 96 func TestCreateWithInvalidEnv(t *testing.T) { 97 ctx := setupTest(t) 98 client := testEnv.APIClient() 99 100 testCases := []struct { 101 env string 102 expectedError string 103 }{ 104 { 105 env: "", 106 expectedError: "invalid environment variable:", 107 }, 108 { 109 env: "=", 110 expectedError: "invalid environment variable: =", 111 }, 112 { 113 env: "=foo", 114 expectedError: "invalid environment variable: =foo", 115 }, 116 } 117 118 for index, tc := range testCases { 119 tc := tc 120 t.Run(strconv.Itoa(index), func(t *testing.T) { 121 t.Parallel() 122 ctx := testutil.StartSpan(ctx, t) 123 _, err := client.ContainerCreate(ctx, 124 &container.Config{ 125 Image: "busybox", 126 Env: []string{tc.env}, 127 }, 128 &container.HostConfig{}, 129 &network.NetworkingConfig{}, 130 nil, 131 "", 132 ) 133 assert.Check(t, is.ErrorContains(err, tc.expectedError)) 134 assert.Check(t, errdefs.IsInvalidParameter(err)) 135 }) 136 } 137 } 138 139 // Test case for #30166 (target was not validated) 140 func TestCreateTmpfsMountsTarget(t *testing.T) { 141 skip.If(t, testEnv.DaemonInfo.OSType == "windows") 142 ctx := setupTest(t) 143 144 client := testEnv.APIClient() 145 146 testCases := []struct { 147 target string 148 expectedError string 149 }{ 150 { 151 target: ".", 152 expectedError: "mount path must be absolute", 153 }, 154 { 155 target: "foo", 156 expectedError: "mount path must be absolute", 157 }, 158 { 159 target: "/", 160 expectedError: "destination can't be '/'", 161 }, 162 { 163 target: "//", 164 expectedError: "destination can't be '/'", 165 }, 166 } 167 168 for _, tc := range testCases { 169 _, err := client.ContainerCreate(ctx, 170 &container.Config{ 171 Image: "busybox", 172 }, 173 &container.HostConfig{ 174 Tmpfs: map[string]string{tc.target: ""}, 175 }, 176 &network.NetworkingConfig{}, 177 nil, 178 "", 179 ) 180 assert.Check(t, is.ErrorContains(err, tc.expectedError)) 181 assert.Check(t, errdefs.IsInvalidParameter(err)) 182 } 183 } 184 185 func TestCreateWithCustomMaskedPaths(t *testing.T) { 186 skip.If(t, testEnv.DaemonInfo.OSType != "linux") 187 188 ctx := setupTest(t) 189 apiClient := testEnv.APIClient() 190 191 testCases := []struct { 192 maskedPaths []string 193 expected []string 194 }{ 195 { 196 maskedPaths: []string{}, 197 expected: []string{}, 198 }, 199 { 200 maskedPaths: nil, 201 expected: oci.DefaultSpec().Linux.MaskedPaths, 202 }, 203 { 204 maskedPaths: []string{"/proc/kcore", "/proc/keys"}, 205 expected: []string{"/proc/kcore", "/proc/keys"}, 206 }, 207 } 208 209 checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) { 210 _, b, err := apiClient.ContainerInspectWithRaw(ctx, name, false) 211 assert.NilError(t, err) 212 213 var inspectJSON map[string]interface{} 214 err = json.Unmarshal(b, &inspectJSON) 215 assert.NilError(t, err) 216 217 cfg, ok := inspectJSON["HostConfig"].(map[string]interface{}) 218 assert.Check(t, is.Equal(true, ok), name) 219 220 maskedPaths, ok := cfg["MaskedPaths"].([]interface{}) 221 assert.Check(t, is.Equal(true, ok), name) 222 223 mps := []string{} 224 for _, mp := range maskedPaths { 225 mps = append(mps, mp.(string)) 226 } 227 228 assert.DeepEqual(t, expected, mps) 229 } 230 231 // TODO: This should be using subtests 232 233 for i, tc := range testCases { 234 name := fmt.Sprintf("create-masked-paths-%d", i) 235 config := container.Config{ 236 Image: "busybox", 237 Cmd: []string{"true"}, 238 } 239 hc := container.HostConfig{} 240 if tc.maskedPaths != nil { 241 hc.MaskedPaths = tc.maskedPaths 242 } 243 244 // Create the container. 245 c, err := apiClient.ContainerCreate(ctx, 246 &config, 247 &hc, 248 &network.NetworkingConfig{}, 249 nil, 250 name, 251 ) 252 assert.NilError(t, err) 253 254 checkInspect(t, ctx, name, tc.expected) 255 256 // Start the container. 257 err = apiClient.ContainerStart(ctx, c.ID, container.StartOptions{}) 258 assert.NilError(t, err) 259 260 poll.WaitOn(t, ctr.IsInState(ctx, apiClient, c.ID, "exited"), poll.WithDelay(100*time.Millisecond)) 261 262 checkInspect(t, ctx, name, tc.expected) 263 } 264 } 265 266 func TestCreateWithCustomReadonlyPaths(t *testing.T) { 267 skip.If(t, testEnv.DaemonInfo.OSType != "linux") 268 269 ctx := setupTest(t) 270 apiClient := testEnv.APIClient() 271 272 testCases := []struct { 273 readonlyPaths []string 274 expected []string 275 }{ 276 { 277 readonlyPaths: []string{}, 278 expected: []string{}, 279 }, 280 { 281 readonlyPaths: nil, 282 expected: oci.DefaultSpec().Linux.ReadonlyPaths, 283 }, 284 { 285 readonlyPaths: []string{"/proc/asound", "/proc/bus"}, 286 expected: []string{"/proc/asound", "/proc/bus"}, 287 }, 288 } 289 290 checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) { 291 _, b, err := apiClient.ContainerInspectWithRaw(ctx, name, false) 292 assert.NilError(t, err) 293 294 var inspectJSON map[string]interface{} 295 err = json.Unmarshal(b, &inspectJSON) 296 assert.NilError(t, err) 297 298 cfg, ok := inspectJSON["HostConfig"].(map[string]interface{}) 299 assert.Check(t, is.Equal(true, ok), name) 300 301 readonlyPaths, ok := cfg["ReadonlyPaths"].([]interface{}) 302 assert.Check(t, is.Equal(true, ok), name) 303 304 rops := []string{} 305 for _, rop := range readonlyPaths { 306 rops = append(rops, rop.(string)) 307 } 308 assert.DeepEqual(t, expected, rops) 309 } 310 311 for i, tc := range testCases { 312 name := fmt.Sprintf("create-readonly-paths-%d", i) 313 config := container.Config{ 314 Image: "busybox", 315 Cmd: []string{"true"}, 316 } 317 hc := container.HostConfig{} 318 if tc.readonlyPaths != nil { 319 hc.ReadonlyPaths = tc.readonlyPaths 320 } 321 322 // Create the container. 323 c, err := apiClient.ContainerCreate(ctx, 324 &config, 325 &hc, 326 &network.NetworkingConfig{}, 327 nil, 328 name, 329 ) 330 assert.NilError(t, err) 331 332 checkInspect(t, ctx, name, tc.expected) 333 334 // Start the container. 335 err = apiClient.ContainerStart(ctx, c.ID, container.StartOptions{}) 336 assert.NilError(t, err) 337 338 poll.WaitOn(t, ctr.IsInState(ctx, apiClient, c.ID, "exited"), poll.WithDelay(100*time.Millisecond)) 339 340 checkInspect(t, ctx, name, tc.expected) 341 } 342 } 343 344 func TestCreateWithInvalidHealthcheckParams(t *testing.T) { 345 ctx := setupTest(t) 346 apiClient := testEnv.APIClient() 347 348 testCases := []struct { 349 doc string 350 interval time.Duration 351 timeout time.Duration 352 retries int 353 startPeriod time.Duration 354 startInterval time.Duration 355 expectedErr string 356 }{ 357 { 358 doc: "test invalid Interval in Healthcheck: less than 0s", 359 interval: -10 * time.Millisecond, 360 timeout: time.Second, 361 retries: 1000, 362 expectedErr: fmt.Sprintf("Interval in Healthcheck cannot be less than %s", container.MinimumDuration), 363 }, 364 { 365 doc: "test invalid Interval in Healthcheck: larger than 0s but less than 1ms", 366 interval: 500 * time.Microsecond, 367 timeout: time.Second, 368 retries: 1000, 369 expectedErr: fmt.Sprintf("Interval in Healthcheck cannot be less than %s", container.MinimumDuration), 370 }, 371 { 372 doc: "test invalid Timeout in Healthcheck: less than 1ms", 373 interval: time.Second, 374 timeout: -100 * time.Millisecond, 375 retries: 1000, 376 expectedErr: fmt.Sprintf("Timeout in Healthcheck cannot be less than %s", container.MinimumDuration), 377 }, 378 { 379 doc: "test invalid Retries in Healthcheck: less than 0", 380 interval: time.Second, 381 timeout: time.Second, 382 retries: -10, 383 expectedErr: "Retries in Healthcheck cannot be negative", 384 }, 385 { 386 doc: "test invalid StartPeriod in Healthcheck: not 0 and less than 1ms", 387 interval: time.Second, 388 timeout: time.Second, 389 retries: 1000, 390 startPeriod: 100 * time.Microsecond, 391 expectedErr: fmt.Sprintf("StartPeriod in Healthcheck cannot be less than %s", container.MinimumDuration), 392 }, 393 { 394 doc: "test invalid StartInterval in Healthcheck: not 0 and less than 1ms", 395 interval: time.Second, 396 timeout: time.Second, 397 retries: 1000, 398 startPeriod: time.Second, 399 startInterval: 100 * time.Microsecond, 400 expectedErr: fmt.Sprintf("StartInterval in Healthcheck cannot be less than %s", container.MinimumDuration), 401 }, 402 } 403 404 for _, tc := range testCases { 405 tc := tc 406 t.Run(tc.doc, func(t *testing.T) { 407 t.Parallel() 408 ctx := testutil.StartSpan(ctx, t) 409 cfg := container.Config{ 410 Image: "busybox", 411 Healthcheck: &container.HealthConfig{ 412 Interval: tc.interval, 413 Timeout: tc.timeout, 414 Retries: tc.retries, 415 StartInterval: tc.startInterval, 416 }, 417 } 418 if tc.startPeriod != 0 { 419 cfg.Healthcheck.StartPeriod = tc.startPeriod 420 } 421 422 resp, err := apiClient.ContainerCreate(ctx, &cfg, &container.HostConfig{}, nil, nil, "") 423 assert.Check(t, is.Equal(len(resp.Warnings), 0)) 424 assert.Check(t, errdefs.IsInvalidParameter(err)) 425 assert.ErrorContains(t, err, tc.expectedErr) 426 }) 427 } 428 } 429 430 // Make sure that anonymous volumes can be overritten by tmpfs 431 // https://github.com/moby/moby/issues/40446 432 func TestCreateTmpfsOverrideAnonymousVolume(t *testing.T) { 433 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "windows does not support tmpfs") 434 ctx := setupTest(t) 435 apiClient := testEnv.APIClient() 436 437 id := ctr.Create(ctx, t, apiClient, 438 ctr.WithVolume("/foo"), 439 ctr.WithTmpfs("/foo"), 440 ctr.WithVolume("/bar"), 441 ctr.WithTmpfs("/bar:size=999"), 442 ctr.WithCmd("/bin/sh", "-c", "mount | grep '/foo' | grep tmpfs && mount | grep '/bar' | grep tmpfs"), 443 ) 444 445 defer func() { 446 err := apiClient.ContainerRemove(ctx, id, container.RemoveOptions{Force: true}) 447 assert.NilError(t, err) 448 }() 449 450 inspect, err := apiClient.ContainerInspect(ctx, id) 451 assert.NilError(t, err) 452 // tmpfs do not currently get added to inspect.Mounts 453 // Normally an anonymous volume would, except now tmpfs should prevent that. 454 assert.Assert(t, is.Len(inspect.Mounts, 0)) 455 456 chWait, chErr := apiClient.ContainerWait(ctx, id, container.WaitConditionNextExit) 457 assert.NilError(t, apiClient.ContainerStart(ctx, id, container.StartOptions{})) 458 459 timeout := time.NewTimer(30 * time.Second) 460 defer timeout.Stop() 461 462 select { 463 case <-timeout.C: 464 t.Fatal("timeout waiting for container to exit") 465 case status := <-chWait: 466 var errMsg string 467 if status.Error != nil { 468 errMsg = status.Error.Message 469 } 470 assert.Equal(t, int(status.StatusCode), 0, errMsg) 471 case err := <-chErr: 472 assert.NilError(t, err) 473 } 474 } 475 476 // Test that if the referenced image platform does not match the requested platform on container create that we get an 477 // error. 478 func TestCreateDifferentPlatform(t *testing.T) { 479 ctx := setupTest(t) 480 apiClient := testEnv.APIClient() 481 482 img, _, err := apiClient.ImageInspectWithRaw(ctx, "busybox:latest") 483 assert.NilError(t, err) 484 assert.Assert(t, img.Architecture != "") 485 486 t.Run("different os", func(t *testing.T) { 487 ctx := testutil.StartSpan(ctx, t) 488 p := ocispec.Platform{ 489 OS: img.Os + "DifferentOS", 490 Architecture: img.Architecture, 491 Variant: img.Variant, 492 } 493 _, err := apiClient.ContainerCreate(ctx, &container.Config{Image: "busybox:latest"}, &container.HostConfig{}, nil, &p, "") 494 assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) 495 }) 496 t.Run("different cpu arch", func(t *testing.T) { 497 ctx := testutil.StartSpan(ctx, t) 498 p := ocispec.Platform{ 499 OS: img.Os, 500 Architecture: img.Architecture + "DifferentArch", 501 Variant: img.Variant, 502 } 503 _, err := apiClient.ContainerCreate(ctx, &container.Config{Image: "busybox:latest"}, &container.HostConfig{}, nil, &p, "") 504 assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) 505 }) 506 } 507 508 func TestCreateVolumesFromNonExistingContainer(t *testing.T) { 509 ctx := setupTest(t) 510 cli := testEnv.APIClient() 511 512 _, err := cli.ContainerCreate( 513 ctx, 514 &container.Config{Image: "busybox"}, 515 &container.HostConfig{VolumesFrom: []string{"nosuchcontainer"}}, 516 nil, 517 nil, 518 "", 519 ) 520 assert.Check(t, errdefs.IsInvalidParameter(err)) 521 } 522 523 // Test that we can create a container from an image that is for a different platform even if a platform was not specified 524 // This is for the regression detailed here: https://github.com/moby/moby/issues/41552 525 func TestCreatePlatformSpecificImageNoPlatform(t *testing.T) { 526 ctx := setupTest(t) 527 528 skip.If(t, testEnv.DaemonInfo.Architecture == "arm", "test only makes sense to run on non-arm systems") 529 skip.If(t, testEnv.DaemonInfo.OSType != "linux", "test image is only available on linux") 530 cli := testEnv.APIClient() 531 532 _, err := cli.ContainerCreate( 533 ctx, 534 &container.Config{Image: "arm32v7/hello-world"}, 535 &container.HostConfig{}, 536 nil, 537 nil, 538 "", 539 ) 540 assert.NilError(t, err) 541 } 542 543 func TestCreateInvalidHostConfig(t *testing.T) { 544 skip.If(t, testEnv.DaemonInfo.OSType == "windows") 545 546 ctx := setupTest(t) 547 apiClient := testEnv.APIClient() 548 549 testCases := []struct { 550 doc string 551 hc container.HostConfig 552 expectedErr string 553 }{ 554 { 555 doc: "invalid IpcMode", 556 hc: container.HostConfig{IpcMode: "invalid"}, 557 expectedErr: "Error response from daemon: invalid IPC mode: invalid", 558 }, 559 { 560 doc: "invalid PidMode", 561 hc: container.HostConfig{PidMode: "invalid"}, 562 expectedErr: "Error response from daemon: invalid PID mode: invalid", 563 }, 564 { 565 doc: "invalid PidMode without container ID", 566 hc: container.HostConfig{PidMode: "container"}, 567 expectedErr: "Error response from daemon: invalid PID mode: container", 568 }, 569 { 570 doc: "invalid UTSMode", 571 hc: container.HostConfig{UTSMode: "invalid"}, 572 expectedErr: "Error response from daemon: invalid UTS mode: invalid", 573 }, 574 { 575 doc: "invalid Annotations", 576 hc: container.HostConfig{Annotations: map[string]string{"": "a"}}, 577 expectedErr: "Error response from daemon: invalid Annotations: the empty string is not permitted as an annotation key", 578 }, 579 } 580 581 for _, tc := range testCases { 582 tc := tc 583 t.Run(tc.doc, func(t *testing.T) { 584 t.Parallel() 585 ctx := testutil.StartSpan(ctx, t) 586 cfg := container.Config{ 587 Image: "busybox", 588 } 589 resp, err := apiClient.ContainerCreate(ctx, &cfg, &tc.hc, nil, nil, "") 590 assert.Check(t, is.Equal(len(resp.Warnings), 0)) 591 assert.Check(t, errdefs.IsInvalidParameter(err), "got: %T", err) 592 assert.Error(t, err, tc.expectedErr) 593 }) 594 } 595 } 596 597 func TestCreateWithMultipleEndpointSettings(t *testing.T) { 598 ctx := setupTest(t) 599 600 testcases := []struct { 601 apiVersion string 602 expectedErr string 603 }{ 604 {apiVersion: "1.44"}, 605 {apiVersion: "1.43", expectedErr: "Container cannot be created with multiple network endpoints"}, 606 } 607 608 for _, tc := range testcases { 609 t.Run("with API v"+tc.apiVersion, func(t *testing.T) { 610 apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(tc.apiVersion)) 611 assert.NilError(t, err) 612 613 config := container.Config{ 614 Image: "busybox", 615 } 616 networkingConfig := network.NetworkingConfig{ 617 EndpointsConfig: map[string]*network.EndpointSettings{ 618 "net1": {}, 619 "net2": {}, 620 "net3": {}, 621 }, 622 } 623 _, err = apiClient.ContainerCreate(ctx, &config, &container.HostConfig{}, &networkingConfig, nil, "") 624 if tc.expectedErr == "" { 625 assert.NilError(t, err) 626 } else { 627 assert.ErrorContains(t, err, tc.expectedErr) 628 } 629 }) 630 } 631 } 632 633 func TestCreateWithCustomMACs(t *testing.T) { 634 skip.If(t, testEnv.DaemonInfo.OSType == "windows") 635 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44") 636 637 ctx := setupTest(t) 638 apiClient := testEnv.APIClient() 639 640 net.CreateNoError(ctx, t, apiClient, "testnet") 641 642 attachCtx, cancel := context.WithTimeout(ctx, 1*time.Second) 643 defer cancel() 644 res := ctr.RunAttach(attachCtx, t, apiClient, 645 ctr.WithCmd("ip", "-o", "link", "show"), 646 ctr.WithNetworkMode("bridge"), 647 ctr.WithMacAddress("bridge", "02:32:1c:23:00:04")) 648 649 assert.Equal(t, res.ExitCode, 0) 650 assert.Equal(t, res.Stderr.String(), "") 651 652 scanner := bufio.NewScanner(res.Stdout) 653 for scanner.Scan() { 654 fields := strings.Fields(scanner.Text()) 655 // The expected output is: 656 // 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000\ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 657 // 134: eth0@if135: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1400 qdisc noqueue \ link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff 658 if len(fields) < 11 { 659 continue 660 } 661 662 ifaceName := fields[1] 663 if ifaceName[:3] != "eth" { 664 continue 665 } 666 667 mac := fields[len(fields)-3] 668 assert.Equal(t, mac, "02:32:1c:23:00:04") 669 } 670 }