github.com/anuvu/nomad@v0.8.7-atom1/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 "syscall" 16 "testing" 17 "time" 18 19 docker "github.com/fsouza/go-dockerclient" 20 "github.com/hashicorp/consul/lib/freeport" 21 sockaddr "github.com/hashicorp/go-sockaddr" 22 "github.com/hashicorp/nomad/client/allocdir" 23 "github.com/hashicorp/nomad/client/config" 24 "github.com/hashicorp/nomad/client/driver/env" 25 "github.com/hashicorp/nomad/client/fingerprint" 26 cstructs "github.com/hashicorp/nomad/client/structs" 27 "github.com/hashicorp/nomad/client/testutil" 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, testLogger(), 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, testLogger(), 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()*10) * 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(testLogger(), 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 := testLogger() 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, testLogger(), 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 // TestDockerDriver_VolumesMounts_Ok asserts Docker can run a task with volume 1895 // mounts. 1896 func TestDockerDriver_VolumeMounts_Ok(t *testing.T) { 1897 if !tu.IsTravis() { 1898 t.Parallel() 1899 } 1900 if !testutil.DockerIsConnected(t) { 1901 t.Skip("Docker not connected") 1902 } 1903 1904 cfg := testConfig(t) 1905 1906 tmpvol, err := ioutil.TempDir("", "nomadtest_docker_volumemounts") 1907 if err != nil { 1908 t.Fatalf("error creating temporary dir: %v", err) 1909 } 1910 1911 // Evaluate symlinks so it works on MacOS 1912 tmpvol, err = filepath.EvalSymlinks(tmpvol) 1913 if err != nil { 1914 t.Fatalf("error evaluating symlinks: %v", err) 1915 } 1916 1917 // Create test volume 1918 volName := fmt.Sprintf("nomad_testvol_%s", uuid.Generate()[:4]) 1919 dockerClient := newTestDockerClient(t) 1920 dockerVol, err := dockerClient.CreateVolume(docker.CreateVolumeOptions{ 1921 Name: volName, 1922 }) 1923 require.NoError(t, err) 1924 defer dockerClient.RemoveVolume(volName) 1925 1926 task, driver, execCtx, _, cleanup := setupDockerVolumes(t, cfg, tmpvol) 1927 defer cleanup() 1928 1929 // Alter task to test volume mounts, not the old volumes config 1930 delete(task.Config, "volumes") 1931 task.Config["args"] = []string{"${VOL_PATH}/foo"} 1932 task.Config["mounts"] = []interface{}{ 1933 map[string]interface{}{ 1934 // omit `type` to make sure defaulting works 1935 "target": "${VOL_PATH}", 1936 "source": volName, 1937 }, 1938 } 1939 1940 _, err = driver.Prestart(execCtx, task) 1941 if err != nil { 1942 t.Fatalf("error in prestart: %v", err) 1943 } 1944 resp, err := driver.Start(execCtx, task) 1945 if err != nil { 1946 t.Fatalf("Failed to start docker driver: %v", err) 1947 } 1948 defer resp.Handle.Kill() 1949 1950 select { 1951 case res := <-resp.Handle.WaitCh(): 1952 if !res.Successful() { 1953 t.Fatalf("unexpected err: %v", res) 1954 } 1955 case <-time.After(time.Duration(tu.TestMultiplier()*10) * time.Second): 1956 t.Fatalf("timeout") 1957 } 1958 1959 // Reading from the docker volume host path requires root 1960 if syscall.Geteuid() != 0 { 1961 t.Skip("Must run as root on Unix") 1962 } 1963 hostfile := filepath.Join(dockerVol.Mountpoint, "foo") 1964 if _, err := ioutil.ReadFile(hostfile); err != nil { 1965 t.Fatalf("unexpected error reading %s: %v", hostfile, err) 1966 } 1967 } 1968 1969 func TestDockerDriver_VolumeMounts_Validation(t *testing.T) { 1970 if !tu.IsTravis() { 1971 t.Parallel() 1972 } 1973 if !testutil.DockerIsConnected(t) { 1974 t.Skip("Docker not connected") 1975 } 1976 1977 goodMount := map[string]interface{}{ 1978 "type": "volume", 1979 "target": "/nomad", 1980 "volume_options": []interface{}{ 1981 map[string]interface{}{ 1982 "labels": []interface{}{ 1983 map[string]string{"foo": "bar"}, 1984 }, 1985 "driver_config": []interface{}{ 1986 map[string]interface{}{ 1987 "name": "local", 1988 "options": []interface{}{ 1989 map[string]interface{}{ 1990 "foo": "bar", 1991 }, 1992 }, 1993 }, 1994 }, 1995 }, 1996 }, 1997 "readonly": true, 1998 "source": "test", 1999 } 2000 2001 cases := []struct { 2002 Name string 2003 Mounts []interface{} 2004 Error string 2005 }{ 2006 { 2007 Name: "good-one", 2008 Error: "", 2009 Mounts: []interface{}{goodMount}, 2010 }, 2011 { 2012 Name: "good-many", 2013 Error: "", 2014 Mounts: []interface{}{goodMount, goodMount, goodMount}, 2015 }, 2016 { 2017 Name: "bind_options in bind mount error", 2018 Error: "bind_options invalid on \"volume\" mount type", 2019 Mounts: []interface{}{ 2020 map[string]interface{}{ 2021 "type": "volume", 2022 "target": "/nomad", 2023 "bind_options": []interface{}{ 2024 map[string]interface{}{ 2025 "propagation": "shared", 2026 }, 2027 }, 2028 }, 2029 }, 2030 }, 2031 { 2032 Name: "multiple volume options", 2033 Error: "only one volume_options stanza allowed", 2034 Mounts: []interface{}{ 2035 map[string]interface{}{ 2036 "type": "volume", 2037 "target": "/nomad", 2038 "volume_options": []interface{}{ 2039 map[string]interface{}{ 2040 "driver_config": []interface{}{ 2041 map[string]interface{}{ 2042 "name": "local", 2043 }, 2044 }, 2045 }, 2046 map[string]interface{}{ 2047 "driver_config": []interface{}{ 2048 map[string]interface{}{ 2049 "name": "local", 2050 }, 2051 }, 2052 }, 2053 }, 2054 }, 2055 }, 2056 }, 2057 { 2058 Name: "multiple driver configs", 2059 Error: "volume driver config may only be specified once", 2060 Mounts: []interface{}{ 2061 map[string]interface{}{ 2062 "type": "volume", 2063 "target": "/nomad", 2064 "volume_options": []interface{}{ 2065 map[string]interface{}{ 2066 "driver_config": []interface{}{ 2067 map[string]interface{}{ 2068 "name": "local", 2069 }, 2070 map[string]interface{}{ 2071 "name": "local", 2072 }, 2073 }, 2074 }, 2075 }, 2076 }, 2077 }, 2078 }, 2079 { 2080 Name: "multiple volume labels", 2081 Error: "labels may only be", 2082 Mounts: []interface{}{ 2083 map[string]interface{}{ 2084 "type": "volume", 2085 "target": "/nomad", 2086 "volume_options": []interface{}{ 2087 map[string]interface{}{ 2088 "labels": []interface{}{ 2089 map[string]string{"foo": "bar"}, 2090 map[string]string{"baz": "bam"}, 2091 }, 2092 }, 2093 }, 2094 }, 2095 }, 2096 }, 2097 { 2098 Name: "multiple driver options", 2099 Error: "driver options may only", 2100 Mounts: []interface{}{ 2101 map[string]interface{}{ 2102 "type": "volume", 2103 "target": "/nomad", 2104 "volume_options": []interface{}{ 2105 map[string]interface{}{ 2106 "driver_config": []interface{}{ 2107 map[string]interface{}{ 2108 "name": "local", 2109 "options": []interface{}{ 2110 map[string]interface{}{ 2111 "foo": "bar", 2112 }, 2113 map[string]interface{}{ 2114 "bam": "bar", 2115 }, 2116 }, 2117 }, 2118 }, 2119 }, 2120 }, 2121 }, 2122 }, 2123 }, 2124 } 2125 2126 task := &structs.Task{ 2127 Name: "redis-demo", 2128 Driver: "docker", 2129 Config: map[string]interface{}{ 2130 "image": "busybox", 2131 "load": "busybox.tar", 2132 "command": "/bin/sleep", 2133 "args": []string{"10000"}, 2134 }, 2135 Resources: &structs.Resources{ 2136 MemoryMB: 256, 2137 CPU: 512, 2138 }, 2139 LogConfig: &structs.LogConfig{ 2140 MaxFiles: 10, 2141 MaxFileSizeMB: 10, 2142 }, 2143 } 2144 2145 for _, c := range cases { 2146 t.Run(c.Name, func(t *testing.T) { 2147 // Build the task 2148 task.Config["mounts"] = c.Mounts 2149 2150 ctx := testDockerDriverContexts(t, task) 2151 driver := NewDockerDriver(ctx.DriverCtx) 2152 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 2153 defer ctx.AllocDir.Destroy() 2154 2155 _, err := driver.Prestart(ctx.ExecCtx, task) 2156 if err == nil && c.Error != "" { 2157 t.Fatalf("expected error: %v", c.Error) 2158 } else if err != nil { 2159 if c.Error == "" { 2160 t.Fatalf("unexpected error in prestart: %v", err) 2161 } else if !strings.Contains(err.Error(), c.Error) { 2162 t.Fatalf("expected error %q; got %v", c.Error, err) 2163 } 2164 } 2165 }) 2166 } 2167 } 2168 2169 func setupDockerBindMount(t *testing.T, cfg *config.Config, hostpath string) (*structs.Task, Driver, *ExecContext, string, func()) { 2170 if !testutil.DockerIsConnected(t) { 2171 t.Skip("Docker not connected") 2172 } 2173 2174 randfn := fmt.Sprintf("test-%d", rand.Int()) 2175 hostfile := filepath.Join(hostpath, randfn) 2176 containerPath := "/mnt/vol" 2177 containerFile := filepath.Join(containerPath, randfn) 2178 2179 task := &structs.Task{ 2180 Name: "ls", 2181 Driver: "docker", 2182 Config: map[string]interface{}{ 2183 "image": "busybox", 2184 "load": "busybox.tar", 2185 "command": "touch", 2186 "args": []string{containerFile}, 2187 "mounts": []interface{}{ 2188 map[string]interface{}{ 2189 "readonly": "false", 2190 "source": hostpath, 2191 "target": containerPath, 2192 "type": "bind", 2193 "bind_options": []interface{}{ 2194 map[string]interface{}{ 2195 "propagation": "shared", 2196 }, 2197 }, 2198 }, 2199 }, 2200 }, 2201 LogConfig: &structs.LogConfig{ 2202 MaxFiles: 10, 2203 MaxFileSizeMB: 10, 2204 }, 2205 Resources: basicResources, 2206 } 2207 2208 // Build alloc and task directory structure 2209 allocDir := allocdir.NewAllocDir(testLogger(), filepath.Join(cfg.AllocDir, uuid.Generate())) 2210 if err := allocDir.Build(); err != nil { 2211 t.Fatalf("failed to build alloc dir: %v", err) 2212 } 2213 taskDir := allocDir.NewTaskDir(task.Name) 2214 if err := taskDir.Build(false, nil, cstructs.FSIsolationImage); err != nil { 2215 allocDir.Destroy() 2216 t.Fatalf("failed to build task dir: %v", err) 2217 } 2218 copyImage(t, taskDir, "busybox.tar") 2219 2220 // Setup driver 2221 alloc := mock.Alloc() 2222 logger := testLogger() 2223 emitter := func(m string, args ...interface{}) { 2224 logger.Printf("[EVENT] "+m, args...) 2225 } 2226 driverCtx := NewDriverContext(alloc.Job.Name, alloc.TaskGroup, task.Name, alloc.ID, cfg, cfg.Node, testLogger(), emitter) 2227 driver := NewDockerDriver(driverCtx) 2228 2229 // Setup execCtx 2230 envBuilder := env.NewBuilder(cfg.Node, alloc, task, cfg.Region) 2231 SetEnvvars(envBuilder, driver.FSIsolation(), taskDir, cfg) 2232 execCtx := NewExecContext(taskDir, envBuilder.Build()) 2233 2234 // Setup cleanup function 2235 cleanup := func() { 2236 allocDir.Destroy() 2237 if filepath.IsAbs(hostpath) { 2238 os.RemoveAll(hostpath) 2239 } 2240 } 2241 return task, driver, execCtx, hostfile, cleanup 2242 } 2243 2244 func TestDockerDriver_BindMount_VolumesDisabled(t *testing.T) { 2245 if tu.IsTravis() { 2246 t.Skip("Need to upgrade to Ubuntu 16.04+") 2247 } else { 2248 t.Parallel() 2249 } 2250 if !testutil.DockerIsConnected(t) { 2251 t.Skip("Docker not connected") 2252 } 2253 2254 cfg := testConfig(t) 2255 cfg.Options = map[string]string{ 2256 dockerVolumesConfigOption: "false", 2257 "docker.cleanup.image": "false", 2258 } 2259 2260 { 2261 tmpvol, err := ioutil.TempDir("", "nomadtest_docker_volumesdisabled") 2262 if err != nil { 2263 t.Fatalf("error creating temporary dir: %v", err) 2264 } 2265 2266 task, driver, execCtx, _, cleanup := setupDockerBindMount(t, cfg, tmpvol) 2267 defer cleanup() 2268 2269 _, err = driver.Prestart(execCtx, task) 2270 if err != nil { 2271 t.Fatalf("error in prestart: %v", err) 2272 } 2273 if _, err := driver.Start(execCtx, task); err == nil { 2274 t.Fatalf("Started driver successfully when volumes should have been disabled.") 2275 } 2276 } 2277 2278 // Relative paths should still be allowed 2279 { 2280 task, driver, execCtx, fn, cleanup := setupDockerBindMount(t, cfg, ".") 2281 defer cleanup() 2282 2283 _, err := driver.Prestart(execCtx, task) 2284 if err != nil { 2285 t.Fatalf("error in prestart: %v", err) 2286 } 2287 resp, err := driver.Start(execCtx, task) 2288 if err != nil { 2289 t.Fatalf("err: %v", err) 2290 } 2291 defer resp.Handle.Kill() 2292 2293 select { 2294 case res := <-resp.Handle.WaitCh(): 2295 if !res.Successful() { 2296 t.Fatalf("unexpected err: %v", res) 2297 } 2298 case <-time.After(time.Duration(tu.TestMultiplier()*10) * time.Second): 2299 t.Fatalf("timeout") 2300 } 2301 2302 if _, err := ioutil.ReadFile(filepath.Join(execCtx.TaskDir.Dir, fn)); err != nil { 2303 t.Fatalf("unexpected error reading %s: %v", fn, err) 2304 } 2305 } 2306 } 2307 2308 func TestDockerDriver_BindMount_VolumesEnabled(t *testing.T) { 2309 if tu.IsTravis() { 2310 t.Skip("Need to upgrade to Ubuntu 16.04+") 2311 } else { 2312 t.Parallel() 2313 } 2314 if !testutil.DockerIsConnected(t) { 2315 t.Skip("Docker not connected") 2316 } 2317 2318 cfg := testConfig(t) 2319 2320 tmpvol, err := ioutil.TempDir("", "nomadtest_docker_volumesenabled") 2321 if err != nil { 2322 t.Fatalf("error creating temporary dir: %v", err) 2323 } 2324 2325 // Evaluate symlinks so it works on MacOS 2326 tmpvol, err = filepath.EvalSymlinks(tmpvol) 2327 if err != nil { 2328 t.Fatalf("error evaluating symlinks: %v", err) 2329 } 2330 2331 task, driver, execCtx, hostpath, cleanup := setupDockerBindMount(t, cfg, tmpvol) 2332 defer cleanup() 2333 2334 _, err = driver.Prestart(execCtx, task) 2335 if err != nil { 2336 t.Fatalf("error in prestart: %v", err) 2337 } 2338 resp, err := driver.Start(execCtx, task) 2339 if err != nil { 2340 t.Fatalf("Failed to start docker driver: %v", err) 2341 } 2342 defer resp.Handle.Kill() 2343 2344 select { 2345 case res := <-resp.Handle.WaitCh(): 2346 if !res.Successful() { 2347 t.Fatalf("unexpected err: %v", res) 2348 } 2349 case <-time.After(time.Duration(tu.TestMultiplier()*10) * time.Second): 2350 t.Fatalf("timeout") 2351 } 2352 2353 if _, err := ioutil.ReadFile(hostpath); err != nil { 2354 t.Fatalf("unexpected error reading %s: %v", hostpath, err) 2355 } 2356 } 2357 2358 func TestDockerDriver_BindMount(t *testing.T) { 2359 if !tu.IsTravis() { 2360 t.Parallel() 2361 } 2362 if !testutil.DockerIsConnected(t) { 2363 t.Skip("Docker not connected") 2364 } 2365 2366 goodMount := map[string]interface{}{ 2367 "type": "bind", 2368 "target": "/nomad", 2369 "bind_options": []interface{}{ 2370 map[string]interface{}{ 2371 "propagation": "shared", 2372 }, 2373 }, 2374 "readonly": true, 2375 "source": "test", 2376 } 2377 2378 cases := []struct { 2379 Name string 2380 Mounts []interface{} 2381 Error string 2382 }{ 2383 { 2384 Name: "good-one", 2385 Error: "", 2386 Mounts: []interface{}{goodMount}, 2387 }, 2388 { 2389 Name: "good-many", 2390 Error: "", 2391 Mounts: []interface{}{goodMount, goodMount, goodMount}, 2392 }, 2393 { 2394 Name: "volume_options in bind mount error", 2395 Error: "volume_options invalid on \"bind\" mount type", 2396 Mounts: []interface{}{ 2397 map[string]interface{}{ 2398 "type": "bind", 2399 "target": "/nomad", 2400 "volume_options": []interface{}{ 2401 map[string]interface{}{ 2402 "driver_config": []interface{}{ 2403 map[string]interface{}{ 2404 "name": "local", 2405 }, 2406 }, 2407 }, 2408 }, 2409 }, 2410 }, 2411 }, 2412 { 2413 Name: "multiple bind options", 2414 Error: "only one bind_options stanza allowed", 2415 Mounts: []interface{}{ 2416 map[string]interface{}{ 2417 "type": "bind", 2418 "target": "/nomad", 2419 "bind_options": []interface{}{ 2420 map[string]interface{}{ 2421 "propagation": "shared", 2422 }, 2423 map[string]interface{}{ 2424 "propagation": "shared", 2425 }, 2426 }, 2427 }, 2428 }, 2429 }, 2430 } 2431 2432 task := &structs.Task{ 2433 Name: "redis-demo", 2434 Driver: "docker", 2435 Config: map[string]interface{}{ 2436 "image": "busybox", 2437 "load": "busybox.tar", 2438 "command": "/bin/sleep", 2439 "args": []string{"10000"}, 2440 }, 2441 Resources: &structs.Resources{ 2442 MemoryMB: 256, 2443 CPU: 512, 2444 }, 2445 LogConfig: &structs.LogConfig{ 2446 MaxFiles: 10, 2447 MaxFileSizeMB: 10, 2448 }, 2449 } 2450 2451 for _, c := range cases { 2452 t.Run(c.Name, func(t *testing.T) { 2453 // Build the task 2454 task.Config["mounts"] = c.Mounts 2455 2456 ctx := testDockerDriverContexts(t, task) 2457 driver := NewDockerDriver(ctx.DriverCtx) 2458 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 2459 defer ctx.AllocDir.Destroy() 2460 2461 _, err := driver.Prestart(ctx.ExecCtx, task) 2462 if err == nil && c.Error != "" { 2463 t.Fatalf("expected error: %v", c.Error) 2464 } else if err != nil { 2465 if c.Error == "" { 2466 t.Fatalf("unexpected error in prestart: %v", err) 2467 } else if !strings.Contains(err.Error(), c.Error) { 2468 t.Fatalf("expected error %q; got %v", c.Error, err) 2469 } 2470 } 2471 }) 2472 } 2473 } 2474 2475 // TestDockerDriver_Cleanup ensures Cleanup removes only downloaded images. 2476 func TestDockerDriver_Cleanup(t *testing.T) { 2477 if !tu.IsTravis() { 2478 t.Parallel() 2479 } 2480 if !testutil.DockerIsConnected(t) { 2481 t.Skip("Docker not connected") 2482 } 2483 2484 imageName := "hello-world:latest" 2485 task := &structs.Task{ 2486 Name: "cleanup_test", 2487 Driver: "docker", 2488 Config: map[string]interface{}{ 2489 "image": imageName, 2490 }, 2491 } 2492 tctx := testDockerDriverContexts(t, task) 2493 defer tctx.AllocDir.Destroy() 2494 2495 // Run Prestart 2496 driver := NewDockerDriver(tctx.DriverCtx).(*DockerDriver) 2497 resp, err := driver.Prestart(tctx.ExecCtx, task) 2498 if err != nil { 2499 t.Fatalf("error in prestart: %v", err) 2500 } 2501 res := resp.CreatedResources 2502 if len(res.Resources) == 0 || len(res.Resources[dockerImageResKey]) == 0 { 2503 t.Fatalf("no created resources: %#v", res) 2504 } 2505 2506 // Cleanup 2507 rescopy := res.Copy() 2508 if err := driver.Cleanup(tctx.ExecCtx, rescopy); err != nil { 2509 t.Fatalf("Cleanup failed: %v", err) 2510 } 2511 2512 // Make sure rescopy is updated 2513 if len(rescopy.Resources) > 0 { 2514 t.Errorf("Cleanup should have cleared resource map: %#v", rescopy.Resources) 2515 } 2516 2517 // Ensure image was removed 2518 tu.WaitForResult(func() (bool, error) { 2519 if _, err := client.InspectImage(driver.driverConfig.ImageName); err == nil { 2520 return false, fmt.Errorf("image exists but should have been removed. Does another %v container exist?", imageName) 2521 } 2522 2523 return true, nil 2524 }, func(err error) { 2525 t.Fatalf("err: %v", err) 2526 }) 2527 2528 // The image doesn't exist which shouldn't be an error when calling 2529 // Cleanup, so call it again to make sure. 2530 if err := driver.Cleanup(tctx.ExecCtx, res.Copy()); err != nil { 2531 t.Fatalf("Cleanup failed: %v", err) 2532 } 2533 } 2534 2535 func copyImage(t *testing.T, taskDir *allocdir.TaskDir, image string) { 2536 dst := filepath.Join(taskDir.LocalDir, image) 2537 copyFile(filepath.Join("./test-resources/docker", image), dst, t) 2538 } 2539 2540 func TestDockerDriver_AuthConfiguration(t *testing.T) { 2541 if !tu.IsTravis() { 2542 t.Parallel() 2543 } 2544 if !testutil.DockerIsConnected(t) { 2545 t.Skip("Docker not connected") 2546 } 2547 2548 path := "./test-resources/docker/auth.json" 2549 cases := []struct { 2550 Repo string 2551 AuthConfig *docker.AuthConfiguration 2552 }{ 2553 { 2554 Repo: "lolwhat.com/what:1337", 2555 AuthConfig: nil, 2556 }, 2557 { 2558 Repo: "redis:3.2", 2559 AuthConfig: &docker.AuthConfiguration{ 2560 Username: "test", 2561 Password: "1234", 2562 Email: "", 2563 ServerAddress: "https://index.docker.io/v1/", 2564 }, 2565 }, 2566 { 2567 Repo: "quay.io/redis:3.2", 2568 AuthConfig: &docker.AuthConfiguration{ 2569 Username: "test", 2570 Password: "5678", 2571 Email: "", 2572 ServerAddress: "quay.io", 2573 }, 2574 }, 2575 { 2576 Repo: "other.io/redis:3.2", 2577 AuthConfig: &docker.AuthConfiguration{ 2578 Username: "test", 2579 Password: "abcd", 2580 Email: "", 2581 ServerAddress: "https://other.io/v1/", 2582 }, 2583 }, 2584 } 2585 2586 for i, c := range cases { 2587 act, err := authFromDockerConfig(path)(c.Repo) 2588 if err != nil { 2589 t.Fatalf("Test %d failed: %v", i+1, err) 2590 } 2591 2592 if !reflect.DeepEqual(act, c.AuthConfig) { 2593 t.Fatalf("Test %d failed: Unexpected auth config: got %+v; want %+v", i+1, act, c.AuthConfig) 2594 } 2595 } 2596 } 2597 2598 func TestDockerDriver_OOMKilled(t *testing.T) { 2599 if !tu.IsTravis() { 2600 t.Parallel() 2601 } 2602 if !testutil.DockerIsConnected(t) { 2603 t.Skip("Docker not connected") 2604 } 2605 2606 task := &structs.Task{ 2607 Name: "oom-killed", 2608 Driver: "docker", 2609 Config: map[string]interface{}{ 2610 "image": "busybox", 2611 "load": "busybox.tar", 2612 "command": "sh", 2613 // Incrementally creates a bigger and bigger variable. 2614 "args": []string{"-c", "x=a; while true; do eval x='$x$x'; done"}, 2615 }, 2616 LogConfig: &structs.LogConfig{ 2617 MaxFiles: 10, 2618 MaxFileSizeMB: 10, 2619 }, 2620 Resources: &structs.Resources{ 2621 CPU: 250, 2622 MemoryMB: 10, 2623 DiskMB: 20, 2624 Networks: []*structs.NetworkResource{}, 2625 }, 2626 } 2627 2628 _, handle, cleanup := dockerSetup(t, task) 2629 defer cleanup() 2630 2631 select { 2632 case res := <-handle.WaitCh(): 2633 if res.Successful() { 2634 t.Fatalf("expected error, but container exited successful") 2635 } 2636 2637 if res.Err.Error() != "OOM Killed" { 2638 t.Fatalf("not killed by OOM killer: %s", res.Err) 2639 } 2640 2641 t.Logf("Successfully killed by OOM killer") 2642 2643 case <-time.After(time.Duration(tu.TestMultiplier()*5) * time.Second): 2644 t.Fatalf("timeout") 2645 } 2646 } 2647 2648 func TestDockerDriver_Devices_IsInvalidConfig(t *testing.T) { 2649 if !tu.IsTravis() { 2650 t.Parallel() 2651 } 2652 if !testutil.DockerIsConnected(t) { 2653 t.Skip("Docker not connected") 2654 } 2655 2656 brokenConfigs := []interface{}{ 2657 map[string]interface{}{ 2658 "host_path": "", 2659 }, 2660 map[string]interface{}{ 2661 "host_path": "/dev/sda1", 2662 "cgroup_permissions": "rxb", 2663 }, 2664 } 2665 2666 test_cases := []struct { 2667 deviceConfig interface{} 2668 err error 2669 }{ 2670 {[]interface{}{brokenConfigs[0]}, fmt.Errorf("host path must be set in configuration for devices")}, 2671 {[]interface{}{brokenConfigs[1]}, fmt.Errorf("invalid cgroup permission string: \"rxb\"")}, 2672 } 2673 2674 for _, tc := range test_cases { 2675 task, _, _ := dockerTask(t) 2676 task.Config["devices"] = tc.deviceConfig 2677 2678 ctx := testDockerDriverContexts(t, task) 2679 driver := NewDockerDriver(ctx.DriverCtx) 2680 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 2681 defer ctx.AllocDir.Destroy() 2682 2683 if _, err := driver.Prestart(ctx.ExecCtx, task); err == nil || err.Error() != tc.err.Error() { 2684 t.Fatalf("error expected in prestart, got %v, expected %v", err, tc.err) 2685 } 2686 } 2687 } 2688 2689 func TestDockerDriver_Device_Success(t *testing.T) { 2690 if !tu.IsTravis() { 2691 t.Parallel() 2692 } 2693 if !testutil.DockerIsConnected(t) { 2694 t.Skip("Docker not connected") 2695 } 2696 2697 if runtime.GOOS != "linux" { 2698 t.Skip("test device mounts only on linux") 2699 } 2700 2701 hostPath := "/dev/random" 2702 containerPath := "/dev/myrandom" 2703 perms := "rwm" 2704 2705 expectedDevice := docker.Device{ 2706 PathOnHost: hostPath, 2707 PathInContainer: containerPath, 2708 CgroupPermissions: perms, 2709 } 2710 config := map[string]interface{}{ 2711 "host_path": hostPath, 2712 "container_path": containerPath, 2713 } 2714 2715 task, _, _ := dockerTask(t) 2716 task.Config["devices"] = []interface{}{config} 2717 2718 client, handle, cleanup := dockerSetup(t, task) 2719 defer cleanup() 2720 2721 waitForExist(t, client, handle) 2722 2723 container, err := client.InspectContainer(handle.ContainerID()) 2724 if err != nil { 2725 t.Fatalf("err: %v", err) 2726 } 2727 2728 assert.NotEmpty(t, container.HostConfig.Devices, "Expected one device") 2729 assert.Equal(t, expectedDevice, container.HostConfig.Devices[0], "Incorrect device ") 2730 } 2731 2732 func TestDockerDriver_Entrypoint(t *testing.T) { 2733 if !tu.IsTravis() { 2734 t.Parallel() 2735 } 2736 if !testutil.DockerIsConnected(t) { 2737 t.Skip("Docker not connected") 2738 } 2739 2740 entrypoint := []string{"/bin/sh", "-c"} 2741 task, _, _ := dockerTask(t) 2742 task.Config["entrypoint"] = entrypoint 2743 2744 client, handle, cleanup := dockerSetup(t, task) 2745 defer cleanup() 2746 2747 waitForExist(t, client, handle) 2748 2749 container, err := client.InspectContainer(handle.ContainerID()) 2750 if err != nil { 2751 t.Fatalf("err: %v", err) 2752 } 2753 2754 require.Len(t, container.Config.Entrypoint, 2, "Expected one entrypoint") 2755 require.Equal(t, entrypoint, container.Config.Entrypoint, "Incorrect entrypoint ") 2756 } 2757 2758 func TestDockerDriver_Kill(t *testing.T) { 2759 assert := assert.New(t) 2760 if !tu.IsTravis() { 2761 t.Parallel() 2762 } 2763 if !testutil.DockerIsConnected(t) { 2764 t.Skip("Docker not connected") 2765 } 2766 2767 // Tasks started with a signal that is not supported should not error 2768 task := &structs.Task{ 2769 Name: "nc-demo", 2770 Driver: "docker", 2771 KillSignal: "SIGKILL", 2772 Config: map[string]interface{}{ 2773 "load": "busybox.tar", 2774 "image": "busybox", 2775 "command": "/bin/nc", 2776 "args": []string{"-l", "127.0.0.1", "-p", "0"}, 2777 }, 2778 LogConfig: &structs.LogConfig{ 2779 MaxFiles: 10, 2780 MaxFileSizeMB: 10, 2781 }, 2782 Resources: basicResources, 2783 } 2784 2785 ctx := testDockerDriverContexts(t, task) 2786 defer ctx.AllocDir.Destroy() 2787 d := NewDockerDriver(ctx.DriverCtx) 2788 copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") 2789 2790 _, err := d.Prestart(ctx.ExecCtx, task) 2791 if err != nil { 2792 t.Fatalf("error in prestart: %v", err) 2793 } 2794 2795 resp, err := d.Start(ctx.ExecCtx, task) 2796 assert.Nil(err) 2797 assert.NotNil(resp.Handle) 2798 2799 handle := resp.Handle.(*DockerHandle) 2800 waitForExist(t, client, handle) 2801 err = handle.Kill() 2802 assert.Nil(err) 2803 } 2804 2805 func TestDockerDriver_ReadonlyRootfs(t *testing.T) { 2806 if !tu.IsTravis() { 2807 t.Parallel() 2808 } 2809 if !testutil.DockerIsConnected(t) { 2810 t.Skip("Docker not connected") 2811 } 2812 2813 task, _, _ := dockerTask(t) 2814 task.Config["readonly_rootfs"] = true 2815 2816 client, handle, cleanup := dockerSetup(t, task) 2817 defer cleanup() 2818 2819 waitForExist(t, client, handle) 2820 2821 container, err := client.InspectContainer(handle.ContainerID()) 2822 assert.Nil(t, err, "Error inspecting container: %v", err) 2823 2824 assert.True(t, container.HostConfig.ReadonlyRootfs, "ReadonlyRootfs option not set") 2825 } 2826 2827 // fakeDockerClient can be used in places that accept an interface for the 2828 // docker client such as createContainer. 2829 type fakeDockerClient struct{} 2830 2831 func (fakeDockerClient) CreateContainer(docker.CreateContainerOptions) (*docker.Container, error) { 2832 return nil, fmt.Errorf("volume is attached on another node") 2833 } 2834 func (fakeDockerClient) InspectContainer(id string) (*docker.Container, error) { 2835 panic("not implemented") 2836 } 2837 func (fakeDockerClient) ListContainers(docker.ListContainersOptions) ([]docker.APIContainers, error) { 2838 panic("not implemented") 2839 } 2840 func (fakeDockerClient) RemoveContainer(opts docker.RemoveContainerOptions) error { 2841 panic("not implemented") 2842 } 2843 2844 // TestDockerDriver_VolumeError asserts volume related errors when creating a 2845 // container are recoverable. 2846 func TestDockerDriver_VolumeError(t *testing.T) { 2847 if !tu.IsTravis() { 2848 t.Parallel() 2849 } 2850 2851 // setup 2852 task, _, _ := dockerTask(t) 2853 tctx := testDockerDriverContexts(t, task) 2854 driver := NewDockerDriver(tctx.DriverCtx).(*DockerDriver) 2855 driver.driverConfig = &DockerDriverConfig{ImageName: "test"} 2856 2857 // assert volume error is recoverable 2858 _, err := driver.createContainer(fakeDockerClient{}, docker.CreateContainerOptions{}) 2859 require.True(t, structs.IsRecoverable(err)) 2860 } 2861 2862 func TestDockerDriver_AdvertiseIPv6Address(t *testing.T) { 2863 if !tu.IsTravis() { 2864 t.Parallel() 2865 } 2866 if !testutil.DockerIsConnected(t) { 2867 t.Skip("Docker not connected") 2868 } 2869 2870 expectedPrefix := "2001:db8:1::242:ac11" 2871 expectedAdvertise := true 2872 task := &structs.Task{ 2873 Name: "nc-demo", 2874 Driver: "docker", 2875 Config: map[string]interface{}{ 2876 "image": "busybox", 2877 "load": "busybox.tar", 2878 "command": "/bin/nc", 2879 "args": []string{"-l", "127.0.0.1", "-p", "0"}, 2880 "advertise_ipv6_address": expectedAdvertise, 2881 }, 2882 Resources: &structs.Resources{ 2883 MemoryMB: 256, 2884 CPU: 512, 2885 }, 2886 LogConfig: &structs.LogConfig{ 2887 MaxFiles: 10, 2888 MaxFileSizeMB: 10, 2889 }, 2890 } 2891 2892 client := newTestDockerClient(t) 2893 2894 // Make sure IPv6 is enabled 2895 net, err := client.NetworkInfo("bridge") 2896 if err != nil { 2897 t.Skip("error retrieving bridge network information, skipping") 2898 } 2899 if net == nil || !net.EnableIPv6 { 2900 t.Skip("IPv6 not enabled on bridge network, skipping") 2901 } 2902 2903 tctx := testDockerDriverContexts(t, task) 2904 driver := NewDockerDriver(tctx.DriverCtx) 2905 copyImage(t, tctx.ExecCtx.TaskDir, "busybox.tar") 2906 defer tctx.AllocDir.Destroy() 2907 2908 presp, err := driver.Prestart(tctx.ExecCtx, task) 2909 defer driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) 2910 if err != nil { 2911 t.Fatalf("Error in prestart: %v", err) 2912 } 2913 2914 sresp, err := driver.Start(tctx.ExecCtx, task) 2915 if err != nil { 2916 t.Fatalf("Error in start: %v", err) 2917 } 2918 2919 if sresp.Handle == nil { 2920 t.Fatalf("handle is nil\nStack\n%s", debug.Stack()) 2921 } 2922 2923 assert.Equal(t, expectedAdvertise, sresp.Network.AutoAdvertise, "Wrong autoadvertise. Expect: %s, got: %s", expectedAdvertise, sresp.Network.AutoAdvertise) 2924 2925 if !strings.HasPrefix(sresp.Network.IP, expectedPrefix) { 2926 t.Fatalf("Got IP address %q want ip address with prefix %q", sresp.Network.IP, expectedPrefix) 2927 } 2928 2929 defer sresp.Handle.Kill() 2930 handle := sresp.Handle.(*DockerHandle) 2931 2932 waitForExist(t, client, handle) 2933 2934 container, err := client.InspectContainer(handle.ContainerID()) 2935 if err != nil { 2936 t.Fatalf("Error inspecting container: %v", err) 2937 } 2938 2939 if !strings.HasPrefix(container.NetworkSettings.GlobalIPv6Address, expectedPrefix) { 2940 t.Fatalf("Got GlobalIPv6address %s want GlobalIPv6address with prefix %s", expectedPrefix, container.NetworkSettings.GlobalIPv6Address) 2941 } 2942 } 2943 2944 func TestParseDockerImage(t *testing.T) { 2945 tests := []struct { 2946 Image string 2947 Repo string 2948 Tag string 2949 }{ 2950 {"library/hello-world:1.0", "library/hello-world", "1.0"}, 2951 {"library/hello-world", "library/hello-world", "latest"}, 2952 {"library/hello-world:latest", "library/hello-world", "latest"}, 2953 {"library/hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77", "library/hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77", ""}, 2954 } 2955 for _, test := range tests { 2956 t.Run(test.Image, func(t *testing.T) { 2957 repo, tag := parseDockerImage(test.Image) 2958 require.Equal(t, test.Repo, repo) 2959 require.Equal(t, test.Tag, tag) 2960 }) 2961 } 2962 } 2963 2964 func TestDockerImageRef(t *testing.T) { 2965 tests := []struct { 2966 Image string 2967 Repo string 2968 Tag string 2969 }{ 2970 {"library/hello-world:1.0", "library/hello-world", "1.0"}, 2971 {"library/hello-world:latest", "library/hello-world", "latest"}, 2972 {"library/hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77", "library/hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77", ""}, 2973 } 2974 for _, test := range tests { 2975 t.Run(test.Image, func(t *testing.T) { 2976 image := dockerImageRef(test.Repo, test.Tag) 2977 require.Equal(t, test.Image, image) 2978 }) 2979 } 2980 }