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