github.com/quite/nomad@v0.8.6/client/driver/docker_test.go (about) 1 package driver 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "math/rand" 7 "os" 8 "path/filepath" 9 "reflect" 10 "runtime" 11 "runtime/debug" 12 "sort" 13 "strconv" 14 "strings" 15 "testing" 16 "time" 17 18 docker "github.com/fsouza/go-dockerclient" 19 "github.com/hashicorp/consul/lib/freeport" 20 sockaddr "github.com/hashicorp/go-sockaddr" 21 "github.com/hashicorp/nomad/client/allocdir" 22 "github.com/hashicorp/nomad/client/config" 23 "github.com/hashicorp/nomad/client/driver/env" 24 "github.com/hashicorp/nomad/client/fingerprint" 25 cstructs "github.com/hashicorp/nomad/client/structs" 26 "github.com/hashicorp/nomad/client/testutil" 27 "github.com/hashicorp/nomad/helper/uuid" 28 "github.com/hashicorp/nomad/nomad/mock" 29 "github.com/hashicorp/nomad/nomad/structs" 30 tu "github.com/hashicorp/nomad/testutil" 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/require" 33 ) 34 35 func dockerIsRemote(t *testing.T) bool { 36 client, err := docker.NewClientFromEnv() 37 if err != nil { 38 return false 39 } 40 41 // Technically this could be a local tcp socket but for testing purposes 42 // we'll just assume that tcp is only used for remote connections. 43 if client.Endpoint()[0:3] == "tcp" { 44 return true 45 } 46 return false 47 } 48 49 // Returns a task with a reserved and dynamic port. The ports are returned 50 // respectively. 51 func dockerTask(t *testing.T) (*structs.Task, int, int) { 52 ports := freeport.GetT(t, 2) 53 dockerReserved := ports[0] 54 dockerDynamic := ports[1] 55 return &structs.Task{ 56 Name: "redis-demo", 57 Driver: "docker", 58 Config: map[string]interface{}{ 59 "image": "busybox", 60 "load": "busybox.tar", 61 "command": "/bin/nc", 62 "args": []string{"-l", "127.0.0.1", "-p", "0"}, 63 }, 64 LogConfig: &structs.LogConfig{ 65 MaxFiles: 10, 66 MaxFileSizeMB: 10, 67 }, 68 Resources: &structs.Resources{ 69 MemoryMB: 256, 70 CPU: 512, 71 Networks: []*structs.NetworkResource{ 72 { 73 IP: "127.0.0.1", 74 ReservedPorts: []structs.Port{{Label: "main", Value: dockerReserved}}, 75 DynamicPorts: []structs.Port{{Label: "REDIS", Value: dockerDynamic}}, 76 }, 77 }, 78 }, 79 }, dockerReserved, dockerDynamic 80 } 81 82 // dockerSetup does all of the basic setup you need to get a running docker 83 // process up and running for testing. Use like: 84 // 85 // task := taskTemplate() 86 // // do custom task configuration 87 // client, handle, cleanup := dockerSetup(t, task) 88 // defer cleanup() 89 // // do test stuff 90 // 91 // If there is a problem during setup this function will abort or skip the test 92 // and indicate the reason. 93 func dockerSetup(t *testing.T, task *structs.Task) (*docker.Client, *DockerHandle, func()) { 94 client := newTestDockerClient(t) 95 return dockerSetupWithClient(t, task, client) 96 } 97 98 func testDockerDriverContexts(t *testing.T, task *structs.Task) *testContext { 99 tctx := testDriverContexts(t, task) 100 101 // Drop the delay 102 tctx.DriverCtx.config.Options = make(map[string]string) 103 tctx.DriverCtx.config.Options[dockerImageRemoveDelayConfigOption] = "1s" 104 105 return tctx 106 } 107 108 func dockerSetupWithClient(t *testing.T, task *structs.Task, client *docker.Client) (*docker.Client, *DockerHandle, func()) { 109 t.Helper() 110 tctx := testDockerDriverContexts(t, task) 111 driver := NewDockerDriver(tctx.DriverCtx) 112 copyImage(t, tctx.ExecCtx.TaskDir, "busybox.tar") 113 114 presp, err := driver.Prestart(tctx.ExecCtx, task) 115 if err != nil { 116 if presp != nil && presp.CreatedResources != nil { 117 driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 118 } 119 tctx.AllocDir.Destroy() 120 t.Fatalf("error in prestart: %v", err) 121 } 122 // Update the exec ctx with the driver network env vars 123 tctx.ExecCtx.TaskEnv = tctx.EnvBuilder.SetDriverNetwork(presp.Network).Build() 124 125 sresp, err := driver.Start(tctx.ExecCtx, task) 126 if err != nil { 127 driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 128 tctx.AllocDir.Destroy() 129 t.Fatalf("Failed to start driver: %s\nStack\n%s", err, debug.Stack()) 130 } 131 132 if sresp.Handle == nil { 133 driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 134 tctx.AllocDir.Destroy() 135 t.Fatalf("handle is nil\nStack\n%s", debug.Stack()) 136 } 137 138 // At runtime this is handled by TaskRunner 139 tctx.ExecCtx.TaskEnv = tctx.EnvBuilder.SetDriverNetwork(sresp.Network).Build() 140 141 cleanup := func() { 142 driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 143 sresp.Handle.Kill() 144 tctx.AllocDir.Destroy() 145 } 146 147 return client, sresp.Handle.(*DockerHandle), cleanup 148 } 149 150 func newTestDockerClient(t *testing.T) *docker.Client { 151 t.Helper() 152 if !testutil.DockerIsConnected(t) { 153 t.Skip("Docker not connected") 154 } 155 156 client, err := docker.NewClientFromEnv() 157 if err != nil { 158 t.Fatalf("Failed to initialize client: %s\nStack\n%s", err, debug.Stack()) 159 } 160 return client 161 } 162 163 // This test should always pass, even if docker daemon is not available 164 func TestDockerDriver_Fingerprint(t *testing.T) { 165 if !tu.IsTravis() { 166 t.Parallel() 167 } 168 169 ctx := testDockerDriverContexts(t, &structs.Task{Name: "foo", Driver: "docker", Resources: basicResources}) 170 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 171 defer ctx.AllocDir.Destroy() 172 d := NewDockerDriver(ctx.DriverCtx) 173 node := &structs.Node{ 174 Attributes: make(map[string]string), 175 } 176 177 request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} 178 var response cstructs.FingerprintResponse 179 err := d.Fingerprint(request, &response) 180 if err != nil { 181 t.Fatalf("err: %v", err) 182 } 183 184 attributes := response.Attributes 185 if testutil.DockerIsConnected(t) && attributes["driver.docker"] == "" { 186 t.Fatalf("Fingerprinter should detect when docker is available") 187 } 188 189 if attributes["driver.docker"] != "1" { 190 t.Log("Docker daemon not available. The remainder of the docker tests will be skipped.") 191 } else { 192 193 // if docker is available, make sure that the response is tagged as 194 // applicable 195 if !response.Detected { 196 t.Fatalf("expected response to be applicable") 197 } 198 } 199 200 t.Logf("Found docker version %s", attributes["driver.docker.version"]) 201 } 202 203 // TestDockerDriver_Fingerprint_Bridge asserts that if Docker is running we set 204 // the bridge network's IP as a node attribute. See #2785 205 func TestDockerDriver_Fingerprint_Bridge(t *testing.T) { 206 if !tu.IsTravis() { 207 t.Parallel() 208 } 209 if !testutil.DockerIsConnected(t) { 210 t.Skip("requires Docker") 211 } 212 if runtime.GOOS != "linux" { 213 t.Skip("expect only on linux") 214 } 215 216 // This seems fragile, so we might need to reconsider this test if it 217 // proves flaky 218 expectedAddr, err := sockaddr.GetInterfaceIP("docker0") 219 if err != nil { 220 t.Fatalf("unable to get ip for docker0: %v", err) 221 } 222 if expectedAddr == "" { 223 t.Fatalf("unable to get ip for docker bridge") 224 } 225 226 conf := testConfig(t) 227 conf.Node = mock.Node() 228 dd := NewDockerDriver(NewDriverContext("", "", "", "", conf, conf.Node, testLogger(), nil)) 229 230 request := &cstructs.FingerprintRequest{Config: conf, Node: conf.Node} 231 var response cstructs.FingerprintResponse 232 233 err = dd.Fingerprint(request, &response) 234 if err != nil { 235 t.Fatalf("error fingerprinting docker: %v", err) 236 } 237 238 if !response.Detected { 239 t.Fatalf("expected response to be applicable") 240 } 241 242 attributes := response.Attributes 243 if attributes == nil { 244 t.Fatalf("expected attributes to be set") 245 } 246 247 if attributes["driver.docker"] == "" { 248 t.Fatalf("expected Docker to be enabled but false was returned") 249 } 250 251 if found := attributes["driver.docker.bridge_ip"]; found != expectedAddr { 252 t.Fatalf("expected bridge ip %q but found: %q", expectedAddr, found) 253 } 254 t.Logf("docker bridge ip: %q", attributes["driver.docker.bridge_ip"]) 255 } 256 257 func TestDockerDriver_Check_DockerHealthStatus(t *testing.T) { 258 if !tu.IsTravis() { 259 t.Parallel() 260 } 261 if !testutil.DockerIsConnected(t) { 262 t.Skip("requires Docker") 263 } 264 if runtime.GOOS != "linux" { 265 t.Skip("expect only on linux") 266 } 267 268 require := require.New(t) 269 270 expectedAddr, err := sockaddr.GetInterfaceIP("docker0") 271 if err != nil { 272 t.Fatalf("unable to get ip for docker0: %v", err) 273 } 274 if expectedAddr == "" { 275 t.Fatalf("unable to get ip for docker bridge") 276 } 277 278 conf := testConfig(t) 279 conf.Node = mock.Node() 280 dd := NewDockerDriver(NewDriverContext("", "", "", "", conf, conf.Node, testLogger(), nil)) 281 282 request := &cstructs.HealthCheckRequest{} 283 var response cstructs.HealthCheckResponse 284 285 dc, ok := dd.(fingerprint.HealthCheck) 286 require.True(ok) 287 err = dc.HealthCheck(request, &response) 288 require.Nil(err) 289 290 driverInfo := response.Drivers["docker"] 291 require.NotNil(driverInfo) 292 require.True(driverInfo.Healthy) 293 } 294 295 func TestDockerDriver_StartOpen_Wait(t *testing.T) { 296 if !tu.IsTravis() { 297 t.Parallel() 298 } 299 if !testutil.DockerIsConnected(t) { 300 t.Skip("Docker not connected") 301 } 302 303 task := &structs.Task{ 304 Name: "nc-demo", 305 Driver: "docker", 306 Config: map[string]interface{}{ 307 "load": "busybox.tar", 308 "image": "busybox", 309 "command": "/bin/nc", 310 "args": []string{"-l", "127.0.0.1", "-p", "0"}, 311 }, 312 LogConfig: &structs.LogConfig{ 313 MaxFiles: 10, 314 MaxFileSizeMB: 10, 315 }, 316 Resources: basicResources, 317 } 318 319 ctx := testDockerDriverContexts(t, task) 320 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 321 defer ctx.AllocDir.Destroy() 322 d := NewDockerDriver(ctx.DriverCtx) 323 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 324 325 _, err := d.Prestart(ctx.ExecCtx, task) 326 if err != nil { 327 t.Fatalf("error in prestart: %v", err) 328 } 329 330 resp, err := d.Start(ctx.ExecCtx, task) 331 if err != nil { 332 t.Fatalf("err: %v", err) 333 } 334 if resp.Handle == nil { 335 t.Fatalf("missing handle") 336 } 337 defer resp.Handle.Kill() 338 339 // Attempt to open 340 resp2, err := d.Open(ctx.ExecCtx, resp.Handle.ID()) 341 if err != nil { 342 t.Fatalf("err: %v", err) 343 } 344 if resp2 == nil { 345 t.Fatalf("missing handle") 346 } 347 } 348 349 func TestDockerDriver_Start_Wait(t *testing.T) { 350 if !tu.IsTravis() { 351 t.Parallel() 352 } 353 if !testutil.DockerIsConnected(t) { 354 t.Skip("Docker not connected") 355 } 356 task := &structs.Task{ 357 Name: "nc-demo", 358 Driver: "docker", 359 Config: map[string]interface{}{ 360 "load": "busybox.tar", 361 "image": "busybox", 362 "command": "/bin/echo", 363 "args": []string{"hello"}, 364 }, 365 Resources: &structs.Resources{ 366 MemoryMB: 256, 367 CPU: 512, 368 }, 369 LogConfig: &structs.LogConfig{ 370 MaxFiles: 10, 371 MaxFileSizeMB: 10, 372 }, 373 } 374 375 _, handle, cleanup := dockerSetup(t, task) 376 defer cleanup() 377 378 // Update should be a no-op 379 err := handle.Update(task) 380 if err != nil { 381 t.Fatalf("err: %v", err) 382 } 383 384 select { 385 case res := <-handle.WaitCh(): 386 if !res.Successful() { 387 t.Fatalf("err: %v", res) 388 } 389 case <-time.After(time.Duration(tu.TestMultiplier()*5) * time.Second): 390 t.Fatalf("timeout") 391 } 392 } 393 394 // TestDockerDriver_Start_StoppedContainer asserts that Nomad will detect a 395 // stopped task container, remove it, and start a new container. 396 // 397 // See https://github.com/hashicorp/nomad/issues/3419 398 func TestDockerDriver_Start_StoppedContainer(t *testing.T) { 399 if !tu.IsTravis() { 400 t.Parallel() 401 } 402 if !testutil.DockerIsConnected(t) { 403 t.Skip("Docker not connected") 404 } 405 task := &structs.Task{ 406 Name: "nc-demo", 407 Driver: "docker", 408 Config: map[string]interface{}{ 409 "load": "busybox.tar", 410 "image": "busybox", 411 "command": "sleep", 412 "args": []string{"9000"}, 413 }, 414 Resources: &structs.Resources{ 415 MemoryMB: 100, 416 CPU: 100, 417 }, 418 LogConfig: &structs.LogConfig{ 419 MaxFiles: 1, 420 MaxFileSizeMB: 10, 421 }, 422 } 423 424 tctx := testDockerDriverContexts(t, task) 425 defer tctx.AllocDir.Destroy() 426 427 copyImage(t, tctx.ExecCtx.TaskDir, "busybox.tar") 428 client := newTestDockerClient(t) 429 driver := NewDockerDriver(tctx.DriverCtx).(*DockerDriver) 430 driverConfig := &DockerDriverConfig{ImageName: "busybox", LoadImage: "busybox.tar"} 431 if _, err := driver.loadImage(driverConfig, client, tctx.ExecCtx.TaskDir); err != nil { 432 t.Fatalf("error loading image: %v", err) 433 } 434 435 // Create a container of the same name but don't start it. This mimics 436 // the case of dockerd getting restarted and stopping containers while 437 // Nomad is watching them. 438 opts := docker.CreateContainerOptions{ 439 Name: fmt.Sprintf("%s-%s", task.Name, tctx.DriverCtx.allocID), 440 Config: &docker.Config{ 441 Image: "busybox", 442 Cmd: []string{"sleep", "9000"}, 443 }, 444 } 445 if _, err := client.CreateContainer(opts); err != nil { 446 t.Fatalf("error creating initial container: %v", err) 447 } 448 449 // Now assert that the driver can still start normally 450 presp, err := driver.Prestart(tctx.ExecCtx, task) 451 if err != nil { 452 driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 453 t.Fatalf("error in prestart: %v", err) 454 } 455 defer driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 456 457 sresp, err := driver.Start(tctx.ExecCtx, task) 458 if err != nil { 459 t.Fatalf("failed to start driver: %s", err) 460 } 461 handle := sresp.Handle.(*DockerHandle) 462 waitForExist(t, client, handle) 463 handle.Kill() 464 } 465 466 func TestDockerDriver_Start_LoadImage(t *testing.T) { 467 if !tu.IsTravis() { 468 t.Parallel() 469 } 470 if !testutil.DockerIsConnected(t) { 471 t.Skip("Docker not connected") 472 } 473 task := &structs.Task{ 474 Name: "busybox-demo", 475 Driver: "docker", 476 Config: map[string]interface{}{ 477 "image": "busybox", 478 "load": "busybox.tar", 479 "command": "/bin/sh", 480 "args": []string{ 481 "-c", 482 "echo hello > $NOMAD_TASK_DIR/output", 483 }, 484 }, 485 LogConfig: &structs.LogConfig{ 486 MaxFiles: 10, 487 MaxFileSizeMB: 10, 488 }, 489 Resources: &structs.Resources{ 490 MemoryMB: 256, 491 CPU: 512, 492 }, 493 } 494 495 ctx := testDockerDriverContexts(t, task) 496 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 497 defer ctx.AllocDir.Destroy() 498 d := NewDockerDriver(ctx.DriverCtx) 499 500 // Copy the image into the task's directory 501 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 502 503 _, err := d.Prestart(ctx.ExecCtx, task) 504 if err != nil { 505 t.Fatalf("error in prestart: %v", err) 506 } 507 resp, err := d.Start(ctx.ExecCtx, task) 508 if err != nil { 509 t.Fatalf("err: %v", err) 510 } 511 defer resp.Handle.Kill() 512 513 select { 514 case res := <-resp.Handle.WaitCh(): 515 if !res.Successful() { 516 t.Fatalf("err: %v", res) 517 } 518 case <-time.After(time.Duration(tu.TestMultiplier()*5) * time.Second): 519 t.Fatalf("timeout") 520 } 521 522 // Check that data was written to the shared alloc directory. 523 outputFile := filepath.Join(ctx.ExecCtx.TaskDir.LocalDir, "output") 524 act, err := ioutil.ReadFile(outputFile) 525 if err != nil { 526 t.Fatalf("Couldn't read expected output: %v", err) 527 } 528 529 exp := "hello" 530 if strings.TrimSpace(string(act)) != exp { 531 t.Fatalf("Command outputted %v; want %v", act, exp) 532 } 533 534 } 535 536 func TestDockerDriver_Start_BadPull_Recoverable(t *testing.T) { 537 if !tu.IsTravis() { 538 t.Parallel() 539 } 540 if !testutil.DockerIsConnected(t) { 541 t.Skip("Docker not connected") 542 } 543 task := &structs.Task{ 544 Name: "busybox-demo", 545 Driver: "docker", 546 Config: map[string]interface{}{ 547 "image": "127.0.1.1:32121/foo", // bad path 548 "command": "/bin/echo", 549 "args": []string{ 550 "hello", 551 }, 552 }, 553 LogConfig: &structs.LogConfig{ 554 MaxFiles: 10, 555 MaxFileSizeMB: 10, 556 }, 557 Resources: &structs.Resources{ 558 MemoryMB: 256, 559 CPU: 512, 560 }, 561 } 562 563 ctx := testDockerDriverContexts(t, task) 564 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 565 defer ctx.AllocDir.Destroy() 566 d := NewDockerDriver(ctx.DriverCtx) 567 568 _, err := d.Prestart(ctx.ExecCtx, task) 569 if err == nil { 570 t.Fatalf("want error in prestart: %v", err) 571 } 572 573 if rerr, ok := err.(*structs.RecoverableError); !ok { 574 t.Fatalf("want recoverable error: %+v", err) 575 } else if !rerr.IsRecoverable() { 576 t.Fatalf("error not recoverable: %+v", err) 577 } 578 } 579 580 func TestDockerDriver_Start_Wait_AllocDir(t *testing.T) { 581 if !tu.IsTravis() { 582 t.Parallel() 583 } 584 // This test requires that the alloc dir be mounted into docker as a volume. 585 // Because this cannot happen when docker is run remotely, e.g. when running 586 // docker in a VM, we skip this when we detect Docker is being run remotely. 587 if !testutil.DockerIsConnected(t) || dockerIsRemote(t) { 588 t.Skip("Docker not connected") 589 } 590 591 exp := []byte{'w', 'i', 'n'} 592 file := "output.txt" 593 task := &structs.Task{ 594 Name: "nc-demo", 595 Driver: "docker", 596 Config: map[string]interface{}{ 597 "image": "busybox", 598 "load": "busybox.tar", 599 "command": "/bin/sh", 600 "args": []string{ 601 "-c", 602 fmt.Sprintf(`sleep 1; echo -n %s > $%s/%s`, 603 string(exp), env.AllocDir, file), 604 }, 605 }, 606 LogConfig: &structs.LogConfig{ 607 MaxFiles: 10, 608 MaxFileSizeMB: 10, 609 }, 610 Resources: &structs.Resources{ 611 MemoryMB: 256, 612 CPU: 512, 613 }, 614 } 615 616 ctx := testDockerDriverContexts(t, task) 617 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 618 defer ctx.AllocDir.Destroy() 619 d := NewDockerDriver(ctx.DriverCtx) 620 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 621 622 _, err := d.Prestart(ctx.ExecCtx, task) 623 if err != nil { 624 t.Fatalf("error in prestart: %v", err) 625 } 626 resp, err := d.Start(ctx.ExecCtx, task) 627 if err != nil { 628 t.Fatalf("err: %v", err) 629 } 630 defer resp.Handle.Kill() 631 632 select { 633 case res := <-resp.Handle.WaitCh(): 634 if !res.Successful() { 635 t.Fatalf("err: %v", res) 636 } 637 case <-time.After(time.Duration(tu.TestMultiplier()*5) * time.Second): 638 t.Fatalf("timeout") 639 } 640 641 // Check that data was written to the shared alloc directory. 642 outputFile := filepath.Join(ctx.AllocDir.SharedDir, file) 643 act, err := ioutil.ReadFile(outputFile) 644 if err != nil { 645 t.Fatalf("Couldn't read expected output: %v", err) 646 } 647 648 if !reflect.DeepEqual(act, exp) { 649 t.Fatalf("Command outputted %v; want %v", act, exp) 650 } 651 } 652 653 func TestDockerDriver_Start_Kill_Wait(t *testing.T) { 654 if !tu.IsTravis() { 655 t.Parallel() 656 } 657 if !testutil.DockerIsConnected(t) { 658 t.Skip("Docker not connected") 659 } 660 task := &structs.Task{ 661 Name: "nc-demo", 662 Driver: "docker", 663 Config: map[string]interface{}{ 664 "image": "busybox", 665 "load": "busybox.tar", 666 "command": "/bin/sleep", 667 "args": []string{"10"}, 668 }, 669 LogConfig: &structs.LogConfig{ 670 MaxFiles: 10, 671 MaxFileSizeMB: 10, 672 }, 673 Resources: basicResources, 674 } 675 676 _, handle, cleanup := dockerSetup(t, task) 677 defer cleanup() 678 679 go func() { 680 time.Sleep(100 * time.Millisecond) 681 err := handle.Kill() 682 if err != nil { 683 t.Fatalf("err: %v", err) 684 } 685 }() 686 687 select { 688 case res := <-handle.WaitCh(): 689 if res.Successful() { 690 t.Fatalf("should err: %v", res) 691 } 692 case <-time.After(time.Duration(tu.TestMultiplier()*10) * time.Second): 693 t.Fatalf("timeout") 694 } 695 } 696 697 func TestDockerDriver_Start_KillTimeout(t *testing.T) { 698 if !tu.IsTravis() { 699 t.Parallel() 700 } 701 if !testutil.DockerIsConnected(t) { 702 t.Skip("Docker not connected") 703 } 704 timeout := 2 * time.Second 705 task := &structs.Task{ 706 Name: "nc-demo", 707 Driver: "docker", 708 Config: map[string]interface{}{ 709 "image": "busybox", 710 "load": "busybox.tar", 711 "command": "/bin/sleep", 712 "args": []string{"10"}, 713 }, 714 LogConfig: &structs.LogConfig{ 715 MaxFiles: 10, 716 MaxFileSizeMB: 10, 717 }, 718 Resources: basicResources, 719 KillTimeout: timeout, 720 KillSignal: "SIGUSR1", // Pick something that doesn't actually kill it 721 } 722 723 _, handle, cleanup := dockerSetup(t, task) 724 defer cleanup() 725 726 // Reduce the timeout for the docker client. 727 handle.client.SetTimeout(1 * time.Second) 728 729 // Kill the task 730 var killSent, killed time.Time 731 go func() { 732 killSent = time.Now() 733 if err := handle.Kill(); err != nil { 734 t.Fatalf("err: %v", err) 735 } 736 }() 737 738 select { 739 case <-handle.WaitCh(): 740 killed = time.Now() 741 case <-time.After(10 * time.Second): 742 t.Fatalf("timeout") 743 } 744 745 if killed.Sub(killSent) < timeout { 746 t.Fatalf("kill timeout not respected") 747 } 748 } 749 750 func TestDockerDriver_StartN(t *testing.T) { 751 if !tu.IsTravis() { 752 t.Parallel() 753 } 754 if !testutil.DockerIsConnected(t) { 755 t.Skip("Docker not connected") 756 } 757 758 task1, _, _ := dockerTask(t) 759 task2, _, _ := dockerTask(t) 760 task3, _, _ := dockerTask(t) 761 taskList := []*structs.Task{task1, task2, task3} 762 763 handles := make([]DriverHandle, len(taskList)) 764 765 t.Logf("Starting %d tasks", len(taskList)) 766 767 // Let's spin up a bunch of things 768 for idx, task := range taskList { 769 ctx := testDockerDriverContexts(t, task) 770 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 771 defer ctx.AllocDir.Destroy() 772 d := NewDockerDriver(ctx.DriverCtx) 773 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 774 775 _, err := d.Prestart(ctx.ExecCtx, task) 776 if err != nil { 777 t.Fatalf("error in prestart #%d: %v", idx+1, err) 778 } 779 resp, err := d.Start(ctx.ExecCtx, task) 780 if err != nil { 781 t.Errorf("Failed starting task #%d: %s", idx+1, err) 782 continue 783 } 784 handles[idx] = resp.Handle 785 } 786 787 t.Log("All tasks are started. Terminating...") 788 789 for idx, handle := range handles { 790 if handle == nil { 791 t.Errorf("Bad handle for task #%d", idx+1) 792 continue 793 } 794 795 err := handle.Kill() 796 if err != nil { 797 t.Errorf("Failed stopping task #%d: %s", idx+1, err) 798 } 799 } 800 801 t.Log("Test complete!") 802 } 803 804 func TestDockerDriver_StartNVersions(t *testing.T) { 805 if !tu.IsTravis() { 806 t.Parallel() 807 } 808 if !testutil.DockerIsConnected(t) { 809 t.Skip("Docker not connected") 810 } 811 812 task1, _, _ := dockerTask(t) 813 task1.Config["image"] = "busybox" 814 task1.Config["load"] = "busybox.tar" 815 816 task2, _, _ := dockerTask(t) 817 task2.Config["image"] = "busybox:musl" 818 task2.Config["load"] = "busybox_musl.tar" 819 task2.Config["args"] = []string{"-l", "-p", "0"} 820 821 task3, _, _ := dockerTask(t) 822 task3.Config["image"] = "busybox:glibc" 823 task3.Config["load"] = "busybox_glibc.tar" 824 825 taskList := []*structs.Task{task1, task2, task3} 826 827 handles := make([]DriverHandle, len(taskList)) 828 829 t.Logf("Starting %d tasks", len(taskList)) 830 client := newTestDockerClient(t) 831 832 // Let's spin up a bunch of things 833 for idx, task := range taskList { 834 ctx := testDockerDriverContexts(t, task) 835 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 836 defer ctx.AllocDir.Destroy() 837 d := NewDockerDriver(ctx.DriverCtx) 838 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 839 copyImage(t, ctx.ExecCtx.TaskDir, "busybox_musl.tar") 840 copyImage(t, ctx.ExecCtx.TaskDir, "busybox_glibc.tar") 841 842 _, err := d.Prestart(ctx.ExecCtx, task) 843 if err != nil { 844 t.Fatalf("error in prestart #%d: %v", idx+1, err) 845 } 846 resp, err := d.Start(ctx.ExecCtx, task) 847 if err != nil { 848 t.Errorf("Failed starting task #%d: %s", idx+1, err) 849 continue 850 } 851 handles[idx] = resp.Handle 852 waitForExist(t, client, resp.Handle.(*DockerHandle)) 853 } 854 855 t.Log("All tasks are started. Terminating...") 856 857 for idx, handle := range handles { 858 if handle == nil { 859 t.Errorf("Bad handle for task #%d", idx+1) 860 continue 861 } 862 863 err := handle.Kill() 864 if err != nil { 865 t.Errorf("Failed stopping task #%d: %s", idx+1, err) 866 } 867 } 868 869 t.Log("Test complete!") 870 } 871 872 func waitForExist(t *testing.T, client *docker.Client, handle *DockerHandle) { 873 handle.logger.Printf("[DEBUG] docker.test: waiting for container %s to exist...", handle.ContainerID()) 874 tu.WaitForResult(func() (bool, error) { 875 container, err := client.InspectContainer(handle.ContainerID()) 876 if err != nil { 877 if _, ok := err.(*docker.NoSuchContainer); !ok { 878 return false, err 879 } 880 } 881 882 return container != nil, nil 883 }, func(err error) { 884 t.Fatalf("err: %v", err) 885 }) 886 handle.logger.Printf("[DEBUG] docker.test: ...container %s exists!", handle.ContainerID()) 887 } 888 889 func TestDockerDriver_NetworkMode_Host(t *testing.T) { 890 if !tu.IsTravis() { 891 t.Parallel() 892 } 893 if !testutil.DockerIsConnected(t) { 894 t.Skip("Docker not connected") 895 } 896 expected := "host" 897 898 task := &structs.Task{ 899 Name: "nc-demo", 900 Driver: "docker", 901 Config: map[string]interface{}{ 902 "image": "busybox", 903 "load": "busybox.tar", 904 "command": "/bin/nc", 905 "args": []string{"-l", "127.0.0.1", "-p", "0"}, 906 "network_mode": expected, 907 }, 908 Resources: &structs.Resources{ 909 MemoryMB: 256, 910 CPU: 512, 911 }, 912 LogConfig: &structs.LogConfig{ 913 MaxFiles: 10, 914 MaxFileSizeMB: 10, 915 }, 916 } 917 918 client, handle, cleanup := dockerSetup(t, task) 919 defer cleanup() 920 921 waitForExist(t, client, handle) 922 923 container, err := client.InspectContainer(handle.ContainerID()) 924 if err != nil { 925 t.Fatalf("err: %v", err) 926 } 927 928 actual := container.HostConfig.NetworkMode 929 if actual != expected { 930 t.Fatalf("Got network mode %q; want %q", expected, actual) 931 } 932 } 933 934 func TestDockerDriver_NetworkAliases_Bridge(t *testing.T) { 935 if !tu.IsTravis() { 936 t.Parallel() 937 } 938 if !testutil.DockerIsConnected(t) { 939 t.Skip("Docker not connected") 940 } 941 942 // Because go-dockerclient doesn't provide api for query network aliases, just check that 943 // a container can be created with a 'network_aliases' property 944 945 // Create network, network-scoped alias is supported only for containers in user defined networks 946 client := newTestDockerClient(t) 947 networkOpts := docker.CreateNetworkOptions{Name: "foobar", Driver: "bridge"} 948 network, err := client.CreateNetwork(networkOpts) 949 if err != nil { 950 t.Fatalf("err: %v", err) 951 } 952 defer client.RemoveNetwork(network.ID) 953 954 expected := []string{"foobar"} 955 task := &structs.Task{ 956 Name: "nc-demo", 957 Driver: "docker", 958 Config: map[string]interface{}{ 959 "image": "busybox", 960 "load": "busybox.tar", 961 "command": "/bin/nc", 962 "args": []string{"-l", "127.0.0.1", "-p", "0"}, 963 "network_mode": network.Name, 964 "network_aliases": expected, 965 }, 966 Resources: &structs.Resources{ 967 MemoryMB: 256, 968 CPU: 512, 969 }, 970 LogConfig: &structs.LogConfig{ 971 MaxFiles: 10, 972 MaxFileSizeMB: 10, 973 }, 974 } 975 976 client, handle, cleanup := dockerSetupWithClient(t, task, client) 977 defer cleanup() 978 979 waitForExist(t, client, handle) 980 981 _, err = client.InspectContainer(handle.ContainerID()) 982 if err != nil { 983 t.Fatalf("err: %v", err) 984 } 985 } 986 987 func TestDockerDriver_Sysctl_Ulimit(t *testing.T) { 988 task, _, _ := dockerTask(t) 989 expectedUlimits := map[string]string{ 990 "nproc": "4242", 991 "nofile": "2048:4096", 992 } 993 task.Config["sysctl"] = []map[string]string{ 994 { 995 "net.core.somaxconn": "16384", 996 }, 997 } 998 task.Config["ulimit"] = []map[string]string{ 999 expectedUlimits, 1000 } 1001 1002 client, handle, cleanup := dockerSetup(t, task) 1003 defer cleanup() 1004 1005 waitForExist(t, client, handle) 1006 1007 container, err := client.InspectContainer(handle.ContainerID()) 1008 assert.Nil(t, err, "unexpected error: %v", err) 1009 1010 want := "16384" 1011 got := container.HostConfig.Sysctls["net.core.somaxconn"] 1012 assert.Equal(t, want, got, "Wrong net.core.somaxconn config for docker job. Expect: %s, got: %s", want, got) 1013 1014 expectedUlimitLen := 2 1015 actualUlimitLen := len(container.HostConfig.Ulimits) 1016 assert.Equal(t, want, got, "Wrong number of ulimit configs for docker job. Expect: %d, got: %d", expectedUlimitLen, actualUlimitLen) 1017 1018 for _, got := range container.HostConfig.Ulimits { 1019 if expectedStr, ok := expectedUlimits[got.Name]; !ok { 1020 t.Errorf("%s config unexpected for docker job.", got.Name) 1021 } else { 1022 if !strings.Contains(expectedStr, ":") { 1023 expectedStr = expectedStr + ":" + expectedStr 1024 } 1025 1026 splitted := strings.SplitN(expectedStr, ":", 2) 1027 soft, _ := strconv.Atoi(splitted[0]) 1028 hard, _ := strconv.Atoi(splitted[1]) 1029 assert.Equal(t, int64(soft), got.Soft, "Wrong soft %s ulimit for docker job. Expect: %d, got: %d", got.Name, soft, got.Soft) 1030 assert.Equal(t, int64(hard), got.Hard, "Wrong hard %s ulimit for docker job. Expect: %d, got: %d", got.Name, hard, got.Hard) 1031 1032 } 1033 } 1034 } 1035 1036 func TestDockerDriver_Sysctl_Ulimit_Errors(t *testing.T) { 1037 brokenConfigs := []interface{}{ 1038 map[string]interface{}{ 1039 "nofile": "", 1040 }, 1041 map[string]interface{}{ 1042 "nofile": "abc:1234", 1043 }, 1044 map[string]interface{}{ 1045 "nofile": "1234:abc", 1046 }, 1047 } 1048 1049 test_cases := []struct { 1050 ulimitConfig interface{} 1051 err error 1052 }{ 1053 {[]interface{}{brokenConfigs[0]}, fmt.Errorf("Malformed ulimit specification nofile: \"\", cannot be empty")}, 1054 {[]interface{}{brokenConfigs[1]}, fmt.Errorf("Malformed soft ulimit nofile: abc:1234")}, 1055 {[]interface{}{brokenConfigs[2]}, fmt.Errorf("Malformed hard ulimit nofile: 1234:abc")}, 1056 } 1057 1058 for _, tc := range test_cases { 1059 task, _, _ := dockerTask(t) 1060 task.Config["ulimit"] = tc.ulimitConfig 1061 1062 ctx := testDockerDriverContexts(t, task) 1063 driver := NewDockerDriver(ctx.DriverCtx) 1064 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 1065 defer ctx.AllocDir.Destroy() 1066 1067 _, err := driver.Prestart(ctx.ExecCtx, task) 1068 assert.NotNil(t, err, "Expected non nil error") 1069 assert.Equal(t, err.Error(), tc.err.Error(), "unexpected error in prestart, got %v, expected %v", err, tc.err) 1070 } 1071 } 1072 1073 func TestDockerDriver_Labels(t *testing.T) { 1074 if !tu.IsTravis() { 1075 t.Parallel() 1076 } 1077 if !testutil.DockerIsConnected(t) { 1078 t.Skip("Docker not connected") 1079 } 1080 1081 task, _, _ := dockerTask(t) 1082 task.Config["labels"] = []map[string]string{ 1083 { 1084 "label1": "value1", 1085 "label2": "value2", 1086 }, 1087 } 1088 1089 client, handle, cleanup := dockerSetup(t, task) 1090 defer cleanup() 1091 1092 waitForExist(t, client, handle) 1093 1094 container, err := client.InspectContainer(handle.ContainerID()) 1095 if err != nil { 1096 t.Fatalf("err: %v", err) 1097 } 1098 1099 if want, got := 2, len(container.Config.Labels); want != got { 1100 t.Errorf("Wrong labels count for docker job. Expect: %d, got: %d", want, got) 1101 } 1102 1103 if want, got := "value1", container.Config.Labels["label1"]; want != got { 1104 t.Errorf("Wrong label value docker job. Expect: %s, got: %s", want, got) 1105 } 1106 } 1107 1108 func TestDockerDriver_ForcePull_IsInvalidConfig(t *testing.T) { 1109 if !tu.IsTravis() { 1110 t.Parallel() 1111 } 1112 if !testutil.DockerIsConnected(t) { 1113 t.Skip("Docker not connected") 1114 } 1115 1116 task, _, _ := dockerTask(t) 1117 task.Config["force_pull"] = "nothing" 1118 1119 ctx := testDockerDriverContexts(t, task) 1120 defer ctx.AllocDir.Destroy() 1121 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 1122 driver := NewDockerDriver(ctx.DriverCtx) 1123 1124 if _, err := driver.Prestart(ctx.ExecCtx, task); err == nil { 1125 t.Fatalf("error expected in prestart") 1126 } 1127 } 1128 1129 func TestDockerDriver_ForcePull(t *testing.T) { 1130 if !tu.IsTravis() { 1131 t.Parallel() 1132 } 1133 if !testutil.DockerIsConnected(t) { 1134 t.Skip("Docker not connected") 1135 } 1136 1137 task, _, _ := dockerTask(t) 1138 task.Config["force_pull"] = "true" 1139 1140 client, handle, cleanup := dockerSetup(t, task) 1141 defer cleanup() 1142 1143 waitForExist(t, client, handle) 1144 1145 _, err := client.InspectContainer(handle.ContainerID()) 1146 if err != nil { 1147 t.Fatalf("err: %v", err) 1148 } 1149 } 1150 1151 func TestDockerDriver_ForcePull_RepoDigest(t *testing.T) { 1152 if !tu.IsTravis() { 1153 t.Parallel() 1154 } 1155 if !testutil.DockerIsConnected(t) { 1156 t.Skip("Docker not connected") 1157 } 1158 1159 task, _, _ := dockerTask(t) 1160 task.Config["load"] = "" 1161 task.Config["image"] = "library/busybox@sha256:58ac43b2cc92c687a32c8be6278e50a063579655fe3090125dcb2af0ff9e1a64" 1162 localDigest := "sha256:8ac48589692a53a9b8c2d1ceaa6b402665aa7fe667ba51ccc03002300856d8c7" 1163 task.Config["force_pull"] = "true" 1164 1165 client, handle, cleanup := dockerSetup(t, task) 1166 defer cleanup() 1167 1168 waitForExist(t, client, handle) 1169 1170 container, err := client.InspectContainer(handle.ContainerID()) 1171 require.NoError(t, err) 1172 require.Equal(t, localDigest, container.Image) 1173 } 1174 1175 func TestDockerDriver_SecurityOpt(t *testing.T) { 1176 if !tu.IsTravis() { 1177 t.Parallel() 1178 } 1179 if !testutil.DockerIsConnected(t) { 1180 t.Skip("Docker not connected") 1181 } 1182 1183 task, _, _ := dockerTask(t) 1184 task.Config["security_opt"] = []string{"seccomp=unconfined"} 1185 1186 client, handle, cleanup := dockerSetup(t, task) 1187 defer cleanup() 1188 1189 waitForExist(t, client, handle) 1190 1191 container, err := client.InspectContainer(handle.ContainerID()) 1192 if err != nil { 1193 t.Fatalf("err: %v", err) 1194 } 1195 1196 if !reflect.DeepEqual(task.Config["security_opt"], container.HostConfig.SecurityOpt) { 1197 t.Errorf("Security Opts don't match.\nExpected:\n%s\nGot:\n%s\n", task.Config["security_opt"], container.HostConfig.SecurityOpt) 1198 } 1199 } 1200 1201 func TestDockerDriver_Capabilities(t *testing.T) { 1202 if !tu.IsTravis() { 1203 t.Parallel() 1204 } 1205 if !testutil.DockerIsConnected(t) { 1206 t.Skip("Docker not connected") 1207 } 1208 if runtime.GOOS == "windows" { 1209 t.Skip("Capabilities not supported on windows") 1210 } 1211 1212 testCases := []struct { 1213 Name string 1214 CapAdd []string 1215 CapDrop []string 1216 Whitelist string 1217 StartError string 1218 }{ 1219 { 1220 Name: "default-whitelist-add-allowed", 1221 CapAdd: []string{"fowner", "mknod"}, 1222 CapDrop: []string{"all"}, 1223 }, 1224 { 1225 Name: "default-whitelist-add-forbidden", 1226 CapAdd: []string{"net_admin"}, 1227 StartError: "net_admin", 1228 }, 1229 { 1230 Name: "default-whitelist-drop-existing", 1231 CapDrop: []string{"fowner", "mknod"}, 1232 }, 1233 { 1234 Name: "restrictive-whitelist-drop-all", 1235 CapDrop: []string{"all"}, 1236 Whitelist: "fowner,mknod", 1237 }, 1238 { 1239 Name: "restrictive-whitelist-add-allowed", 1240 CapAdd: []string{"fowner", "mknod"}, 1241 CapDrop: []string{"all"}, 1242 Whitelist: "fowner,mknod", 1243 }, 1244 { 1245 Name: "restrictive-whitelist-add-forbidden", 1246 CapAdd: []string{"net_admin", "mknod"}, 1247 CapDrop: []string{"all"}, 1248 Whitelist: "fowner,mknod", 1249 StartError: "net_admin", 1250 }, 1251 { 1252 Name: "permissive-whitelist", 1253 CapAdd: []string{"net_admin", "mknod"}, 1254 Whitelist: "all", 1255 }, 1256 { 1257 Name: "permissive-whitelist-add-all", 1258 CapAdd: []string{"all"}, 1259 Whitelist: "all", 1260 }, 1261 } 1262 1263 for _, tc := range testCases { 1264 t.Run(tc.Name, func(t *testing.T) { 1265 client := newTestDockerClient(t) 1266 task, _, _ := dockerTask(t) 1267 if len(tc.CapAdd) > 0 { 1268 task.Config["cap_add"] = tc.CapAdd 1269 } 1270 if len(tc.CapDrop) > 0 { 1271 task.Config["cap_drop"] = tc.CapDrop 1272 } 1273 1274 tctx := testDockerDriverContexts(t, task) 1275 if tc.Whitelist != "" { 1276 tctx.DriverCtx.config.Options[dockerCapsWhitelistConfigOption] = tc.Whitelist 1277 } 1278 1279 driver := NewDockerDriver(tctx.DriverCtx) 1280 copyImage(t, tctx.ExecCtx.TaskDir, "busybox.tar") 1281 defer tctx.AllocDir.Destroy() 1282 1283 presp, err := driver.Prestart(tctx.ExecCtx, task) 1284 defer driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 1285 if err != nil { 1286 t.Fatalf("Error in prestart: %v", err) 1287 } 1288 1289 sresp, err := driver.Start(tctx.ExecCtx, task) 1290 if err == nil && tc.StartError != "" { 1291 t.Fatalf("Expected error in start: %v", tc.StartError) 1292 } else if err != nil { 1293 if tc.StartError == "" { 1294 t.Fatalf("Failed to start driver: %s\nStack\n%s", err, debug.Stack()) 1295 } else if !strings.Contains(err.Error(), tc.StartError) { 1296 t.Fatalf("Expect error containing \"%s\", got %v", tc.StartError, err) 1297 } 1298 return 1299 } 1300 1301 if sresp.Handle == nil { 1302 t.Fatalf("handle is nil\nStack\n%s", debug.Stack()) 1303 } 1304 defer sresp.Handle.Kill() 1305 handle := sresp.Handle.(*DockerHandle) 1306 1307 waitForExist(t, client, handle) 1308 1309 container, err := client.InspectContainer(handle.ContainerID()) 1310 if err != nil { 1311 t.Fatalf("Error inspecting container: %v", err) 1312 } 1313 1314 if !reflect.DeepEqual(tc.CapAdd, container.HostConfig.CapAdd) { 1315 t.Errorf("CapAdd doesn't match.\nExpected:\n%s\nGot:\n%s\n", tc.CapAdd, container.HostConfig.CapAdd) 1316 } 1317 1318 if !reflect.DeepEqual(tc.CapDrop, container.HostConfig.CapDrop) { 1319 t.Errorf("CapDrop doesn't match.\nExpected:\n%s\nGot:\n%s\n", tc.CapDrop, container.HostConfig.CapDrop) 1320 } 1321 }) 1322 } 1323 } 1324 1325 func TestDockerDriver_DNS(t *testing.T) { 1326 if !tu.IsTravis() { 1327 t.Parallel() 1328 } 1329 if !testutil.DockerIsConnected(t) { 1330 t.Skip("Docker not connected") 1331 } 1332 1333 task, _, _ := dockerTask(t) 1334 task.Config["dns_servers"] = []string{"8.8.8.8", "8.8.4.4"} 1335 task.Config["dns_search_domains"] = []string{"example.com", "example.org", "example.net"} 1336 task.Config["dns_options"] = []string{"ndots:1"} 1337 1338 client, handle, cleanup := dockerSetup(t, task) 1339 defer cleanup() 1340 1341 waitForExist(t, client, handle) 1342 1343 container, err := client.InspectContainer(handle.ContainerID()) 1344 if err != nil { 1345 t.Fatalf("err: %v", err) 1346 } 1347 1348 if !reflect.DeepEqual(task.Config["dns_servers"], container.HostConfig.DNS) { 1349 t.Errorf("DNS Servers don't match.\nExpected:\n%s\nGot:\n%s\n", task.Config["dns_servers"], container.HostConfig.DNS) 1350 } 1351 1352 if !reflect.DeepEqual(task.Config["dns_search_domains"], container.HostConfig.DNSSearch) { 1353 t.Errorf("DNS Search Domains don't match.\nExpected:\n%s\nGot:\n%s\n", task.Config["dns_search_domains"], container.HostConfig.DNSSearch) 1354 } 1355 1356 if !reflect.DeepEqual(task.Config["dns_options"], container.HostConfig.DNSOptions) { 1357 t.Errorf("DNS Options don't match.\nExpected:\n%s\nGot:\n%s\n", task.Config["dns_options"], container.HostConfig.DNSOptions) 1358 } 1359 } 1360 1361 func TestDockerDriver_MACAddress(t *testing.T) { 1362 if !tu.IsTravis() { 1363 t.Parallel() 1364 } 1365 if !testutil.DockerIsConnected(t) { 1366 t.Skip("Docker not connected") 1367 } 1368 1369 task, _, _ := dockerTask(t) 1370 task.Config["mac_address"] = "00:16:3e:00:00:00" 1371 1372 client, handle, cleanup := dockerSetup(t, task) 1373 defer cleanup() 1374 1375 waitForExist(t, client, handle) 1376 1377 container, err := client.InspectContainer(handle.ContainerID()) 1378 if err != nil { 1379 t.Fatalf("err: %v", err) 1380 } 1381 1382 if container.NetworkSettings.MacAddress != task.Config["mac_address"] { 1383 t.Errorf("expected mac_address=%q but found %q", task.Config["mac_address"], container.NetworkSettings.MacAddress) 1384 } 1385 } 1386 1387 func TestDockerWorkDir(t *testing.T) { 1388 if !tu.IsTravis() { 1389 t.Parallel() 1390 } 1391 if !testutil.DockerIsConnected(t) { 1392 t.Skip("Docker not connected") 1393 } 1394 1395 task, _, _ := dockerTask(t) 1396 task.Config["work_dir"] = "/some/path" 1397 1398 client, handle, cleanup := dockerSetup(t, task) 1399 defer cleanup() 1400 1401 container, err := client.InspectContainer(handle.ContainerID()) 1402 if err != nil { 1403 t.Fatalf("err: %v", err) 1404 } 1405 1406 if want, got := "/some/path", container.Config.WorkingDir; want != got { 1407 t.Errorf("Wrong working directory for docker job. Expect: %s, got: %s", want, got) 1408 } 1409 } 1410 1411 func inSlice(needle string, haystack []string) bool { 1412 for _, h := range haystack { 1413 if h == needle { 1414 return true 1415 } 1416 } 1417 return false 1418 } 1419 1420 func TestDockerDriver_PortsNoMap(t *testing.T) { 1421 if !tu.IsTravis() { 1422 t.Parallel() 1423 } 1424 if !testutil.DockerIsConnected(t) { 1425 t.Skip("Docker not connected") 1426 } 1427 1428 task, res, dyn := dockerTask(t) 1429 1430 client, handle, cleanup := dockerSetup(t, task) 1431 defer cleanup() 1432 1433 waitForExist(t, client, handle) 1434 1435 container, err := client.InspectContainer(handle.ContainerID()) 1436 if err != nil { 1437 t.Fatalf("err: %v", err) 1438 } 1439 1440 // Verify that the correct ports are EXPOSED 1441 expectedExposedPorts := map[docker.Port]struct{}{ 1442 docker.Port(fmt.Sprintf("%d/tcp", res)): {}, 1443 docker.Port(fmt.Sprintf("%d/udp", res)): {}, 1444 docker.Port(fmt.Sprintf("%d/tcp", dyn)): {}, 1445 docker.Port(fmt.Sprintf("%d/udp", dyn)): {}, 1446 } 1447 1448 if !reflect.DeepEqual(container.Config.ExposedPorts, expectedExposedPorts) { 1449 t.Errorf("Exposed ports don't match.\nExpected:\n%s\nGot:\n%s\n", expectedExposedPorts, container.Config.ExposedPorts) 1450 } 1451 1452 // Verify that the correct ports are FORWARDED 1453 expectedPortBindings := map[docker.Port][]docker.PortBinding{ 1454 docker.Port(fmt.Sprintf("%d/tcp", res)): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", res)}}, 1455 docker.Port(fmt.Sprintf("%d/udp", res)): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", res)}}, 1456 docker.Port(fmt.Sprintf("%d/tcp", dyn)): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", dyn)}}, 1457 docker.Port(fmt.Sprintf("%d/udp", dyn)): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", dyn)}}, 1458 } 1459 1460 if !reflect.DeepEqual(container.HostConfig.PortBindings, expectedPortBindings) { 1461 t.Errorf("Forwarded ports don't match.\nExpected:\n%s\nGot:\n%s\n", expectedPortBindings, container.HostConfig.PortBindings) 1462 } 1463 1464 expectedEnvironment := map[string]string{ 1465 "NOMAD_ADDR_main": fmt.Sprintf("127.0.0.1:%d", res), 1466 "NOMAD_ADDR_REDIS": fmt.Sprintf("127.0.0.1:%d", dyn), 1467 } 1468 1469 for key, val := range expectedEnvironment { 1470 search := fmt.Sprintf("%s=%s", key, val) 1471 if !inSlice(search, container.Config.Env) { 1472 t.Errorf("Expected to find %s in container environment: %+v", search, container.Config.Env) 1473 } 1474 } 1475 } 1476 1477 func TestDockerDriver_PortsMapping(t *testing.T) { 1478 if !tu.IsTravis() { 1479 t.Parallel() 1480 } 1481 if !testutil.DockerIsConnected(t) { 1482 t.Skip("Docker not connected") 1483 } 1484 1485 task, res, dyn := dockerTask(t) 1486 task.Config["port_map"] = []map[string]string{ 1487 { 1488 "main": "8080", 1489 "REDIS": "6379", 1490 }, 1491 } 1492 1493 client, handle, cleanup := dockerSetup(t, task) 1494 defer cleanup() 1495 1496 waitForExist(t, client, handle) 1497 1498 container, err := client.InspectContainer(handle.ContainerID()) 1499 if err != nil { 1500 t.Fatalf("err: %v", err) 1501 } 1502 1503 // Verify that the correct ports are EXPOSED 1504 expectedExposedPorts := map[docker.Port]struct{}{ 1505 docker.Port("8080/tcp"): {}, 1506 docker.Port("8080/udp"): {}, 1507 docker.Port("6379/tcp"): {}, 1508 docker.Port("6379/udp"): {}, 1509 } 1510 1511 if !reflect.DeepEqual(container.Config.ExposedPorts, expectedExposedPorts) { 1512 t.Errorf("Exposed ports don't match.\nExpected:\n%s\nGot:\n%s\n", expectedExposedPorts, container.Config.ExposedPorts) 1513 } 1514 1515 // Verify that the correct ports are FORWARDED 1516 expectedPortBindings := map[docker.Port][]docker.PortBinding{ 1517 docker.Port("8080/tcp"): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", res)}}, 1518 docker.Port("8080/udp"): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", res)}}, 1519 docker.Port("6379/tcp"): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", dyn)}}, 1520 docker.Port("6379/udp"): {{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", dyn)}}, 1521 } 1522 1523 if !reflect.DeepEqual(container.HostConfig.PortBindings, expectedPortBindings) { 1524 t.Errorf("Forwarded ports don't match.\nExpected:\n%s\nGot:\n%s\n", expectedPortBindings, container.HostConfig.PortBindings) 1525 } 1526 1527 expectedEnvironment := map[string]string{ 1528 "NOMAD_PORT_main": "8080", 1529 "NOMAD_PORT_REDIS": "6379", 1530 "NOMAD_HOST_PORT_main": strconv.Itoa(res), 1531 } 1532 1533 sort.Strings(container.Config.Env) 1534 for key, val := range expectedEnvironment { 1535 search := fmt.Sprintf("%s=%s", key, val) 1536 if !inSlice(search, container.Config.Env) { 1537 t.Errorf("Expected to find %s in container environment:\n%s\n\n", search, strings.Join(container.Config.Env, "\n")) 1538 } 1539 } 1540 } 1541 1542 func TestDockerDriver_User(t *testing.T) { 1543 if !tu.IsTravis() { 1544 t.Parallel() 1545 } 1546 if !testutil.DockerIsConnected(t) { 1547 t.Skip("Docker not connected") 1548 } 1549 1550 task := &structs.Task{ 1551 Name: "redis-demo", 1552 User: "alice", 1553 Driver: "docker", 1554 Config: map[string]interface{}{ 1555 "image": "busybox", 1556 "load": "busybox.tar", 1557 "command": "/bin/sleep", 1558 "args": []string{"10000"}, 1559 }, 1560 Resources: &structs.Resources{ 1561 MemoryMB: 256, 1562 CPU: 512, 1563 }, 1564 LogConfig: &structs.LogConfig{ 1565 MaxFiles: 10, 1566 MaxFileSizeMB: 10, 1567 }, 1568 } 1569 1570 ctx := testDockerDriverContexts(t, task) 1571 //ctx.DriverCtx.config.Options = map[string]string{"docker.cleanup.image": "false"} 1572 driver := NewDockerDriver(ctx.DriverCtx) 1573 defer ctx.AllocDir.Destroy() 1574 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 1575 1576 _, err := driver.Prestart(ctx.ExecCtx, task) 1577 if err != nil { 1578 t.Fatalf("error in prestart: %v", err) 1579 } 1580 1581 // It should fail because the user "alice" does not exist on the given 1582 // image. 1583 resp, err := driver.Start(ctx.ExecCtx, task) 1584 if err == nil { 1585 resp.Handle.Kill() 1586 t.Fatalf("Should've failed") 1587 } 1588 1589 if !strings.Contains(err.Error(), "alice") { 1590 t.Fatalf("Expected failure string not found, found %q instead", err.Error()) 1591 } 1592 } 1593 1594 func TestDockerDriver_CleanupContainer(t *testing.T) { 1595 if !tu.IsTravis() { 1596 t.Parallel() 1597 } 1598 if !testutil.DockerIsConnected(t) { 1599 t.Skip("Docker not connected") 1600 } 1601 1602 task := &structs.Task{ 1603 Name: "redis-demo", 1604 Driver: "docker", 1605 Config: map[string]interface{}{ 1606 "image": "busybox", 1607 "load": "busybox.tar", 1608 "command": "/bin/echo", 1609 "args": []string{"hello"}, 1610 }, 1611 Resources: &structs.Resources{ 1612 MemoryMB: 256, 1613 CPU: 512, 1614 }, 1615 LogConfig: &structs.LogConfig{ 1616 MaxFiles: 10, 1617 MaxFileSizeMB: 10, 1618 }, 1619 } 1620 1621 _, handle, cleanup := dockerSetup(t, task) 1622 defer cleanup() 1623 1624 // Update should be a no-op 1625 err := handle.Update(task) 1626 if err != nil { 1627 t.Fatalf("err: %v", err) 1628 } 1629 1630 select { 1631 case res := <-handle.WaitCh(): 1632 if !res.Successful() { 1633 t.Fatalf("err: %v", res) 1634 } 1635 1636 time.Sleep(3 * time.Second) 1637 1638 // Ensure that the container isn't present 1639 _, err := client.InspectContainer(handle.containerID) 1640 if err == nil { 1641 t.Fatalf("expected to not get container") 1642 } 1643 1644 case <-time.After(time.Duration(tu.TestMultiplier()*5) * time.Second): 1645 t.Fatalf("timeout") 1646 } 1647 } 1648 1649 func TestDockerDriver_Stats(t *testing.T) { 1650 if !tu.IsTravis() { 1651 t.Parallel() 1652 } 1653 if !testutil.DockerIsConnected(t) { 1654 t.Skip("Docker not connected") 1655 } 1656 1657 task := &structs.Task{ 1658 Name: "sleep", 1659 Driver: "docker", 1660 Config: map[string]interface{}{ 1661 "image": "busybox", 1662 "load": "busybox.tar", 1663 "command": "/bin/sleep", 1664 "args": []string{"100"}, 1665 }, 1666 LogConfig: &structs.LogConfig{ 1667 MaxFiles: 10, 1668 MaxFileSizeMB: 10, 1669 }, 1670 Resources: basicResources, 1671 } 1672 1673 _, handle, cleanup := dockerSetup(t, task) 1674 defer cleanup() 1675 1676 waitForExist(t, client, handle) 1677 1678 go func() { 1679 time.Sleep(3 * time.Second) 1680 ru, err := handle.Stats() 1681 if err != nil { 1682 t.Fatalf("err: %v", err) 1683 } 1684 if ru.ResourceUsage == nil { 1685 handle.Kill() 1686 t.Fatalf("expected resource usage") 1687 } 1688 err = handle.Kill() 1689 if err != nil { 1690 t.Fatalf("err: %v", err) 1691 } 1692 }() 1693 1694 select { 1695 case res := <-handle.WaitCh(): 1696 if res.Successful() { 1697 t.Fatalf("should err: %v", res) 1698 } 1699 case <-time.After(time.Duration(tu.TestMultiplier()*10) * time.Second): 1700 t.Fatalf("timeout") 1701 } 1702 } 1703 1704 func setupDockerVolumes(t *testing.T, cfg *config.Config, hostpath string) (*structs.Task, Driver, *ExecContext, string, func()) { 1705 if !testutil.DockerIsConnected(t) { 1706 t.Skip("Docker not connected") 1707 } 1708 1709 randfn := fmt.Sprintf("test-%d", rand.Int()) 1710 hostfile := filepath.Join(hostpath, randfn) 1711 containerPath := "/mnt/vol" 1712 containerFile := filepath.Join(containerPath, randfn) 1713 1714 task := &structs.Task{ 1715 Name: "ls", 1716 Env: map[string]string{"VOL_PATH": containerPath}, 1717 Driver: "docker", 1718 Config: map[string]interface{}{ 1719 "image": "busybox", 1720 "load": "busybox.tar", 1721 "command": "touch", 1722 "args": []string{containerFile}, 1723 "volumes": []string{fmt.Sprintf("%s:${VOL_PATH}", hostpath)}, 1724 }, 1725 LogConfig: &structs.LogConfig{ 1726 MaxFiles: 10, 1727 MaxFileSizeMB: 10, 1728 }, 1729 Resources: basicResources, 1730 } 1731 1732 // Build alloc and task directory structure 1733 allocDir := allocdir.NewAllocDir(testLogger(), filepath.Join(cfg.AllocDir, uuid.Generate())) 1734 if err := allocDir.Build(); err != nil { 1735 t.Fatalf("failed to build alloc dir: %v", err) 1736 } 1737 taskDir := allocDir.NewTaskDir(task.Name) 1738 if err := taskDir.Build(false, nil, cstructs.FSIsolationImage); err != nil { 1739 allocDir.Destroy() 1740 t.Fatalf("failed to build task dir: %v", err) 1741 } 1742 copyImage(t, taskDir, "busybox.tar") 1743 1744 // Setup driver 1745 alloc := mock.Alloc() 1746 logger := testLogger() 1747 emitter := func(m string, args ...interface{}) { 1748 logger.Printf("[EVENT] "+m, args...) 1749 } 1750 driverCtx := NewDriverContext(alloc.Job.Name, alloc.TaskGroup, task.Name, alloc.ID, cfg, cfg.Node, testLogger(), emitter) 1751 driver := NewDockerDriver(driverCtx) 1752 1753 // Setup execCtx 1754 envBuilder := env.NewBuilder(cfg.Node, alloc, task, cfg.Region) 1755 SetEnvvars(envBuilder, driver.FSIsolation(), taskDir, cfg) 1756 execCtx := NewExecContext(taskDir, envBuilder.Build()) 1757 1758 // Setup cleanup function 1759 cleanup := func() { 1760 allocDir.Destroy() 1761 if filepath.IsAbs(hostpath) { 1762 os.RemoveAll(hostpath) 1763 } 1764 } 1765 return task, driver, execCtx, hostfile, cleanup 1766 } 1767 1768 func TestDockerDriver_VolumesDisabled(t *testing.T) { 1769 if !tu.IsTravis() { 1770 t.Parallel() 1771 } 1772 if !testutil.DockerIsConnected(t) { 1773 t.Skip("Docker not connected") 1774 } 1775 1776 cfg := testConfig(t) 1777 cfg.Options = map[string]string{ 1778 dockerVolumesConfigOption: "false", 1779 "docker.cleanup.image": "false", 1780 } 1781 1782 { 1783 tmpvol, err := ioutil.TempDir("", "nomadtest_docker_volumesdisabled") 1784 if err != nil { 1785 t.Fatalf("error creating temporary dir: %v", err) 1786 } 1787 1788 task, driver, execCtx, _, cleanup := setupDockerVolumes(t, cfg, tmpvol) 1789 defer cleanup() 1790 1791 _, err = driver.Prestart(execCtx, task) 1792 if err != nil { 1793 t.Fatalf("error in prestart: %v", err) 1794 } 1795 if _, err := driver.Start(execCtx, task); err == nil { 1796 t.Fatalf("Started driver successfully when volumes should have been disabled.") 1797 } 1798 } 1799 1800 // Relative paths should still be allowed 1801 { 1802 task, driver, execCtx, fn, cleanup := setupDockerVolumes(t, cfg, ".") 1803 defer cleanup() 1804 1805 _, err := driver.Prestart(execCtx, task) 1806 if err != nil { 1807 t.Fatalf("error in prestart: %v", err) 1808 } 1809 resp, err := driver.Start(execCtx, task) 1810 if err != nil { 1811 t.Fatalf("err: %v", err) 1812 } 1813 defer resp.Handle.Kill() 1814 1815 select { 1816 case res := <-resp.Handle.WaitCh(): 1817 if !res.Successful() { 1818 t.Fatalf("unexpected err: %v", res) 1819 } 1820 case <-time.After(time.Duration(tu.TestMultiplier()*10) * time.Second): 1821 t.Fatalf("timeout") 1822 } 1823 1824 if _, err := ioutil.ReadFile(filepath.Join(execCtx.TaskDir.Dir, fn)); err != nil { 1825 t.Fatalf("unexpected error reading %s: %v", fn, err) 1826 } 1827 } 1828 1829 // Volume Drivers should be rejected (error) 1830 { 1831 task, driver, execCtx, _, cleanup := setupDockerVolumes(t, cfg, "fake_flocker_vol") 1832 defer cleanup() 1833 task.Config["volume_driver"] = "flocker" 1834 1835 if _, err := driver.Prestart(execCtx, task); err != nil { 1836 t.Fatalf("error in prestart: %v", err) 1837 } 1838 if _, err := driver.Start(execCtx, task); err == nil { 1839 t.Fatalf("Started driver successfully when volume drivers should have been disabled.") 1840 } 1841 } 1842 1843 } 1844 1845 func TestDockerDriver_VolumesEnabled(t *testing.T) { 1846 if !tu.IsTravis() { 1847 t.Parallel() 1848 } 1849 if !testutil.DockerIsConnected(t) { 1850 t.Skip("Docker not connected") 1851 } 1852 1853 cfg := testConfig(t) 1854 1855 tmpvol, err := ioutil.TempDir("", "nomadtest_docker_volumesenabled") 1856 if err != nil { 1857 t.Fatalf("error creating temporary dir: %v", err) 1858 } 1859 1860 // Evaluate symlinks so it works on MacOS 1861 tmpvol, err = filepath.EvalSymlinks(tmpvol) 1862 if err != nil { 1863 t.Fatalf("error evaluating symlinks: %v", err) 1864 } 1865 1866 task, driver, execCtx, hostpath, cleanup := setupDockerVolumes(t, cfg, tmpvol) 1867 defer cleanup() 1868 1869 _, err = driver.Prestart(execCtx, task) 1870 if err != nil { 1871 t.Fatalf("error in prestart: %v", err) 1872 } 1873 resp, err := driver.Start(execCtx, task) 1874 if err != nil { 1875 t.Fatalf("Failed to start docker driver: %v", err) 1876 } 1877 defer resp.Handle.Kill() 1878 1879 select { 1880 case res := <-resp.Handle.WaitCh(): 1881 if !res.Successful() { 1882 t.Fatalf("unexpected err: %v", res) 1883 } 1884 case <-time.After(time.Duration(tu.TestMultiplier()*10) * time.Second): 1885 t.Fatalf("timeout") 1886 } 1887 1888 if _, err := ioutil.ReadFile(hostpath); err != nil { 1889 t.Fatalf("unexpected error reading %s: %v", hostpath, err) 1890 } 1891 } 1892 1893 func TestDockerDriver_Mounts(t *testing.T) { 1894 if !tu.IsTravis() { 1895 t.Parallel() 1896 } 1897 if !testutil.DockerIsConnected(t) { 1898 t.Skip("Docker not connected") 1899 } 1900 1901 goodMount := map[string]interface{}{ 1902 "target": "/nomad", 1903 "volume_options": []interface{}{ 1904 map[string]interface{}{ 1905 "labels": []interface{}{ 1906 map[string]string{"foo": "bar"}, 1907 }, 1908 "driver_config": []interface{}{ 1909 map[string]interface{}{ 1910 "name": "local", 1911 "options": []interface{}{ 1912 map[string]interface{}{ 1913 "foo": "bar", 1914 }, 1915 }, 1916 }, 1917 }, 1918 }, 1919 }, 1920 "readonly": true, 1921 "source": "test", 1922 } 1923 1924 cases := []struct { 1925 Name string 1926 Mounts []interface{} 1927 Error string 1928 }{ 1929 { 1930 Name: "good-one", 1931 Error: "", 1932 Mounts: []interface{}{goodMount}, 1933 }, 1934 { 1935 Name: "good-many", 1936 Error: "", 1937 Mounts: []interface{}{goodMount, goodMount, goodMount}, 1938 }, 1939 { 1940 Name: "multiple volume options", 1941 Error: "Only one volume_options stanza allowed", 1942 Mounts: []interface{}{ 1943 map[string]interface{}{ 1944 "target": "/nomad", 1945 "volume_options": []interface{}{ 1946 map[string]interface{}{ 1947 "driver_config": []interface{}{ 1948 map[string]interface{}{ 1949 "name": "local", 1950 }, 1951 }, 1952 }, 1953 map[string]interface{}{ 1954 "driver_config": []interface{}{ 1955 map[string]interface{}{ 1956 "name": "local", 1957 }, 1958 }, 1959 }, 1960 }, 1961 }, 1962 }, 1963 }, 1964 { 1965 Name: "multiple driver configs", 1966 Error: "volume driver config may only be specified once", 1967 Mounts: []interface{}{ 1968 map[string]interface{}{ 1969 "target": "/nomad", 1970 "volume_options": []interface{}{ 1971 map[string]interface{}{ 1972 "driver_config": []interface{}{ 1973 map[string]interface{}{ 1974 "name": "local", 1975 }, 1976 map[string]interface{}{ 1977 "name": "local", 1978 }, 1979 }, 1980 }, 1981 }, 1982 }, 1983 }, 1984 }, 1985 { 1986 Name: "multiple volume labels", 1987 Error: "labels may only be", 1988 Mounts: []interface{}{ 1989 map[string]interface{}{ 1990 "target": "/nomad", 1991 "volume_options": []interface{}{ 1992 map[string]interface{}{ 1993 "labels": []interface{}{ 1994 map[string]string{"foo": "bar"}, 1995 map[string]string{"baz": "bam"}, 1996 }, 1997 }, 1998 }, 1999 }, 2000 }, 2001 }, 2002 { 2003 Name: "multiple driver options", 2004 Error: "driver options may only", 2005 Mounts: []interface{}{ 2006 map[string]interface{}{ 2007 "target": "/nomad", 2008 "volume_options": []interface{}{ 2009 map[string]interface{}{ 2010 "driver_config": []interface{}{ 2011 map[string]interface{}{ 2012 "name": "local", 2013 "options": []interface{}{ 2014 map[string]interface{}{ 2015 "foo": "bar", 2016 }, 2017 map[string]interface{}{ 2018 "bam": "bar", 2019 }, 2020 }, 2021 }, 2022 }, 2023 }, 2024 }, 2025 }, 2026 }, 2027 }, 2028 } 2029 2030 task := &structs.Task{ 2031 Name: "redis-demo", 2032 Driver: "docker", 2033 Config: map[string]interface{}{ 2034 "image": "busybox", 2035 "load": "busybox.tar", 2036 "command": "/bin/sleep", 2037 "args": []string{"10000"}, 2038 }, 2039 Resources: &structs.Resources{ 2040 MemoryMB: 256, 2041 CPU: 512, 2042 }, 2043 LogConfig: &structs.LogConfig{ 2044 MaxFiles: 10, 2045 MaxFileSizeMB: 10, 2046 }, 2047 } 2048 2049 for _, c := range cases { 2050 t.Run(c.Name, func(t *testing.T) { 2051 // Build the task 2052 task.Config["mounts"] = c.Mounts 2053 2054 ctx := testDockerDriverContexts(t, task) 2055 driver := NewDockerDriver(ctx.DriverCtx) 2056 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 2057 defer ctx.AllocDir.Destroy() 2058 2059 _, err := driver.Prestart(ctx.ExecCtx, task) 2060 if err == nil && c.Error != "" { 2061 t.Fatalf("expected error: %v", c.Error) 2062 } else if err != nil { 2063 if c.Error == "" { 2064 t.Fatalf("unexpected error in prestart: %v", err) 2065 } else if !strings.Contains(err.Error(), c.Error) { 2066 t.Fatalf("expected error %q; got %v", c.Error, err) 2067 } 2068 } 2069 }) 2070 } 2071 } 2072 2073 // TestDockerDriver_Cleanup ensures Cleanup removes only downloaded images. 2074 func TestDockerDriver_Cleanup(t *testing.T) { 2075 if !tu.IsTravis() { 2076 t.Parallel() 2077 } 2078 if !testutil.DockerIsConnected(t) { 2079 t.Skip("Docker not connected") 2080 } 2081 2082 imageName := "hello-world:latest" 2083 task := &structs.Task{ 2084 Name: "cleanup_test", 2085 Driver: "docker", 2086 Config: map[string]interface{}{ 2087 "image": imageName, 2088 }, 2089 } 2090 tctx := testDockerDriverContexts(t, task) 2091 defer tctx.AllocDir.Destroy() 2092 2093 // Run Prestart 2094 driver := NewDockerDriver(tctx.DriverCtx).(*DockerDriver) 2095 resp, err := driver.Prestart(tctx.ExecCtx, task) 2096 if err != nil { 2097 t.Fatalf("error in prestart: %v", err) 2098 } 2099 res := resp.CreatedResources 2100 if len(res.Resources) == 0 || len(res.Resources[dockerImageResKey]) == 0 { 2101 t.Fatalf("no created resources: %#v", res) 2102 } 2103 2104 // Cleanup 2105 rescopy := res.Copy() 2106 if err := driver.Cleanup(tctx.ExecCtx, rescopy); err != nil { 2107 t.Fatalf("Cleanup failed: %v", err) 2108 } 2109 2110 // Make sure rescopy is updated 2111 if len(rescopy.Resources) > 0 { 2112 t.Errorf("Cleanup should have cleared resource map: %#v", rescopy.Resources) 2113 } 2114 2115 // Ensure image was removed 2116 tu.WaitForResult(func() (bool, error) { 2117 if _, err := client.InspectImage(driver.driverConfig.ImageName); err == nil { 2118 return false, fmt.Errorf("image exists but should have been removed. Does another %v container exist?", imageName) 2119 } 2120 2121 return true, nil 2122 }, func(err error) { 2123 t.Fatalf("err: %v", err) 2124 }) 2125 2126 // The image doesn't exist which shouldn't be an error when calling 2127 // Cleanup, so call it again to make sure. 2128 if err := driver.Cleanup(tctx.ExecCtx, res.Copy()); err != nil { 2129 t.Fatalf("Cleanup failed: %v", err) 2130 } 2131 } 2132 2133 func copyImage(t *testing.T, taskDir *allocdir.TaskDir, image string) { 2134 dst := filepath.Join(taskDir.LocalDir, image) 2135 copyFile(filepath.Join("./test-resources/docker", image), dst, t) 2136 } 2137 2138 func TestDockerDriver_AuthConfiguration(t *testing.T) { 2139 if !tu.IsTravis() { 2140 t.Parallel() 2141 } 2142 if !testutil.DockerIsConnected(t) { 2143 t.Skip("Docker not connected") 2144 } 2145 2146 path := "./test-resources/docker/auth.json" 2147 cases := []struct { 2148 Repo string 2149 AuthConfig *docker.AuthConfiguration 2150 }{ 2151 { 2152 Repo: "lolwhat.com/what:1337", 2153 AuthConfig: nil, 2154 }, 2155 { 2156 Repo: "redis:3.2", 2157 AuthConfig: &docker.AuthConfiguration{ 2158 Username: "test", 2159 Password: "1234", 2160 Email: "", 2161 ServerAddress: "https://index.docker.io/v1/", 2162 }, 2163 }, 2164 { 2165 Repo: "quay.io/redis:3.2", 2166 AuthConfig: &docker.AuthConfiguration{ 2167 Username: "test", 2168 Password: "5678", 2169 Email: "", 2170 ServerAddress: "quay.io", 2171 }, 2172 }, 2173 { 2174 Repo: "other.io/redis:3.2", 2175 AuthConfig: &docker.AuthConfiguration{ 2176 Username: "test", 2177 Password: "abcd", 2178 Email: "", 2179 ServerAddress: "https://other.io/v1/", 2180 }, 2181 }, 2182 } 2183 2184 for i, c := range cases { 2185 act, err := authFromDockerConfig(path)(c.Repo) 2186 if err != nil { 2187 t.Fatalf("Test %d failed: %v", i+1, err) 2188 } 2189 2190 if !reflect.DeepEqual(act, c.AuthConfig) { 2191 t.Fatalf("Test %d failed: Unexpected auth config: got %+v; want %+v", i+1, act, c.AuthConfig) 2192 } 2193 } 2194 } 2195 2196 func TestDockerDriver_OOMKilled(t *testing.T) { 2197 if !tu.IsTravis() { 2198 t.Parallel() 2199 } 2200 if !testutil.DockerIsConnected(t) { 2201 t.Skip("Docker not connected") 2202 } 2203 2204 task := &structs.Task{ 2205 Name: "oom-killed", 2206 Driver: "docker", 2207 Config: map[string]interface{}{ 2208 "image": "busybox", 2209 "load": "busybox.tar", 2210 "command": "sh", 2211 // Incrementally creates a bigger and bigger variable. 2212 "args": []string{"-c", "x=a; while true; do eval x='$x$x'; done"}, 2213 }, 2214 LogConfig: &structs.LogConfig{ 2215 MaxFiles: 10, 2216 MaxFileSizeMB: 10, 2217 }, 2218 Resources: &structs.Resources{ 2219 CPU: 250, 2220 MemoryMB: 10, 2221 DiskMB: 20, 2222 Networks: []*structs.NetworkResource{}, 2223 }, 2224 } 2225 2226 _, handle, cleanup := dockerSetup(t, task) 2227 defer cleanup() 2228 2229 select { 2230 case res := <-handle.WaitCh(): 2231 if res.Successful() { 2232 t.Fatalf("expected error, but container exited successful") 2233 } 2234 2235 if res.Err.Error() != "OOM Killed" { 2236 t.Fatalf("not killed by OOM killer: %s", res.Err) 2237 } 2238 2239 t.Logf("Successfully killed by OOM killer") 2240 2241 case <-time.After(time.Duration(tu.TestMultiplier()*5) * time.Second): 2242 t.Fatalf("timeout") 2243 } 2244 } 2245 2246 func TestDockerDriver_Devices_IsInvalidConfig(t *testing.T) { 2247 if !tu.IsTravis() { 2248 t.Parallel() 2249 } 2250 if !testutil.DockerIsConnected(t) { 2251 t.Skip("Docker not connected") 2252 } 2253 2254 brokenConfigs := []interface{}{ 2255 map[string]interface{}{ 2256 "host_path": "", 2257 }, 2258 map[string]interface{}{ 2259 "host_path": "/dev/sda1", 2260 "cgroup_permissions": "rxb", 2261 }, 2262 } 2263 2264 test_cases := []struct { 2265 deviceConfig interface{} 2266 err error 2267 }{ 2268 {[]interface{}{brokenConfigs[0]}, fmt.Errorf("host path must be set in configuration for devices")}, 2269 {[]interface{}{brokenConfigs[1]}, fmt.Errorf("invalid cgroup permission string: \"rxb\"")}, 2270 } 2271 2272 for _, tc := range test_cases { 2273 task, _, _ := dockerTask(t) 2274 task.Config["devices"] = tc.deviceConfig 2275 2276 ctx := testDockerDriverContexts(t, task) 2277 driver := NewDockerDriver(ctx.DriverCtx) 2278 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 2279 defer ctx.AllocDir.Destroy() 2280 2281 if _, err := driver.Prestart(ctx.ExecCtx, task); err == nil || err.Error() != tc.err.Error() { 2282 t.Fatalf("error expected in prestart, got %v, expected %v", err, tc.err) 2283 } 2284 } 2285 } 2286 2287 func TestDockerDriver_Device_Success(t *testing.T) { 2288 if !tu.IsTravis() { 2289 t.Parallel() 2290 } 2291 if !testutil.DockerIsConnected(t) { 2292 t.Skip("Docker not connected") 2293 } 2294 2295 if runtime.GOOS != "linux" { 2296 t.Skip("test device mounts only on linux") 2297 } 2298 2299 hostPath := "/dev/random" 2300 containerPath := "/dev/myrandom" 2301 perms := "rwm" 2302 2303 expectedDevice := docker.Device{ 2304 PathOnHost: hostPath, 2305 PathInContainer: containerPath, 2306 CgroupPermissions: perms, 2307 } 2308 config := map[string]interface{}{ 2309 "host_path": hostPath, 2310 "container_path": containerPath, 2311 } 2312 2313 task, _, _ := dockerTask(t) 2314 task.Config["devices"] = []interface{}{config} 2315 2316 client, handle, cleanup := dockerSetup(t, task) 2317 defer cleanup() 2318 2319 waitForExist(t, client, handle) 2320 2321 container, err := client.InspectContainer(handle.ContainerID()) 2322 if err != nil { 2323 t.Fatalf("err: %v", err) 2324 } 2325 2326 assert.NotEmpty(t, container.HostConfig.Devices, "Expected one device") 2327 assert.Equal(t, expectedDevice, container.HostConfig.Devices[0], "Incorrect device ") 2328 } 2329 2330 func TestDockerDriver_Entrypoint(t *testing.T) { 2331 if !tu.IsTravis() { 2332 t.Parallel() 2333 } 2334 if !testutil.DockerIsConnected(t) { 2335 t.Skip("Docker not connected") 2336 } 2337 2338 entrypoint := []string{"/bin/sh", "-c"} 2339 task, _, _ := dockerTask(t) 2340 task.Config["entrypoint"] = entrypoint 2341 2342 client, handle, cleanup := dockerSetup(t, task) 2343 defer cleanup() 2344 2345 waitForExist(t, client, handle) 2346 2347 container, err := client.InspectContainer(handle.ContainerID()) 2348 if err != nil { 2349 t.Fatalf("err: %v", err) 2350 } 2351 2352 require.Len(t, container.Config.Entrypoint, 2, "Expected one entrypoint") 2353 require.Equal(t, entrypoint, container.Config.Entrypoint, "Incorrect entrypoint ") 2354 } 2355 2356 func TestDockerDriver_Kill(t *testing.T) { 2357 assert := assert.New(t) 2358 if !tu.IsTravis() { 2359 t.Parallel() 2360 } 2361 if !testutil.DockerIsConnected(t) { 2362 t.Skip("Docker not connected") 2363 } 2364 2365 // Tasks started with a signal that is not supported should not error 2366 task := &structs.Task{ 2367 Name: "nc-demo", 2368 Driver: "docker", 2369 KillSignal: "SIGKILL", 2370 Config: map[string]interface{}{ 2371 "load": "busybox.tar", 2372 "image": "busybox", 2373 "command": "/bin/nc", 2374 "args": []string{"-l", "127.0.0.1", "-p", "0"}, 2375 }, 2376 LogConfig: &structs.LogConfig{ 2377 MaxFiles: 10, 2378 MaxFileSizeMB: 10, 2379 }, 2380 Resources: basicResources, 2381 } 2382 2383 ctx := testDockerDriverContexts(t, task) 2384 defer ctx.AllocDir.Destroy() 2385 d := NewDockerDriver(ctx.DriverCtx) 2386 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 2387 2388 _, err := d.Prestart(ctx.ExecCtx, task) 2389 if err != nil { 2390 t.Fatalf("error in prestart: %v", err) 2391 } 2392 2393 resp, err := d.Start(ctx.ExecCtx, task) 2394 assert.Nil(err) 2395 assert.NotNil(resp.Handle) 2396 2397 handle := resp.Handle.(*DockerHandle) 2398 waitForExist(t, client, handle) 2399 err = handle.Kill() 2400 assert.Nil(err) 2401 } 2402 2403 func TestDockerDriver_ReadonlyRootfs(t *testing.T) { 2404 if !tu.IsTravis() { 2405 t.Parallel() 2406 } 2407 if !testutil.DockerIsConnected(t) { 2408 t.Skip("Docker not connected") 2409 } 2410 2411 task, _, _ := dockerTask(t) 2412 task.Config["readonly_rootfs"] = true 2413 2414 client, handle, cleanup := dockerSetup(t, task) 2415 defer cleanup() 2416 2417 waitForExist(t, client, handle) 2418 2419 container, err := client.InspectContainer(handle.ContainerID()) 2420 assert.Nil(t, err, "Error inspecting container: %v", err) 2421 2422 assert.True(t, container.HostConfig.ReadonlyRootfs, "ReadonlyRootfs option not set") 2423 } 2424 2425 // fakeDockerClient can be used in places that accept an interface for the 2426 // docker client such as createContainer. 2427 type fakeDockerClient struct{} 2428 2429 func (fakeDockerClient) CreateContainer(docker.CreateContainerOptions) (*docker.Container, error) { 2430 return nil, fmt.Errorf("volume is attached on another node") 2431 } 2432 func (fakeDockerClient) InspectContainer(id string) (*docker.Container, error) { 2433 panic("not implemented") 2434 } 2435 func (fakeDockerClient) ListContainers(docker.ListContainersOptions) ([]docker.APIContainers, error) { 2436 panic("not implemented") 2437 } 2438 func (fakeDockerClient) RemoveContainer(opts docker.RemoveContainerOptions) error { 2439 panic("not implemented") 2440 } 2441 2442 // TestDockerDriver_VolumeError asserts volume related errors when creating a 2443 // container are recoverable. 2444 func TestDockerDriver_VolumeError(t *testing.T) { 2445 if !tu.IsTravis() { 2446 t.Parallel() 2447 } 2448 2449 // setup 2450 task, _, _ := dockerTask(t) 2451 tctx := testDockerDriverContexts(t, task) 2452 driver := NewDockerDriver(tctx.DriverCtx).(*DockerDriver) 2453 driver.driverConfig = &DockerDriverConfig{ImageName: "test"} 2454 2455 // assert volume error is recoverable 2456 _, err := driver.createContainer(fakeDockerClient{}, docker.CreateContainerOptions{}) 2457 require.True(t, structs.IsRecoverable(err)) 2458 } 2459 2460 func TestDockerDriver_AdvertiseIPv6Address(t *testing.T) { 2461 if !tu.IsTravis() { 2462 t.Parallel() 2463 } 2464 if !testutil.DockerIsConnected(t) { 2465 t.Skip("Docker not connected") 2466 } 2467 2468 expectedPrefix := "2001:db8:1::242:ac11" 2469 expectedAdvertise := true 2470 task := &structs.Task{ 2471 Name: "nc-demo", 2472 Driver: "docker", 2473 Config: map[string]interface{}{ 2474 "image": "busybox", 2475 "load": "busybox.tar", 2476 "command": "/bin/nc", 2477 "args": []string{"-l", "127.0.0.1", "-p", "0"}, 2478 "advertise_ipv6_address": expectedAdvertise, 2479 }, 2480 Resources: &structs.Resources{ 2481 MemoryMB: 256, 2482 CPU: 512, 2483 }, 2484 LogConfig: &structs.LogConfig{ 2485 MaxFiles: 10, 2486 MaxFileSizeMB: 10, 2487 }, 2488 } 2489 2490 client := newTestDockerClient(t) 2491 2492 // Make sure IPv6 is enabled 2493 net, err := client.NetworkInfo("bridge") 2494 if err != nil { 2495 t.Skip("error retrieving bridge network information, skipping") 2496 } 2497 if net == nil || !net.EnableIPv6 { 2498 t.Skip("IPv6 not enabled on bridge network, skipping") 2499 } 2500 2501 tctx := testDockerDriverContexts(t, task) 2502 driver := NewDockerDriver(tctx.DriverCtx) 2503 copyImage(t, tctx.ExecCtx.TaskDir, "busybox.tar") 2504 defer tctx.AllocDir.Destroy() 2505 2506 presp, err := driver.Prestart(tctx.ExecCtx, task) 2507 defer driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 2508 if err != nil { 2509 t.Fatalf("Error in prestart: %v", err) 2510 } 2511 2512 sresp, err := driver.Start(tctx.ExecCtx, task) 2513 if err != nil { 2514 t.Fatalf("Error in start: %v", err) 2515 } 2516 2517 if sresp.Handle == nil { 2518 t.Fatalf("handle is nil\nStack\n%s", debug.Stack()) 2519 } 2520 2521 assert.Equal(t, expectedAdvertise, sresp.Network.AutoAdvertise, "Wrong autoadvertise. Expect: %s, got: %s", expectedAdvertise, sresp.Network.AutoAdvertise) 2522 2523 if !strings.HasPrefix(sresp.Network.IP, expectedPrefix) { 2524 t.Fatalf("Got IP address %q want ip address with prefix %q", sresp.Network.IP, expectedPrefix) 2525 } 2526 2527 defer sresp.Handle.Kill() 2528 handle := sresp.Handle.(*DockerHandle) 2529 2530 waitForExist(t, client, handle) 2531 2532 container, err := client.InspectContainer(handle.ContainerID()) 2533 if err != nil { 2534 t.Fatalf("Error inspecting container: %v", err) 2535 } 2536 2537 if !strings.HasPrefix(container.NetworkSettings.GlobalIPv6Address, expectedPrefix) { 2538 t.Fatalf("Got GlobalIPv6address %s want GlobalIPv6address with prefix %s", expectedPrefix, container.NetworkSettings.GlobalIPv6Address) 2539 } 2540 } 2541 2542 func TestParseDockerImage(t *testing.T) { 2543 tests := []struct { 2544 Image string 2545 Repo string 2546 Tag string 2547 }{ 2548 {"library/hello-world:1.0", "library/hello-world", "1.0"}, 2549 {"library/hello-world", "library/hello-world", "latest"}, 2550 {"library/hello-world:latest", "library/hello-world", "latest"}, 2551 {"library/hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77", "library/hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77", ""}, 2552 } 2553 for _, test := range tests { 2554 t.Run(test.Image, func(t *testing.T) { 2555 repo, tag := parseDockerImage(test.Image) 2556 require.Equal(t, test.Repo, repo) 2557 require.Equal(t, test.Tag, tag) 2558 }) 2559 } 2560 } 2561 2562 func TestDockerImageRef(t *testing.T) { 2563 tests := []struct { 2564 Image string 2565 Repo string 2566 Tag string 2567 }{ 2568 {"library/hello-world:1.0", "library/hello-world", "1.0"}, 2569 {"library/hello-world:latest", "library/hello-world", "latest"}, 2570 {"library/hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77", "library/hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77", ""}, 2571 } 2572 for _, test := range tests { 2573 t.Run(test.Image, func(t *testing.T) { 2574 image := dockerImageRef(test.Repo, test.Tag) 2575 require.Equal(t, test.Image, image) 2576 }) 2577 } 2578 }