github.com/ranjib/nomad@v0.1.1-0.20160225204057-97751b02f70b/client/driver/docker_test.go (about) 1 package driver 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "math/rand" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "reflect" 11 "runtime/debug" 12 "testing" 13 "time" 14 15 docker "github.com/fsouza/go-dockerclient" 16 "github.com/hashicorp/go-plugin" 17 "github.com/hashicorp/nomad/client/config" 18 "github.com/hashicorp/nomad/client/driver/env" 19 cstructs "github.com/hashicorp/nomad/client/driver/structs" 20 "github.com/hashicorp/nomad/helper/discover" 21 "github.com/hashicorp/nomad/nomad/structs" 22 "github.com/hashicorp/nomad/testutil" 23 ) 24 25 // dockerIsConnected checks to see if a docker daemon is available (local or remote) 26 func dockerIsConnected(t *testing.T) bool { 27 client, err := docker.NewClientFromEnv() 28 if err != nil { 29 return false 30 } 31 32 // Creating a client doesn't actually connect, so make sure we do something 33 // like call Version() on it. 34 env, err := client.Version() 35 if err != nil { 36 t.Logf("Failed to connect to docker daemon: %s", err) 37 return false 38 } 39 40 t.Logf("Successfully connected to docker daemon running version %s", env.Get("Version")) 41 return true 42 } 43 44 func dockerIsRemote(t *testing.T) bool { 45 client, err := docker.NewClientFromEnv() 46 if err != nil { 47 return false 48 } 49 50 // Technically this could be a local tcp socket but for testing purposes 51 // we'll just assume that tcp is only used for remote connections. 52 if client.Endpoint()[0:3] == "tcp" { 53 return true 54 } 55 return false 56 } 57 58 // Ports used by tests 59 var ( 60 docker_reserved = 32768 + int(rand.Int31n(25000)) 61 docker_dynamic = 32768 + int(rand.Int31n(25000)) 62 ) 63 64 // Returns a task with a reserved and dynamic port. The ports are returned 65 // respectively. 66 func dockerTask() (*structs.Task, int, int) { 67 docker_reserved += 1 68 docker_dynamic += 1 69 return &structs.Task{ 70 Name: "redis-demo", 71 Config: map[string]interface{}{ 72 "image": "redis", 73 }, 74 LogConfig: &structs.LogConfig{ 75 MaxFiles: 10, 76 MaxFileSizeMB: 10, 77 }, 78 Resources: &structs.Resources{ 79 MemoryMB: 256, 80 CPU: 512, 81 Networks: []*structs.NetworkResource{ 82 &structs.NetworkResource{ 83 IP: "127.0.0.1", 84 ReservedPorts: []structs.Port{{"main", docker_reserved}}, 85 DynamicPorts: []structs.Port{{"REDIS", docker_dynamic}}, 86 }, 87 }, 88 }, 89 }, docker_reserved, docker_dynamic 90 } 91 92 // dockerSetup does all of the basic setup you need to get a running docker 93 // process up and running for testing. Use like: 94 // 95 // task := taskTemplate() 96 // // do custom task configuration 97 // client, handle, cleanup := dockerSetup(t, task) 98 // defer cleanup() 99 // // do test stuff 100 // 101 // If there is a problem during setup this function will abort or skip the test 102 // and indicate the reason. 103 func dockerSetup(t *testing.T, task *structs.Task) (*docker.Client, DriverHandle, func()) { 104 if !dockerIsConnected(t) { 105 t.SkipNow() 106 } 107 108 client, err := docker.NewClientFromEnv() 109 if err != nil { 110 t.Fatalf("Failed to initialize client: %s\nStack\n%s", err, debug.Stack()) 111 } 112 113 driverCtx, execCtx := testDriverContexts(task) 114 driver := NewDockerDriver(driverCtx) 115 116 handle, err := driver.Start(execCtx, task) 117 if err != nil { 118 execCtx.AllocDir.Destroy() 119 t.Fatalf("Failed to start driver: %s\nStack\n%s", err, debug.Stack()) 120 } 121 if handle == nil { 122 execCtx.AllocDir.Destroy() 123 t.Fatalf("handle is nil\nStack\n%s", debug.Stack()) 124 } 125 126 cleanup := func() { 127 handle.Kill() 128 execCtx.AllocDir.Destroy() 129 } 130 131 return client, handle, cleanup 132 } 133 134 func TestDockerDriver_Handle(t *testing.T) { 135 t.Parallel() 136 137 bin, err := discover.NomadExecutable() 138 if err != nil { 139 t.Fatalf("got an err: %v", err) 140 } 141 142 f, _ := ioutil.TempFile(os.TempDir(), "") 143 defer f.Close() 144 defer os.Remove(f.Name()) 145 pluginConfig := &plugin.ClientConfig{ 146 Cmd: exec.Command(bin, "syslog", f.Name()), 147 } 148 logCollector, pluginClient, err := createLogCollector(pluginConfig, os.Stdout, &config.Config{}) 149 if err != nil { 150 t.Fatalf("got an err: %v", err) 151 } 152 defer pluginClient.Kill() 153 154 h := &DockerHandle{ 155 version: "version", 156 imageID: "imageid", 157 logCollector: logCollector, 158 pluginClient: pluginClient, 159 containerID: "containerid", 160 killTimeout: 5 * time.Nanosecond, 161 doneCh: make(chan struct{}), 162 waitCh: make(chan *cstructs.WaitResult, 1), 163 } 164 165 actual := h.ID() 166 expected := fmt.Sprintf("DOCKER:{\"Version\":\"version\",\"ImageID\":\"imageid\",\"ContainerID\":\"containerid\",\"KillTimeout\":5,\"PluginConfig\":{\"Pid\":%d,\"AddrNet\":\"unix\",\"AddrName\":\"%s\"}}", 167 pluginClient.ReattachConfig().Pid, pluginClient.ReattachConfig().Addr.String()) 168 if actual != expected { 169 t.Errorf("Expected `%s`, found `%s`", expected, actual) 170 } 171 } 172 173 // This test should always pass, even if docker daemon is not available 174 func TestDockerDriver_Fingerprint(t *testing.T) { 175 t.Parallel() 176 driverCtx, _ := testDriverContexts(&structs.Task{Name: "foo"}) 177 d := NewDockerDriver(driverCtx) 178 node := &structs.Node{ 179 Attributes: make(map[string]string), 180 } 181 apply, err := d.Fingerprint(&config.Config{}, node) 182 if err != nil { 183 t.Fatalf("err: %v", err) 184 } 185 if apply != dockerIsConnected(t) { 186 t.Fatalf("Fingerprinter should detect when docker is available") 187 } 188 if node.Attributes["driver.docker"] != "1" { 189 t.Log("Docker daemon not available. The remainder of the docker tests will be skipped.") 190 } 191 t.Logf("Found docker version %s", node.Attributes["driver.docker.version"]) 192 } 193 194 func TestDockerDriver_StartOpen_Wait(t *testing.T) { 195 t.Parallel() 196 if !dockerIsConnected(t) { 197 t.SkipNow() 198 } 199 200 task := &structs.Task{ 201 Name: "redis-demo", 202 Config: map[string]interface{}{ 203 "image": "redis", 204 }, 205 LogConfig: &structs.LogConfig{ 206 MaxFiles: 10, 207 MaxFileSizeMB: 10, 208 }, 209 Resources: basicResources, 210 } 211 212 driverCtx, execCtx := testDriverContexts(task) 213 defer execCtx.AllocDir.Destroy() 214 d := NewDockerDriver(driverCtx) 215 216 handle, err := d.Start(execCtx, task) 217 if err != nil { 218 t.Fatalf("err: %v", err) 219 } 220 if handle == nil { 221 t.Fatalf("missing handle") 222 } 223 defer handle.Kill() 224 225 // Attempt to open 226 handle2, err := d.Open(execCtx, handle.ID()) 227 if err != nil { 228 t.Fatalf("err: %v", err) 229 } 230 if handle2 == nil { 231 t.Fatalf("missing handle") 232 } 233 } 234 235 func TestDockerDriver_Start_Wait(t *testing.T) { 236 t.Parallel() 237 task := &structs.Task{ 238 Name: "redis-demo", 239 Config: map[string]interface{}{ 240 "image": "redis", 241 "command": "redis-server", 242 "args": []string{"-v"}, 243 }, 244 Resources: &structs.Resources{ 245 MemoryMB: 256, 246 CPU: 512, 247 }, 248 LogConfig: &structs.LogConfig{ 249 MaxFiles: 10, 250 MaxFileSizeMB: 10, 251 }, 252 } 253 254 _, handle, cleanup := dockerSetup(t, task) 255 defer cleanup() 256 257 // Update should be a no-op 258 err := handle.Update(task) 259 if err != nil { 260 t.Fatalf("err: %v", err) 261 } 262 263 select { 264 case res := <-handle.WaitCh(): 265 if !res.Successful() { 266 t.Fatalf("err: %v", res) 267 } 268 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 269 t.Fatalf("timeout") 270 } 271 } 272 273 func TestDockerDriver_Start_Wait_AllocDir(t *testing.T) { 274 t.Parallel() 275 // This test requires that the alloc dir be mounted into docker as a volume. 276 // Because this cannot happen when docker is run remotely, e.g. when running 277 // docker in a VM, we skip this when we detect Docker is being run remotely. 278 if !dockerIsConnected(t) || dockerIsRemote(t) { 279 t.SkipNow() 280 } 281 282 exp := []byte{'w', 'i', 'n'} 283 file := "output.txt" 284 task := &structs.Task{ 285 Name: "redis-demo", 286 Config: map[string]interface{}{ 287 "image": "redis", 288 "command": "/bin/bash", 289 "args": []string{ 290 "-c", 291 fmt.Sprintf(`sleep 1; echo -n %s > $%s/%s`, 292 string(exp), env.AllocDir, file), 293 }, 294 }, 295 LogConfig: &structs.LogConfig{ 296 MaxFiles: 10, 297 MaxFileSizeMB: 10, 298 }, 299 Resources: &structs.Resources{ 300 MemoryMB: 256, 301 CPU: 512, 302 }, 303 } 304 305 driverCtx, execCtx := testDriverContexts(task) 306 defer execCtx.AllocDir.Destroy() 307 d := NewDockerDriver(driverCtx) 308 309 handle, err := d.Start(execCtx, task) 310 if err != nil { 311 t.Fatalf("err: %v", err) 312 } 313 if handle == nil { 314 t.Fatalf("missing handle") 315 } 316 defer handle.Kill() 317 318 select { 319 case res := <-handle.WaitCh(): 320 if !res.Successful() { 321 t.Fatalf("err: %v", res) 322 } 323 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 324 t.Fatalf("timeout") 325 } 326 327 // Check that data was written to the shared alloc directory. 328 outputFile := filepath.Join(execCtx.AllocDir.SharedDir, file) 329 act, err := ioutil.ReadFile(outputFile) 330 if err != nil { 331 t.Fatalf("Couldn't read expected output: %v", err) 332 } 333 334 if !reflect.DeepEqual(act, exp) { 335 t.Fatalf("Command outputted %v; want %v", act, exp) 336 } 337 } 338 339 func TestDockerDriver_Start_Kill_Wait(t *testing.T) { 340 t.Parallel() 341 task := &structs.Task{ 342 Name: "redis-demo", 343 Config: map[string]interface{}{ 344 "image": "redis", 345 "command": "/bin/sleep", 346 "args": []string{"10"}, 347 }, 348 LogConfig: &structs.LogConfig{ 349 MaxFiles: 10, 350 MaxFileSizeMB: 10, 351 }, 352 Resources: basicResources, 353 } 354 355 _, handle, cleanup := dockerSetup(t, task) 356 defer cleanup() 357 358 go func() { 359 time.Sleep(100 * time.Millisecond) 360 err := handle.Kill() 361 if err != nil { 362 t.Fatalf("err: %v", err) 363 } 364 }() 365 366 select { 367 case res := <-handle.WaitCh(): 368 if res.Successful() { 369 t.Fatalf("should err: %v", res) 370 } 371 case <-time.After(time.Duration(testutil.TestMultiplier()*10) * time.Second): 372 t.Fatalf("timeout") 373 } 374 } 375 376 func TestDocker_StartN(t *testing.T) { 377 t.Parallel() 378 if !dockerIsConnected(t) { 379 t.SkipNow() 380 } 381 382 task1, _, _ := dockerTask() 383 task2, _, _ := dockerTask() 384 task3, _, _ := dockerTask() 385 taskList := []*structs.Task{task1, task2, task3} 386 387 handles := make([]DriverHandle, len(taskList)) 388 389 t.Logf("==> Starting %d tasks", len(taskList)) 390 391 // Let's spin up a bunch of things 392 var err error 393 for idx, task := range taskList { 394 driverCtx, execCtx := testDriverContexts(task) 395 defer execCtx.AllocDir.Destroy() 396 d := NewDockerDriver(driverCtx) 397 398 handles[idx], err = d.Start(execCtx, task) 399 if err != nil { 400 t.Errorf("Failed starting task #%d: %s", idx+1, err) 401 } 402 } 403 404 t.Log("==> All tasks are started. Terminating...") 405 406 for idx, handle := range handles { 407 if handle == nil { 408 t.Errorf("Bad handle for task #%d", idx+1) 409 continue 410 } 411 412 err := handle.Kill() 413 if err != nil { 414 t.Errorf("Failed stopping task #%d: %s", idx+1, err) 415 } 416 } 417 418 t.Log("==> Test complete!") 419 } 420 421 func TestDocker_StartNVersions(t *testing.T) { 422 t.Parallel() 423 if !dockerIsConnected(t) { 424 t.SkipNow() 425 } 426 427 task1, _, _ := dockerTask() 428 task1.Config["image"] = "redis" 429 430 task2, _, _ := dockerTask() 431 task2.Config["image"] = "redis:latest" 432 433 task3, _, _ := dockerTask() 434 task3.Config["image"] = "redis:3.0" 435 436 taskList := []*structs.Task{task1, task2, task3} 437 438 handles := make([]DriverHandle, len(taskList)) 439 440 t.Logf("==> Starting %d tasks", len(taskList)) 441 442 // Let's spin up a bunch of things 443 var err error 444 for idx, task := range taskList { 445 driverCtx, execCtx := testDriverContexts(task) 446 defer execCtx.AllocDir.Destroy() 447 d := NewDockerDriver(driverCtx) 448 449 handles[idx], err = d.Start(execCtx, task) 450 if err != nil { 451 t.Errorf("Failed starting task #%d: %s", idx+1, err) 452 } 453 } 454 455 t.Log("==> All tasks are started. Terminating...") 456 457 for idx, handle := range handles { 458 if handle == nil { 459 t.Errorf("Bad handle for task #%d", idx+1) 460 continue 461 } 462 463 err := handle.Kill() 464 if err != nil { 465 t.Errorf("Failed stopping task #%d: %s", idx+1, err) 466 } 467 } 468 469 t.Log("==> Test complete!") 470 } 471 472 func TestDockerHostNet(t *testing.T) { 473 t.Parallel() 474 expected := "host" 475 476 task := &structs.Task{ 477 Name: "redis-demo", 478 Config: map[string]interface{}{ 479 "image": "redis", 480 "network_mode": expected, 481 }, 482 Resources: &structs.Resources{ 483 MemoryMB: 256, 484 CPU: 512, 485 }, 486 LogConfig: &structs.LogConfig{ 487 MaxFiles: 10, 488 MaxFileSizeMB: 10, 489 }, 490 } 491 492 client, handle, cleanup := dockerSetup(t, task) 493 defer cleanup() 494 495 container, err := client.InspectContainer(handle.(*DockerHandle).ContainerID()) 496 if err != nil { 497 t.Fatalf("err: %v", err) 498 } 499 500 actual := container.HostConfig.NetworkMode 501 if actual != expected { 502 t.Errorf("DNS Network mode doesn't match.\nExpected:\n%s\nGot:\n%s\n", expected, actual) 503 } 504 } 505 506 func TestDockerLabels(t *testing.T) { 507 t.Parallel() 508 task, _, _ := dockerTask() 509 task.Config["labels"] = []map[string]string{ 510 map[string]string{ 511 "label1": "value1", 512 "label2": "value2", 513 }, 514 } 515 516 client, handle, cleanup := dockerSetup(t, task) 517 defer cleanup() 518 519 container, err := client.InspectContainer(handle.(*DockerHandle).ContainerID()) 520 if err != nil { 521 t.Fatalf("err: %v", err) 522 } 523 524 if want, got := 2, len(container.Config.Labels); want != got { 525 t.Errorf("Wrong labels count for docker job. Expect: %d, got: %d", want, got) 526 } 527 528 if want, got := "value1", container.Config.Labels["label1"]; want != got { 529 t.Errorf("Wrong label value docker job. Expect: %s, got: %s", want, got) 530 } 531 } 532 533 func TestDockerDNS(t *testing.T) { 534 t.Parallel() 535 task, _, _ := dockerTask() 536 task.Config["dns_servers"] = []string{"8.8.8.8", "8.8.4.4"} 537 task.Config["dns_search_domains"] = []string{"example.com", "example.org", "example.net"} 538 539 client, handle, cleanup := dockerSetup(t, task) 540 defer cleanup() 541 542 container, err := client.InspectContainer(handle.(*DockerHandle).ContainerID()) 543 if err != nil { 544 t.Fatalf("err: %v", err) 545 } 546 547 if !reflect.DeepEqual(task.Config["dns_servers"], container.HostConfig.DNS) { 548 t.Errorf("DNS Servers don't match.\nExpected:\n%s\nGot:\n%s\n", task.Config["dns_servers"], container.HostConfig.DNS) 549 } 550 551 if !reflect.DeepEqual(task.Config["dns_search_domains"], container.HostConfig.DNSSearch) { 552 t.Errorf("DNS Servers don't match.\nExpected:\n%s\nGot:\n%s\n", task.Config["dns_search_domains"], container.HostConfig.DNSSearch) 553 } 554 } 555 556 func inSlice(needle string, haystack []string) bool { 557 for _, h := range haystack { 558 if h == needle { 559 return true 560 } 561 } 562 return false 563 } 564 565 func TestDockerPortsNoMap(t *testing.T) { 566 t.Parallel() 567 task, res, dyn := dockerTask() 568 569 client, handle, cleanup := dockerSetup(t, task) 570 defer cleanup() 571 572 container, err := client.InspectContainer(handle.(*DockerHandle).ContainerID()) 573 if err != nil { 574 t.Fatalf("err: %v", err) 575 } 576 577 // Verify that the correct ports are EXPOSED 578 expectedExposedPorts := map[docker.Port]struct{}{ 579 docker.Port(fmt.Sprintf("%d/tcp", res)): struct{}{}, 580 docker.Port(fmt.Sprintf("%d/udp", res)): struct{}{}, 581 docker.Port(fmt.Sprintf("%d/tcp", dyn)): struct{}{}, 582 docker.Port(fmt.Sprintf("%d/udp", dyn)): struct{}{}, 583 // This one comes from the redis container 584 docker.Port("6379/tcp"): struct{}{}, 585 } 586 587 if !reflect.DeepEqual(container.Config.ExposedPorts, expectedExposedPorts) { 588 t.Errorf("Exposed ports don't match.\nExpected:\n%s\nGot:\n%s\n", expectedExposedPorts, container.Config.ExposedPorts) 589 } 590 591 // Verify that the correct ports are FORWARDED 592 expectedPortBindings := map[docker.Port][]docker.PortBinding{ 593 docker.Port(fmt.Sprintf("%d/tcp", res)): []docker.PortBinding{docker.PortBinding{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", res)}}, 594 docker.Port(fmt.Sprintf("%d/udp", res)): []docker.PortBinding{docker.PortBinding{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", res)}}, 595 docker.Port(fmt.Sprintf("%d/tcp", dyn)): []docker.PortBinding{docker.PortBinding{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", dyn)}}, 596 docker.Port(fmt.Sprintf("%d/udp", dyn)): []docker.PortBinding{docker.PortBinding{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", dyn)}}, 597 } 598 599 if !reflect.DeepEqual(container.HostConfig.PortBindings, expectedPortBindings) { 600 t.Errorf("Forwarded ports don't match.\nExpected:\n%s\nGot:\n%s\n", expectedPortBindings, container.HostConfig.PortBindings) 601 } 602 603 expectedEnvironment := map[string]string{ 604 "NOMAD_ADDR_main": fmt.Sprintf("127.0.0.1:%d", res), 605 "NOMAD_ADDR_REDIS": fmt.Sprintf("127.0.0.1:%d", dyn), 606 } 607 608 for key, val := range expectedEnvironment { 609 search := fmt.Sprintf("%s=%s", key, val) 610 if !inSlice(search, container.Config.Env) { 611 t.Errorf("Expected to find %s in container environment: %+v", search, container.Config.Env) 612 } 613 } 614 } 615 616 func TestDockerPortsMapping(t *testing.T) { 617 t.Parallel() 618 task, res, dyn := dockerTask() 619 task.Config["port_map"] = []map[string]string{ 620 map[string]string{ 621 "main": "8080", 622 "REDIS": "6379", 623 }, 624 } 625 626 client, handle, cleanup := dockerSetup(t, task) 627 defer cleanup() 628 629 container, err := client.InspectContainer(handle.(*DockerHandle).ContainerID()) 630 if err != nil { 631 t.Fatalf("err: %v", err) 632 } 633 634 // Verify that the correct ports are EXPOSED 635 expectedExposedPorts := map[docker.Port]struct{}{ 636 docker.Port("8080/tcp"): struct{}{}, 637 docker.Port("8080/udp"): struct{}{}, 638 docker.Port("6379/tcp"): struct{}{}, 639 docker.Port("6379/udp"): struct{}{}, 640 } 641 642 if !reflect.DeepEqual(container.Config.ExposedPorts, expectedExposedPorts) { 643 t.Errorf("Exposed ports don't match.\nExpected:\n%s\nGot:\n%s\n", expectedExposedPorts, container.Config.ExposedPorts) 644 } 645 646 // Verify that the correct ports are FORWARDED 647 expectedPortBindings := map[docker.Port][]docker.PortBinding{ 648 docker.Port("8080/tcp"): []docker.PortBinding{docker.PortBinding{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", res)}}, 649 docker.Port("8080/udp"): []docker.PortBinding{docker.PortBinding{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", res)}}, 650 docker.Port("6379/tcp"): []docker.PortBinding{docker.PortBinding{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", dyn)}}, 651 docker.Port("6379/udp"): []docker.PortBinding{docker.PortBinding{HostIP: "127.0.0.1", HostPort: fmt.Sprintf("%d", dyn)}}, 652 } 653 654 if !reflect.DeepEqual(container.HostConfig.PortBindings, expectedPortBindings) { 655 t.Errorf("Forwarded ports don't match.\nExpected:\n%s\nGot:\n%s\n", expectedPortBindings, container.HostConfig.PortBindings) 656 } 657 658 expectedEnvironment := map[string]string{ 659 "NOMAD_ADDR_main": "127.0.0.1:8080", 660 "NOMAD_ADDR_REDIS": "127.0.0.1:6379", 661 "NOMAD_HOST_PORT_main": "8080", 662 } 663 664 for key, val := range expectedEnvironment { 665 search := fmt.Sprintf("%s=%s", key, val) 666 if !inSlice(search, container.Config.Env) { 667 t.Errorf("Expected to find %s in container environment: %+v", search, container.Config.Env) 668 } 669 } 670 }