github.com/schwarzm/garden-linux@v0.0.0-20150507151835-33bca2147c47/integration/lifecycle/lifecycle_test.go (about) 1 package lifecycle_test 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "compress/gzip" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/cloudfoundry-incubator/garden" 18 . "github.com/onsi/ginkgo" 19 . "github.com/onsi/gomega" 20 "github.com/onsi/gomega/gbytes" 21 "github.com/onsi/gomega/gexec" 22 archiver "github.com/pivotal-golang/archiver/extractor/test_helper" 23 ) 24 25 var _ = Describe("Creating a container", func() { 26 27 Describe("Overlapping networks", func() { 28 Context("when the requested Network overlaps the dynamic allocation range", func() { 29 It("returns an error message naming the overlapped range", func() { 30 client = startGarden("--networkPool", "1.2.3.0/24") 31 _, err := client.Create(garden.ContainerSpec{Network: "1.2.3.0/25"}) 32 Expect(err).To(MatchError("the requested subnet (1.2.3.0/25) overlaps the dynamic allocation range (1.2.3.0/24)")) 33 }) 34 }) 35 36 Context("when the requested Network overlaps another subnet", func() { 37 It("returns an error message naming the overlapped range", func() { 38 client = startGarden() 39 _, err := client.Create(garden.ContainerSpec{Privileged: false, Network: "10.2.0.0/29"}) 40 Expect(err).ToNot(HaveOccurred()) 41 _, err = client.Create(garden.ContainerSpec{Privileged: false, Network: "10.2.0.0/30"}) 42 Expect(err).To(MatchError("the requested subnet (10.2.0.0/30) overlaps an existing subnet (10.2.0.0/29)")) 43 }) 44 }) 45 }) 46 47 Describe("Docker image download", func() { 48 It("returns a helpful error message when image not found from default registry", func() { 49 client = startGarden() 50 _, err := client.Create(garden.ContainerSpec{RootFSPath: "docker:///cloudfoundry/doesnotexist"}) 51 Expect(err.Error()).To(ContainSubstring("could not fetch image cloudfoundry/doesnotexist from registry https://index.docker.io/v1/")) 52 }) 53 54 It("returns a helpful error message when registry does not exist", func() { 55 client = startGarden() 56 57 // Note: Using a valid url that is not a docker registry would make the test assertion below fail due to a bug in 58 // docker https://github.com/docker/docker/blob/v1.3.3/registry/endpoint.go#L107-L157 59 // eg. client.Create(garden.ContainerSpec{RootFSPath: "docker://example.com/cloudfoundry/doesnotexist"}) 60 _, err := client.Create(garden.ContainerSpec{RootFSPath: "docker://does-not.exist/cloudfoundry/doesnotexist"}) 61 Expect(err.Error()).To(ContainSubstring("could not fetch image cloudfoundry/doesnotexist from registry does-not.exist")) 62 }) 63 }) 64 65 Describe("concurrently destroying", func() { 66 allBridges := func() []byte { 67 stdout := gbytes.NewBuffer() 68 cmd, err := gexec.Start(exec.Command("ip", "a"), stdout, GinkgoWriter) 69 Expect(err).ToNot(HaveOccurred()) 70 cmd.Wait() 71 72 return stdout.Contents() 73 } 74 75 It("does not leave residual bridges", func() { 76 client = startGarden() 77 78 bridgePrefix := fmt.Sprintf("w%db-", GinkgoParallelNode()) 79 Expect(allBridges()).ToNot(ContainSubstring(bridgePrefix)) 80 81 handles := make([]string, 0) 82 for i := 0; i < 5; i++ { 83 c, err := client.Create(garden.ContainerSpec{}) 84 Expect(err).ToNot(HaveOccurred()) 85 86 handles = append(handles, c.Handle()) 87 } 88 89 retry := func(fn func() error) error { 90 var err error 91 for retry := 0; retry < 3; retry++ { 92 err = fn() 93 if err == nil { 94 break 95 } 96 } 97 return err 98 } 99 100 wg := new(sync.WaitGroup) 101 errors := make(chan error, 50) 102 for _, h := range handles { 103 wg.Add(1) 104 go func(h string) { 105 err := retry(func() error { return client.Destroy(h) }) 106 107 if err != nil { 108 errors <- err 109 } 110 111 wg.Done() 112 }(h) 113 } 114 115 wg.Wait() 116 117 Expect(errors).ToNot(Receive()) 118 Expect(client.Containers(garden.Properties{})).To(HaveLen(0)) // sanity check 119 120 Eventually(allBridges).ShouldNot(ContainSubstring(bridgePrefix)) 121 }) 122 }) 123 124 Context("when the container fails to start", func() { 125 It("does not leave resources around", func() { 126 client = startGarden() 127 client.Create(garden.ContainerSpec{ 128 BindMounts: []garden.BindMount{{ 129 SrcPath: "fictional", 130 DstPath: "whereami", 131 }}, 132 }) 133 134 depotDir := filepath.Join( 135 os.TempDir(), 136 fmt.Sprintf("test-garden-%d", GinkgoParallelNode()), 137 "containers", 138 ) 139 Expect(ioutil.ReadDir(depotDir)).To(HaveLen(0)) 140 }) 141 }) 142 143 Context("when the container is created succesfully", func() { 144 var container garden.Container 145 146 var privilegedContainer bool 147 var rootfs string 148 149 JustBeforeEach(func() { 150 client = startGarden() 151 152 var err error 153 container, err = client.Create(garden.ContainerSpec{Privileged: privilegedContainer, RootFSPath: rootfs}) 154 Expect(err).ToNot(HaveOccurred()) 155 }) 156 157 AfterEach(func() { 158 if container != nil { 159 Expect(client.Destroy(container.Handle())).To(Succeed()) 160 } 161 }) 162 163 BeforeEach(func() { 164 privilegedContainer = false 165 rootfs = "" 166 }) 167 168 It("sources /etc/seed", func() { 169 process, err := container.Run(garden.ProcessSpec{ 170 Path: "test", 171 Args: []string{"-e", "/tmp/ran-seed"}, 172 }, garden.ProcessIO{}) 173 Expect(err).ToNot(HaveOccurred()) 174 175 Expect(process.Wait()).To(Equal(0)) 176 }) 177 178 It("provides /dev/shm as tmpfs in the container", func() { 179 process, err := container.Run(garden.ProcessSpec{ 180 Path: "dd", 181 Args: []string{"if=/dev/urandom", "of=/dev/shm/some-data", "count=64", "bs=1k"}, 182 }, garden.ProcessIO{}) 183 Expect(err).ToNot(HaveOccurred()) 184 185 Expect(process.Wait()).To(Equal(0)) 186 187 outBuf := gbytes.NewBuffer() 188 189 process, err = container.Run(garden.ProcessSpec{ 190 Path: "cat", 191 Args: []string{"/proc/mounts"}, 192 }, garden.ProcessIO{ 193 Stdout: outBuf, 194 Stderr: GinkgoWriter, 195 }) 196 Expect(err).ToNot(HaveOccurred()) 197 198 Expect(process.Wait()).To(Equal(0)) 199 200 Expect(outBuf).To(gbytes.Say("tmpfs /dev/shm tmpfs")) 201 Expect(outBuf).To(gbytes.Say("rw,nodev,relatime")) 202 }) 203 204 Context("and sending a List request", func() { 205 It("includes the created container", func() { 206 Expect(getContainerHandles()).To(ContainElement(container.Handle())) 207 }) 208 }) 209 210 Context("and sending an Info request", func() { 211 It("returns the container's info", func() { 212 info, err := container.Info() 213 Expect(err).ToNot(HaveOccurred()) 214 215 Expect(info.State).To(Equal("active")) 216 }) 217 }) 218 219 It("gives the container a hostname based on its id", func() { 220 stdout := gbytes.NewBuffer() 221 222 _, err := container.Run(garden.ProcessSpec{ 223 Path: "hostname", 224 }, garden.ProcessIO{ 225 Stdout: stdout, 226 }) 227 Expect(err).ToNot(HaveOccurred()) 228 229 Eventually(stdout).Should(gbytes.Say(fmt.Sprintf("%s\n", container.Handle()))) 230 }) 231 232 Context("Using a docker image", func() { 233 Context("when there is a VOLUME associated with the docker image", func() { 234 BeforeEach(func() { 235 // dockerfile contains `VOLUME /foo`, see diego-dockerfiles/with-volume 236 rootfs = "docker:///cloudfoundry/with-volume" 237 }) 238 239 It("creates the volume directory, if it does not already exist", func() { 240 stdout := gbytes.NewBuffer() 241 process, err := container.Run(garden.ProcessSpec{ 242 Path: "ls", 243 Args: []string{"-l", "/"}, 244 }, garden.ProcessIO{ 245 Stdout: io.MultiWriter(GinkgoWriter, stdout), 246 Stderr: GinkgoWriter, 247 }) 248 249 Expect(err).ToNot(HaveOccurred()) 250 251 process.Wait() 252 Expect(stdout).To(gbytes.Say("foo")) 253 }) 254 }) 255 256 Context("when the docker image specifies $PATH", func() { 257 BeforeEach(func() { 258 // Dockerfile contains: 259 // ENV PATH /usr/local/bin:/usr/bin:/bin:/from-dockerfile 260 // ENV TEST test-from-dockerfile 261 // ENV TEST second-test-from-dockerfile:$TEST 262 // see diego-dockerfiles/with-volume 263 rootfs = "docker:///cloudfoundry/with-volume" 264 }) 265 266 It("$PATH is taken from the docker image", func() { 267 stdout := gbytes.NewBuffer() 268 process, err := container.Run(garden.ProcessSpec{ 269 Path: "/bin/sh", 270 Args: []string{"-c", "echo $PATH"}, 271 }, garden.ProcessIO{ 272 Stdout: io.MultiWriter(GinkgoWriter, stdout), 273 Stderr: GinkgoWriter, 274 }) 275 276 Expect(err).ToNot(HaveOccurred()) 277 278 process.Wait() 279 Expect(stdout).To(gbytes.Say("/usr/local/bin:/usr/bin:/bin:/from-dockerfile")) 280 }) 281 282 It("$TEST is taken from the docker image", func() { 283 stdout := gbytes.NewBuffer() 284 process, err := container.Run(garden.ProcessSpec{ 285 Path: "/bin/sh", 286 Args: []string{"-c", "echo $TEST"}, 287 }, garden.ProcessIO{ 288 Stdout: io.MultiWriter(GinkgoWriter, stdout), 289 Stderr: GinkgoWriter, 290 }) 291 292 Expect(err).ToNot(HaveOccurred()) 293 294 process.Wait() 295 Expect(stdout).To(gbytes.Say("second-test-from-dockerfile:test-from-dockerfile")) 296 }) 297 }) 298 }) 299 300 Context("and running a process", func() { 301 It("runs as the vcap user by default", func() { 302 stdout := gbytes.NewBuffer() 303 304 _, err := container.Run(garden.ProcessSpec{ 305 Path: "whoami", 306 }, garden.ProcessIO{ 307 Stdout: stdout, 308 }) 309 310 Expect(err).ToNot(HaveOccurred()) 311 Eventually(stdout).Should(gbytes.Say("vcap\n")) 312 }) 313 314 Context("when root is requested", func() { 315 It("runs as root inside the container", func() { 316 stdout := gbytes.NewBuffer() 317 318 _, err := container.Run(garden.ProcessSpec{ 319 Path: "whoami", 320 User: "root", 321 }, garden.ProcessIO{ 322 Stdout: stdout, 323 Stderr: GinkgoWriter, 324 }) 325 326 Expect(err).ToNot(HaveOccurred()) 327 Eventually(stdout).Should(gbytes.Say("root\n")) 328 }) 329 330 Context("and there is no /root directory in the image", func() { 331 BeforeEach(func() { 332 rootfs = "docker:///onsi/grace-busybox" 333 }) 334 335 It("still allows running as root", func() { 336 _, err := container.Run(garden.ProcessSpec{ 337 Path: "ls", 338 User: "root", 339 }, garden.ProcessIO{}) 340 341 Expect(err).ToNot(HaveOccurred()) 342 }) 343 }) 344 345 Context("by default (unprivileged)", func() { 346 It("does not get root privileges on host resources", func() { 347 process, err := container.Run(garden.ProcessSpec{ 348 Path: "sh", 349 User: "root", 350 Args: []string{"-c", "echo h > /proc/sysrq-trigger"}, 351 }, garden.ProcessIO{}) 352 Expect(err).ToNot(HaveOccurred()) 353 354 Expect(process.Wait()).ToNot(Equal(0)) 355 }) 356 357 It("can write to files in the /root directory", func() { 358 process, err := container.Run(garden.ProcessSpec{ 359 User: "root", 360 Path: "sh", 361 Args: []string{"-c", `touch /root/potato`}, 362 }, garden.ProcessIO{}) 363 Expect(err).ToNot(HaveOccurred()) 364 365 Expect(process.Wait()).To(Equal(0)) 366 }) 367 368 Context("with a docker image", func() { 369 BeforeEach(func() { 370 rootfs = "docker:///cloudfoundry/preexisting_users" 371 }) 372 373 It("sees root-owned files in the rootfs as owned by the container's root user", func() { 374 stdout := gbytes.NewBuffer() 375 process, err := container.Run(garden.ProcessSpec{ 376 User: "root", 377 Path: "sh", 378 Args: []string{"-c", `ls -l /sbin | grep -v wsh | grep -v hook`}, 379 }, garden.ProcessIO{Stdout: stdout}) 380 Expect(err).ToNot(HaveOccurred()) 381 382 Expect(process.Wait()).To(Equal(0)) 383 Expect(stdout).NotTo(gbytes.Say("nobody")) 384 Expect(stdout).NotTo(gbytes.Say("65534")) 385 Expect(stdout).To(gbytes.Say(" root ")) 386 }) 387 388 It("sees alice-owned files as owned by alice", func() { 389 stdout := gbytes.NewBuffer() 390 process, err := container.Run(garden.ProcessSpec{ 391 User: "alice", 392 Path: "sh", 393 Args: []string{"-c", `ls -l /home/alice`}, 394 }, garden.ProcessIO{Stdout: stdout}) 395 Expect(err).ToNot(HaveOccurred()) 396 397 Expect(process.Wait()).To(Equal(0)) 398 Expect(stdout).To(gbytes.Say(" alice ")) 399 Expect(stdout).To(gbytes.Say(" alicesfile")) 400 }) 401 402 It("lets alice write in /home/alice", func() { 403 process, err := container.Run(garden.ProcessSpec{ 404 User: "alice", 405 Path: "touch", 406 Args: []string{"/home/alice/newfile"}, 407 }, garden.ProcessIO{}) 408 Expect(err).ToNot(HaveOccurred()) 409 Expect(process.Wait()).To(Equal(0)) 410 }) 411 412 It("lets root write to files in the /root directory", func() { 413 process, err := container.Run(garden.ProcessSpec{ 414 User: "root", 415 Path: "sh", 416 Args: []string{"-c", `touch /root/potato`}, 417 }, garden.ProcessIO{}) 418 Expect(err).ToNot(HaveOccurred()) 419 Expect(process.Wait()).To(Equal(0)) 420 }) 421 422 It("preserves pre-existing dotfiles from base image", func() { 423 out := gbytes.NewBuffer() 424 process, err := container.Run(garden.ProcessSpec{ 425 User: "root", 426 Path: "cat", 427 Args: []string{"/.foo"}, 428 }, garden.ProcessIO{ 429 Stdout: out, 430 }) 431 Expect(err).ToNot(HaveOccurred()) 432 Expect(process.Wait()).To(Equal(0)) 433 Expect(out).To(gbytes.Say("this is a pre-existing dotfile")) 434 }) 435 }) 436 }) 437 438 Context("when the 'privileged' flag is set on the create call", func() { 439 BeforeEach(func() { 440 privilegedContainer = true 441 }) 442 443 It("gets real root privileges", func() { 444 process, err := container.Run(garden.ProcessSpec{ 445 Path: "sh", 446 User: "root", 447 Args: []string{"-c", "echo h > /proc/sysrq-trigger"}, 448 }, garden.ProcessIO{}) 449 Expect(err).ToNot(HaveOccurred()) 450 451 Expect(process.Wait()).To(Equal(0)) 452 }) 453 454 It("can write to files in the /root directory", func() { 455 process, err := container.Run(garden.ProcessSpec{ 456 User: "root", 457 Path: "sh", 458 Args: []string{"-c", `touch /root/potato`}, 459 }, garden.ProcessIO{}) 460 Expect(err).ToNot(HaveOccurred()) 461 462 Expect(process.Wait()).To(Equal(0)) 463 }) 464 465 It("sees root-owned files in the rootfs as owned by the container's root user", func() { 466 stdout := gbytes.NewBuffer() 467 process, err := container.Run(garden.ProcessSpec{ 468 User: "root", 469 Path: "sh", 470 Args: []string{"-c", `ls -l /sbin | grep -v wsh | grep -v hook`}, 471 }, garden.ProcessIO{Stdout: io.MultiWriter(GinkgoWriter, stdout)}) 472 Expect(err).ToNot(HaveOccurred()) 473 474 Expect(process.Wait()).To(Equal(0)) 475 Expect(stdout).NotTo(gbytes.Say("nobody")) 476 Expect(stdout).NotTo(gbytes.Say("65534")) 477 Expect(stdout).To(gbytes.Say(" root ")) 478 }) 479 }) 480 }) 481 482 Measure("it should stream stdout and stderr efficiently", func(b Benchmarker) { 483 b.Time("(baseline) streaming 50M of stdout to /dev/null", func() { 484 stdout := gbytes.NewBuffer() 485 stderr := gbytes.NewBuffer() 486 487 _, err := container.Run(garden.ProcessSpec{ 488 Path: "sh", 489 Args: []string{"-c", "tr '\\0' 'a' < /dev/zero | dd count=50 bs=1M of=/dev/null; echo done"}, 490 }, garden.ProcessIO{ 491 Stdout: stdout, 492 Stderr: stderr, 493 }) 494 Expect(err).ToNot(HaveOccurred()) 495 496 Eventually(stdout, "2s").Should(gbytes.Say("done\n")) 497 }) 498 499 time := b.Time("streaming 50M of data via garden", func() { 500 stdout := gbytes.NewBuffer() 501 stderr := gbytes.NewBuffer() 502 503 _, err := container.Run(garden.ProcessSpec{ 504 Path: "sh", 505 Args: []string{"-c", "tr '\\0' 'a' < /dev/zero | dd count=50 bs=1M; echo done"}, 506 }, garden.ProcessIO{ 507 Stdout: stdout, 508 Stderr: stderr, 509 }) 510 Expect(err).ToNot(HaveOccurred()) 511 512 Eventually(stdout, "2s").Should(gbytes.Say("done\n")) 513 }) 514 515 Expect(time.Seconds()).To(BeNumerically("<", 1)) 516 }, 10) 517 518 It("streams output back and reports the exit status", func() { 519 stdout := gbytes.NewBuffer() 520 stderr := gbytes.NewBuffer() 521 522 process, err := container.Run(garden.ProcessSpec{ 523 Path: "sh", 524 Args: []string{"-c", "sleep 0.5; echo $FIRST; sleep 0.5; echo $SECOND >&2; sleep 0.5; exit 42"}, 525 Env: []string{"FIRST=hello", "SECOND=goodbye"}, 526 }, garden.ProcessIO{ 527 Stdout: stdout, 528 Stderr: stderr, 529 }) 530 Expect(err).ToNot(HaveOccurred()) 531 532 Eventually(stdout).Should(gbytes.Say("hello\n")) 533 Eventually(stderr).Should(gbytes.Say("goodbye\n")) 534 Expect(process.Wait()).To(Equal(42)) 535 }) 536 537 It("sends a TERM signal to the process if requested", func() { 538 stdout := gbytes.NewBuffer() 539 540 process, err := container.Run(garden.ProcessSpec{ 541 Path: "sh", 542 Args: []string{"-c", ` 543 trap 'echo termed; exit 42' SIGTERM 544 545 while true; do 546 echo waiting 547 sleep 1 548 done 549 `}, 550 }, garden.ProcessIO{ 551 Stdout: io.MultiWriter(GinkgoWriter, stdout), 552 Stderr: GinkgoWriter, 553 }) 554 Expect(err).ToNot(HaveOccurred()) 555 556 Eventually(stdout).Should(gbytes.Say("waiting")) 557 Expect(process.Signal(garden.SignalTerminate)).To(Succeed()) 558 Eventually(stdout, "2s").Should(gbytes.Say("termed")) 559 Expect(process.Wait()).To(Equal(42)) 560 }) 561 562 It("sends a KILL signal to the process if requested", func() { 563 stdout := gbytes.NewBuffer() 564 565 process, err := container.Run(garden.ProcessSpec{ 566 Path: "sh", 567 Args: []string{"-c", ` 568 while true; do 569 echo waiting 570 sleep 1 571 done 572 `}, 573 }, garden.ProcessIO{ 574 Stdout: io.MultiWriter(GinkgoWriter, stdout), 575 Stderr: GinkgoWriter, 576 }) 577 Expect(err).ToNot(HaveOccurred()) 578 579 Eventually(stdout).Should(gbytes.Say("waiting")) 580 Expect(process.Signal(garden.SignalKill)).To(Succeed()) 581 Expect(process.Wait()).ToNot(Equal(0)) 582 }) 583 584 It("avoids a race condition when sending a kill signal", func(done Done) { 585 stdout := gbytes.NewBuffer() 586 587 for i := 0; i < 200; i++ { 588 process, err := container.Run(garden.ProcessSpec{ 589 Path: "sh", 590 Args: []string{"-c", `while true; do echo -n "x"; sleep 1; done`}, 591 }, garden.ProcessIO{ 592 Stdout: io.MultiWriter(GinkgoWriter, stdout), 593 Stderr: GinkgoWriter, 594 }) 595 Expect(err).ToNot(HaveOccurred()) 596 597 Expect(process.Signal(garden.SignalKill)).To(Succeed()) 598 Expect(process.Wait()).To(Equal(255)) 599 } 600 close(done) 601 }, 30.0) 602 603 It("collects the process's full output, even if it exits quickly after", func() { 604 for i := 0; i < 100; i++ { 605 stdout := gbytes.NewBuffer() 606 607 process, err := container.Run(garden.ProcessSpec{ 608 Path: "sh", 609 Args: []string{"-c", "cat <&0"}, 610 }, garden.ProcessIO{ 611 Stdin: bytes.NewBuffer([]byte("hi stdout")), 612 Stderr: os.Stderr, 613 Stdout: stdout, 614 }) 615 616 if err != nil { 617 println("ERROR: " + err.Error()) 618 select {} 619 } 620 621 Expect(err).ToNot(HaveOccurred()) 622 Expect(process.Wait()).To(Equal(0)) 623 624 Expect(stdout).To(gbytes.Say("hi stdout")) 625 } 626 }) 627 628 It("streams input to the process's stdin", func() { 629 stdout := gbytes.NewBuffer() 630 631 process, err := container.Run(garden.ProcessSpec{ 632 Path: "sh", 633 Args: []string{"-c", "cat <&0"}, 634 }, garden.ProcessIO{ 635 Stdin: bytes.NewBufferString("hello\nworld"), 636 Stdout: stdout, 637 }) 638 Expect(err).ToNot(HaveOccurred()) 639 640 Eventually(stdout).Should(gbytes.Say("hello\nworld")) 641 Expect(process.Wait()).To(Equal(0)) 642 }) 643 644 It("does not leak open files", func() { 645 openFileCount := func() int { 646 procFd := fmt.Sprintf("/proc/%d/fd", gardenRunner.Command.Process.Pid) 647 files, err := ioutil.ReadDir(procFd) 648 Expect(err).ToNot(HaveOccurred()) 649 return len(files) 650 } 651 652 initialOpenFileCount := openFileCount() 653 654 for i := 0; i < 50; i++ { 655 process, err := container.Run(garden.ProcessSpec{ 656 Path: "true", 657 }, garden.ProcessIO{ 658 Stdout: GinkgoWriter, 659 Stderr: GinkgoWriter, 660 }) 661 Expect(err).ToNot(HaveOccurred()) 662 Expect(process.Wait()).To(Equal(0)) 663 } 664 665 // there's some noise in 'open files' check, but it shouldn't grow 666 // linearly with the number of processes spawned 667 Eventually(openFileCount, "10s").Should(BeNumerically("<", initialOpenFileCount+10)) 668 }) 669 670 It("forwards the exit status even if stdin is still being written", func() { 671 // this covers the case of intermediaries shuffling i/o around (e.g. wsh) 672 // receiving SIGPIPE on write() due to the backing process exiting without 673 // flushing stdin 674 // 675 // in practice it's flaky; sometimes write() finishes just before the 676 // process exits, so run it ~10 times (observed it fail often in this range) 677 678 for i := 0; i < 10; i++ { 679 process, err := container.Run(garden.ProcessSpec{ 680 Path: "ls", 681 }, garden.ProcessIO{ 682 Stdin: bytes.NewBufferString(strings.Repeat("x", 1024)), 683 }) 684 Expect(err).ToNot(HaveOccurred()) 685 686 Expect(process.Wait()).To(Equal(0)) 687 } 688 }) 689 690 Context("with a memory limit", func() { 691 JustBeforeEach(func() { 692 err := container.LimitMemory(garden.MemoryLimits{ 693 LimitInBytes: 64 * 1024 * 1024, 694 }) 695 Expect(err).ToNot(HaveOccurred()) 696 }) 697 698 Context("when the process writes too much to /dev/shm", func() { 699 It("is killed", func() { 700 process, err := container.Run(garden.ProcessSpec{ 701 Path: "dd", 702 Args: []string{"if=/dev/urandom", "of=/dev/shm/too-big", "bs=1M", "count=65"}, 703 }, garden.ProcessIO{}) 704 Expect(err).ToNot(HaveOccurred()) 705 706 Expect(process.Wait()).ToNot(Equal(0)) 707 }) 708 }) 709 }) 710 711 Context("with a tty", func() { 712 It("executes the process with a raw tty with the given window size", func() { 713 stdout := gbytes.NewBuffer() 714 715 inR, inW := io.Pipe() 716 717 process, err := container.Run(garden.ProcessSpec{ 718 Path: "sh", 719 Args: []string{"-c", "read foo; stty -a"}, 720 TTY: &garden.TTYSpec{ 721 WindowSize: &garden.WindowSize{ 722 Columns: 123, 723 Rows: 456, 724 }, 725 }, 726 }, garden.ProcessIO{ 727 Stdin: inR, 728 Stdout: stdout, 729 }) 730 Expect(err).ToNot(HaveOccurred()) 731 732 _, err = inW.Write([]byte("hello")) 733 Expect(err).ToNot(HaveOccurred()) 734 735 Eventually(stdout).Should(gbytes.Say("hello")) 736 737 _, err = inW.Write([]byte("\n")) 738 Expect(err).ToNot(HaveOccurred()) 739 740 Eventually(stdout).Should(gbytes.Say("rows 456; columns 123;")) 741 742 Expect(process.Wait()).To(Equal(0)) 743 }) 744 745 It("can have its terminal resized", func() { 746 stdout := gbytes.NewBuffer() 747 748 inR, inW := io.Pipe() 749 750 process, err := container.Run(garden.ProcessSpec{ 751 Path: "sh", 752 Args: []string{ 753 "-c", 754 ` 755 trap "stty -a" SIGWINCH 756 757 # continuously read so that the trap can keep firing 758 while true; do 759 echo waiting 760 if read; then 761 exit 0 762 fi 763 done 764 `, 765 }, 766 TTY: &garden.TTYSpec{}, 767 }, garden.ProcessIO{ 768 Stdin: inR, 769 Stdout: stdout, 770 }) 771 Expect(err).ToNot(HaveOccurred()) 772 773 Eventually(stdout).Should(gbytes.Say("waiting")) 774 775 err = process.SetTTY(garden.TTYSpec{ 776 WindowSize: &garden.WindowSize{ 777 Columns: 123, 778 Rows: 456, 779 }, 780 }) 781 Expect(err).ToNot(HaveOccurred()) 782 783 Eventually(stdout).Should(gbytes.Say("rows 456; columns 123;")) 784 785 _, err = fmt.Fprintf(inW, "ok\n") 786 Expect(err).ToNot(HaveOccurred()) 787 788 Expect(process.Wait()).To(Equal(0)) 789 }) 790 }) 791 792 Context("with a working directory", func() { 793 It("executes with the working directory as the dir", func() { 794 stdout := gbytes.NewBuffer() 795 796 process, err := container.Run(garden.ProcessSpec{ 797 Path: "pwd", 798 Dir: "/usr", 799 }, garden.ProcessIO{ 800 Stdout: stdout, 801 }) 802 Expect(err).ToNot(HaveOccurred()) 803 804 Eventually(stdout).Should(gbytes.Say("/usr\n")) 805 Expect(process.Wait()).To(Equal(0)) 806 }) 807 }) 808 809 Context("and then attaching to it", func() { 810 It("streams output and the exit status to the attached request", func(done Done) { 811 stdout1 := gbytes.NewBuffer() 812 stdout2 := gbytes.NewBuffer() 813 814 process, err := container.Run(garden.ProcessSpec{ 815 Path: "sh", 816 Args: []string{"-c", "sleep 2; echo hello; sleep 0.5; echo goodbye; sleep 0.5; exit 42"}, 817 }, garden.ProcessIO{ 818 Stdout: stdout1, 819 }) 820 Expect(err).ToNot(HaveOccurred()) 821 822 attached, err := container.Attach(process.ID(), garden.ProcessIO{ 823 Stdout: stdout2, 824 }) 825 Expect(err).ToNot(HaveOccurred()) 826 827 time.Sleep(2 * time.Second) 828 829 Eventually(stdout1).Should(gbytes.Say("hello\n")) 830 Eventually(stdout1).Should(gbytes.Say("goodbye\n")) 831 832 Eventually(stdout2).Should(gbytes.Say("hello\n")) 833 Eventually(stdout2).Should(gbytes.Say("goodbye\n")) 834 835 Expect(process.Wait()).To(Equal(42)) 836 Expect(attached.Wait()).To(Equal(42)) 837 838 close(done) 839 }, 10.0) 840 }) 841 842 Context("and then sending a Stop request", func() { 843 It("terminates all running processes", func() { 844 stdout := gbytes.NewBuffer() 845 846 process, err := container.Run(garden.ProcessSpec{ 847 Path: "sh", 848 Args: []string{ 849 "-c", 850 ` 851 trap 'exit 42' SIGTERM 852 853 # sync with test, and allow trap to fire when not sleeping 854 while true; do 855 echo waiting 856 sleep 0.5 857 done 858 `, 859 }, 860 }, garden.ProcessIO{ 861 Stdout: stdout, 862 }) 863 Expect(err).ToNot(HaveOccurred()) 864 865 Eventually(stdout, 30).Should(gbytes.Say("waiting")) 866 867 err = container.Stop(false) 868 Expect(err).ToNot(HaveOccurred()) 869 870 Expect(process.Wait()).To(Equal(42)) 871 }) 872 873 It("recursively terminates all child processes", func(done Done) { 874 defer close(done) 875 876 stdout := gbytes.NewBuffer() 877 878 process, err := container.Run(garden.ProcessSpec{ 879 Path: "sh", 880 Args: []string{ 881 "-c", 882 ` 883 # don't die until child processes die 884 trap wait SIGTERM 885 886 # spawn child that exits when it receives TERM 887 sh -c 'sleep 100 & wait' & 888 889 # sync with test 890 echo waiting 891 892 # wait on children 893 wait 894 `, 895 }, 896 }, garden.ProcessIO{ 897 Stdout: stdout, 898 }) 899 900 Expect(err).ToNot(HaveOccurred()) 901 902 Eventually(stdout, 5).Should(gbytes.Say("waiting\n")) 903 904 stoppedAt := time.Now() 905 906 err = container.Stop(false) 907 Expect(err).ToNot(HaveOccurred()) 908 909 Expect(process.Wait()).To(Equal(143)) // 143 = 128 + SIGTERM 910 911 Expect(time.Since(stoppedAt)).To(BeNumerically("<=", 5*time.Second)) 912 }, 15) 913 914 Context("when a process does not die 10 seconds after receiving SIGTERM", func() { 915 It("is forcibly killed", func(done Done) { 916 defer close(done) 917 918 process, err := container.Run(garden.ProcessSpec{ 919 Path: "sh", 920 Args: []string{ 921 "-c", 922 ` 923 trap "echo cant touch this; sleep 1000" SIGTERM 924 925 echo waiting 926 sleep 1000 & 927 wait 928 `, 929 }, 930 }, garden.ProcessIO{}) 931 932 Expect(err).ToNot(HaveOccurred()) 933 934 stoppedAt := time.Now() 935 936 err = container.Stop(false) 937 Expect(err).ToNot(HaveOccurred()) 938 939 Expect(process.Wait()).ToNot(Equal(0)) // either 137 or 255 940 941 Expect(time.Since(stoppedAt)).To(BeNumerically(">=", 10*time.Second)) 942 }, 15) 943 }) 944 }) 945 }) 946 947 Context("and streaming files in", func() { 948 var tarStream io.Reader 949 950 JustBeforeEach(func() { 951 tmpdir, err := ioutil.TempDir("", "some-temp-dir-parent") 952 Expect(err).ToNot(HaveOccurred()) 953 954 tgzPath := filepath.Join(tmpdir, "some.tgz") 955 956 archiver.CreateTarGZArchive( 957 tgzPath, 958 []archiver.ArchiveFile{ 959 { 960 Name: "./some-temp-dir", 961 Dir: true, 962 }, 963 { 964 Name: "./some-temp-dir/some-temp-file", 965 Body: "some-body", 966 }, 967 }, 968 ) 969 970 tgz, err := os.Open(tgzPath) 971 Expect(err).ToNot(HaveOccurred()) 972 973 tarStream, err = gzip.NewReader(tgz) 974 Expect(err).ToNot(HaveOccurred()) 975 }) 976 977 It("creates the files in the container, as the vcap user", func() { 978 err := container.StreamIn("/home/vcap", tarStream) 979 Expect(err).ToNot(HaveOccurred()) 980 981 process, err := container.Run(garden.ProcessSpec{ 982 Path: "test", 983 Args: []string{"-f", "/home/vcap/some-temp-dir/some-temp-file"}, 984 }, garden.ProcessIO{}) 985 Expect(err).ToNot(HaveOccurred()) 986 987 Expect(process.Wait()).To(Equal(0)) 988 989 output := gbytes.NewBuffer() 990 process, err = container.Run(garden.ProcessSpec{ 991 Path: "ls", 992 Args: []string{"-al", "/home/vcap/some-temp-dir/some-temp-file"}, 993 }, garden.ProcessIO{ 994 Stdout: output, 995 }) 996 Expect(err).ToNot(HaveOccurred()) 997 998 Expect(process.Wait()).To(Equal(0)) 999 1000 // output should look like -rwxrwxrwx 1 vcap vcap 9 Jan 1 1970 /tmp/some-container-dir/some-temp-dir/some-temp-file 1001 Expect(output).To(gbytes.Say("vcap")) 1002 Expect(output).To(gbytes.Say("vcap")) 1003 }) 1004 1005 PIt("can create files in /tmp") 1006 1007 Context("in a privileged container", func() { 1008 BeforeEach(func() { 1009 privilegedContainer = true 1010 }) 1011 1012 It("streams in relative to the default run directory", func() { 1013 err := container.StreamIn(".", tarStream) 1014 Expect(err).ToNot(HaveOccurred()) 1015 1016 process, err := container.Run(garden.ProcessSpec{ 1017 Path: "test", 1018 Args: []string{"-f", "some-temp-dir/some-temp-file"}, 1019 }, garden.ProcessIO{}) 1020 Expect(err).ToNot(HaveOccurred()) 1021 1022 Expect(process.Wait()).To(Equal(0)) 1023 }) 1024 }) 1025 1026 It("streams in relative to the default run directory", func() { 1027 err := container.StreamIn(".", tarStream) 1028 Expect(err).ToNot(HaveOccurred()) 1029 1030 process, err := container.Run(garden.ProcessSpec{ 1031 Path: "test", 1032 Args: []string{"-f", "some-temp-dir/some-temp-file"}, 1033 }, garden.ProcessIO{}) 1034 Expect(err).ToNot(HaveOccurred()) 1035 1036 Expect(process.Wait()).To(Equal(0)) 1037 }) 1038 1039 It("returns an error when the tar process dies", func() { 1040 err := container.StreamIn("/tmp/some-container-dir", &io.LimitedReader{ 1041 R: tarStream, 1042 N: 10, 1043 }) 1044 Expect(err).To(HaveOccurred()) 1045 }) 1046 1047 Context("and then copying them out", func() { 1048 It("streams the directory", func() { 1049 process, err := container.Run(garden.ProcessSpec{ 1050 Path: "sh", 1051 Args: []string{"-c", `mkdir -p some-outer-dir/some-inner-dir && touch some-outer-dir/some-inner-dir/some-file`}, 1052 }, garden.ProcessIO{}) 1053 Expect(err).ToNot(HaveOccurred()) 1054 1055 Expect(process.Wait()).To(Equal(0)) 1056 1057 tarOutput, err := container.StreamOut("some-outer-dir/some-inner-dir") 1058 Expect(err).ToNot(HaveOccurred()) 1059 1060 tarReader := tar.NewReader(tarOutput) 1061 1062 header, err := tarReader.Next() 1063 Expect(err).ToNot(HaveOccurred()) 1064 Expect(header.Name).To(Equal("some-inner-dir/")) 1065 1066 header, err = tarReader.Next() 1067 Expect(err).ToNot(HaveOccurred()) 1068 Expect(header.Name).To(Equal("some-inner-dir/some-file")) 1069 }) 1070 1071 Context("with a trailing slash", func() { 1072 It("streams the contents of the directory", func() { 1073 process, err := container.Run(garden.ProcessSpec{ 1074 Path: "sh", 1075 Args: []string{"-c", `mkdir -p some-container-dir && touch some-container-dir/some-file`}, 1076 }, garden.ProcessIO{}) 1077 Expect(err).ToNot(HaveOccurred()) 1078 1079 Expect(process.Wait()).To(Equal(0)) 1080 1081 tarOutput, err := container.StreamOut("some-container-dir/") 1082 Expect(err).ToNot(HaveOccurred()) 1083 1084 tarReader := tar.NewReader(tarOutput) 1085 1086 header, err := tarReader.Next() 1087 Expect(err).ToNot(HaveOccurred()) 1088 Expect(header.Name).To(Equal("./")) 1089 1090 header, err = tarReader.Next() 1091 Expect(err).ToNot(HaveOccurred()) 1092 Expect(header.Name).To(Equal("./some-file")) 1093 }) 1094 }) 1095 }) 1096 }) 1097 1098 Context("and sending a Stop request", func() { 1099 It("changes the container's state to 'stopped'", func() { 1100 err := container.Stop(false) 1101 Expect(err).ToNot(HaveOccurred()) 1102 1103 info, err := container.Info() 1104 Expect(err).ToNot(HaveOccurred()) 1105 1106 Expect(info.State).To(Equal("stopped")) 1107 }) 1108 }) 1109 1110 Context("after destroying the container", func() { 1111 It("should return api.ContainerNotFoundError when deleting the container again", func() { 1112 Expect(client.Destroy(container.Handle())).To(Succeed()) 1113 Expect(client.Destroy(container.Handle())).To(MatchError(garden.ContainerNotFoundError{container.Handle()})) 1114 container = nil 1115 }) 1116 1117 It("should ensure any iptables rules which were created no longer exist", func() { 1118 handle := container.Handle() 1119 Expect(client.Destroy(handle)).To(Succeed()) 1120 container = nil 1121 1122 iptables, err := gexec.Start(exec.Command("iptables", "-L"), GinkgoWriter, GinkgoWriter) 1123 Expect(err).ToNot(HaveOccurred()) 1124 Eventually(iptables, "2s").Should(gexec.Exit()) 1125 Expect(iptables).ToNot(gbytes.Say(handle)) 1126 }) 1127 1128 It("destroys multiple containers based on same rootfs", func() { 1129 c1, err := client.Create(garden.ContainerSpec{ 1130 RootFSPath: "docker:///busybox", 1131 Privileged: false, 1132 }) 1133 Expect(err).ToNot(HaveOccurred()) 1134 c2, err := client.Create(garden.ContainerSpec{ 1135 RootFSPath: "docker:///busybox", 1136 Privileged: false, 1137 }) 1138 Expect(err).ToNot(HaveOccurred()) 1139 1140 Expect(client.Destroy(c1.Handle())).To(Succeed()) 1141 Expect(client.Destroy(c2.Handle())).To(Succeed()) 1142 }) 1143 1144 It("should not leak network namespace", func() { 1145 info, err := container.Info() 1146 Expect(err).ToNot(HaveOccurred()) 1147 Expect(info.State).To(Equal("active")) 1148 1149 pidPath := filepath.Join(info.ContainerPath, "run", "wshd.pid") 1150 1151 _, err = ioutil.ReadFile(pidPath) 1152 Expect(err).ToNot(HaveOccurred()) 1153 1154 Expect(client.Destroy(container.Handle())).To(Succeed()) 1155 container = nil 1156 1157 stdout := gbytes.NewBuffer() 1158 cmd, err := gexec.Start( 1159 exec.Command( 1160 "sh", 1161 "-c", 1162 "mount -n -t tmpfs tmpfs /sys && ip netns list && umount /sys", 1163 ), 1164 stdout, 1165 GinkgoWriter, 1166 ) 1167 1168 Expect(err).ToNot(HaveOccurred()) 1169 Expect(cmd.Wait("1s").ExitCode()).To(Equal(0)) 1170 Expect(stdout.Contents()).To(Equal([]byte{})) 1171 }) 1172 }) 1173 }) 1174 })