github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/worker/gclient/connection/connection_test.go (about) 1 package connection_test 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net" 13 "net/http" 14 "net/url" 15 "strings" 16 "time" 17 18 "code.cloudfoundry.org/lager/lagertest" 19 . "github.com/pf-qiu/concourse/v6/atc/worker/gclient/connection" 20 "github.com/pf-qiu/concourse/v6/atc/worker/gclient/connection/connectionfakes" 21 . "github.com/onsi/ginkgo" 22 . "github.com/onsi/gomega" 23 "github.com/onsi/gomega/gbytes" 24 "github.com/onsi/gomega/ghttp" 25 "github.com/tedsuo/rata" 26 27 "code.cloudfoundry.org/garden" 28 "code.cloudfoundry.org/garden/transport" 29 ) 30 31 var _ = Describe("Connection", func() { 32 var ( 33 connection Connection 34 resourceLimits garden.ResourceLimits 35 server *ghttp.Server 36 hijacker HijackStreamer 37 network string 38 address string 39 ) 40 41 BeforeEach(func() { 42 server = ghttp.NewServer() 43 network = "tcp" 44 address = server.HTTPTestServer.Listener.Addr().String() 45 hijacker = NewHijackStreamer(network, address) 46 }) 47 48 JustBeforeEach(func() { 49 connection = NewWithHijacker(hijacker, lagertest.NewTestLogger("test-connection")) 50 }) 51 52 BeforeEach(func() { 53 rlimits := &garden.ResourceLimits{ 54 As: uint64ptr(1), 55 Core: uint64ptr(2), 56 Cpu: uint64ptr(4), 57 Data: uint64ptr(5), 58 Fsize: uint64ptr(6), 59 Locks: uint64ptr(7), 60 Memlock: uint64ptr(8), 61 Msgqueue: uint64ptr(9), 62 Nice: uint64ptr(10), 63 Nofile: uint64ptr(11), 64 Nproc: uint64ptr(12), 65 Rss: uint64ptr(13), 66 Rtprio: uint64ptr(14), 67 Sigpending: uint64ptr(15), 68 Stack: uint64ptr(16), 69 } 70 71 resourceLimits = garden.ResourceLimits{ 72 As: rlimits.As, 73 Core: rlimits.Core, 74 Cpu: rlimits.Cpu, 75 Data: rlimits.Data, 76 Fsize: rlimits.Fsize, 77 Locks: rlimits.Locks, 78 Memlock: rlimits.Memlock, 79 Msgqueue: rlimits.Msgqueue, 80 Nice: rlimits.Nice, 81 Nofile: rlimits.Nofile, 82 Nproc: rlimits.Nproc, 83 Rss: rlimits.Rss, 84 Rtprio: rlimits.Rtprio, 85 Sigpending: rlimits.Sigpending, 86 Stack: rlimits.Stack, 87 } 88 }) 89 90 Describe("Ping", func() { 91 Context("when the response is successful", func() { 92 BeforeEach(func() { 93 server.AppendHandlers( 94 ghttp.CombineHandlers( 95 ghttp.VerifyRequest("GET", "/ping"), 96 ghttp.RespondWith(200, "{}"), 97 ), 98 ) 99 }) 100 101 It("should ping the server", func() { 102 err := connection.Ping() 103 Expect(err).ToNot(HaveOccurred()) 104 }) 105 }) 106 107 Context("when the request fails", func() { 108 BeforeEach(func() { 109 server.AppendHandlers( 110 ghttp.CombineHandlers( 111 ghttp.VerifyRequest("GET", "/ping"), 112 ghttp.RespondWith(500, ""), 113 ), 114 ) 115 }) 116 117 It("should return an error", func() { 118 err := connection.Ping() 119 Expect(err).To(HaveOccurred()) 120 }) 121 }) 122 123 Context("when the request fails with special error code http.StatusServiceUnavailable", func() { 124 BeforeEach(func() { 125 server.AppendHandlers( 126 ghttp.CombineHandlers( 127 ghttp.VerifyRequest("GET", "/ping"), 128 ghttp.RespondWith(http.StatusGatewayTimeout, `{ "Type": "ServiceUnavailableError" , "Message": "Special Error Message"}`), 129 ), 130 ) 131 }) 132 133 It("should return an error without the http info in the error message", func() { 134 err := connection.Ping() 135 Expect(err).To(MatchError("Special Error Message")) 136 }) 137 138 It("should return an error of the appropriate type", func() { 139 err := connection.Ping() 140 Expect(err).To(BeAssignableToTypeOf(garden.ServiceUnavailableError{})) 141 }) 142 }) 143 144 Context("when the request fails with extra special error code http.StatusInternalServerError", func() { 145 BeforeEach(func() { 146 server.AppendHandlers( 147 ghttp.CombineHandlers( 148 ghttp.VerifyRequest("GET", "/ping"), 149 ghttp.RespondWith(http.StatusGatewayTimeout, `{ "Type": "UnrecoverableError" , "Message": "Extra Special Error Message"}`), 150 ), 151 ) 152 }) 153 154 It("should return an error without the http info in the error message", func() { 155 err := connection.Ping() 156 Expect(err).To(MatchError("Extra Special Error Message")) 157 }) 158 159 It("should return an error of the appropriate unrecoverable type", func() { 160 err := connection.Ping() 161 Expect(err).To(BeAssignableToTypeOf(garden.UnrecoverableError{})) 162 }) 163 }) 164 }) 165 166 Describe("Getting capacity", func() { 167 Context("when the response is successful", func() { 168 BeforeEach(func() { 169 server.AppendHandlers( 170 ghttp.CombineHandlers( 171 ghttp.VerifyRequest("GET", "/capacity"), 172 ghttp.RespondWith(200, marshalProto(&garden.Capacity{ 173 MemoryInBytes: 1111, 174 DiskInBytes: 2222, 175 MaxContainers: 42, 176 })))) 177 }) 178 179 It("should return the server's capacity", func() { 180 capacity, err := connection.Capacity() 181 Expect(err).ToNot(HaveOccurred()) 182 183 Expect(capacity.MemoryInBytes).To(BeNumerically("==", 1111)) 184 Expect(capacity.DiskInBytes).To(BeNumerically("==", 2222)) 185 Expect(capacity.MaxContainers).To(BeNumerically("==", 42)) 186 }) 187 }) 188 189 Context("when the request fails", func() { 190 BeforeEach(func() { 191 server.AppendHandlers( 192 ghttp.CombineHandlers( 193 ghttp.VerifyRequest("GET", "/capacity"), 194 ghttp.RespondWith(500, ""))) 195 }) 196 197 It("should return an error", func() { 198 _, err := connection.Capacity() 199 Expect(err).To(HaveOccurred()) 200 }) 201 }) 202 }) 203 204 Describe("Creating", func() { 205 var spec garden.ContainerSpec 206 207 JustBeforeEach(func() { 208 server.AppendHandlers( 209 ghttp.CombineHandlers( 210 ghttp.VerifyRequest("POST", "/containers"), 211 verifyRequestBody(&spec, &garden.ContainerSpec{}), 212 ghttp.RespondWith(200, marshalProto(&struct{ Handle string }{"foohandle"})))) 213 }) 214 215 Context("with an empty ContainerSpec", func() { 216 BeforeEach(func() { 217 spec = garden.ContainerSpec{} 218 }) 219 220 It("sends the ContainerSpec over the connection as JSON", func() { 221 handle, err := connection.Create(spec) 222 Expect(err).ToNot(HaveOccurred()) 223 Expect(handle).To(Equal("foohandle")) 224 }) 225 }) 226 227 Context("with a fully specified ContainerSpec", func() { 228 BeforeEach(func() { 229 spec = garden.ContainerSpec{ 230 Handle: "some-handle", 231 GraceTime: 10 * time.Second, 232 RootFSPath: "some-rootfs-path", 233 Network: "some-network", 234 BindMounts: []garden.BindMount{ 235 { 236 SrcPath: "/src-a", 237 DstPath: "/dst-a", 238 Mode: garden.BindMountModeRO, 239 Origin: garden.BindMountOriginHost, 240 }, 241 { 242 SrcPath: "/src-b", 243 DstPath: "/dst-b", 244 Mode: garden.BindMountModeRW, 245 Origin: garden.BindMountOriginContainer, 246 }, 247 }, 248 Properties: map[string]string{ 249 "foo": "bar", 250 }, 251 Env: []string{"env1=env1Value1"}, 252 } 253 }) 254 255 It("sends the ContainerSpec over the connection as JSON", func() { 256 handle, err := connection.Create(spec) 257 Expect(err).ToNot(HaveOccurred()) 258 Expect(handle).To(Equal("foohandle")) 259 }) 260 }) 261 }) 262 263 Describe("Destroying", func() { 264 Context("when destroying succeeds", func() { 265 BeforeEach(func() { 266 server.AppendHandlers( 267 ghttp.CombineHandlers( 268 ghttp.VerifyRequest("DELETE", "/containers/foo"), 269 ghttp.RespondWith(200, "{}"))) 270 }) 271 272 It("should stop the container", func() { 273 err := connection.Destroy("foo") 274 Expect(err).ToNot(HaveOccurred()) 275 }) 276 }) 277 278 Context("when destroying fails because the container doesn't exist", func() { 279 BeforeEach(func() { 280 server.AppendHandlers( 281 ghttp.CombineHandlers( 282 ghttp.VerifyRequest("DELETE", "/containers/foo"), 283 ghttp.RespondWith(404, `{ "Type": "ContainerNotFoundError", "Handle" : "some handle"}`))) 284 }) 285 286 It("return an appropriate error with the message", func() { 287 err := connection.Destroy("foo") 288 Expect(err).To(MatchError(garden.ContainerNotFoundError{Handle: "some handle"})) 289 }) 290 }) 291 }) 292 293 Describe("Stopping", func() { 294 BeforeEach(func() { 295 server.AppendHandlers( 296 ghttp.CombineHandlers( 297 ghttp.VerifyRequest("PUT", "/containers/foo/stop"), 298 verifyRequestBody(map[string]interface{}{ 299 "kill": true, 300 }, make(map[string]interface{})), 301 ghttp.RespondWith(200, "{}"))) 302 }) 303 304 It("should stop the container", func() { 305 err := connection.Stop("foo", true) 306 Expect(err).ToNot(HaveOccurred()) 307 }) 308 }) 309 310 Describe("fetching limit info", func() { 311 Describe("getting memory limits", func() { 312 BeforeEach(func() { 313 server.AppendHandlers( 314 ghttp.CombineHandlers( 315 ghttp.VerifyRequest("GET", "/containers/foo/limits/memory"), 316 ghttp.RespondWith(200, marshalProto(&garden.MemoryLimits{ 317 LimitInBytes: 40, 318 }, &garden.MemoryLimits{})), 319 ), 320 ) 321 }) 322 323 It("gets the memory limit", func() { 324 currentLimits, err := connection.CurrentMemoryLimits("foo") 325 Expect(err).ToNot(HaveOccurred()) 326 Expect(currentLimits.LimitInBytes).To(BeNumerically("==", 40)) 327 }) 328 }) 329 330 Describe("getting cpu limits", func() { 331 BeforeEach(func() { 332 server.AppendHandlers( 333 ghttp.CombineHandlers( 334 ghttp.VerifyRequest("GET", "/containers/foo/limits/cpu"), 335 ghttp.RespondWith(200, marshalProto(&garden.CPULimits{ 336 LimitInShares: 40, 337 })), 338 ), 339 ) 340 }) 341 342 It("gets the cpu limit", func() { 343 limits, err := connection.CurrentCPULimits("foo") 344 Expect(err).ToNot(HaveOccurred()) 345 346 Expect(limits.LimitInShares).To(BeNumerically("==", 40)) 347 }) 348 }) 349 350 Describe("getting bandwidth limits", func() { 351 BeforeEach(func() { 352 server.AppendHandlers( 353 ghttp.CombineHandlers( 354 ghttp.VerifyRequest("GET", "/containers/foo/limits/bandwidth"), 355 ghttp.RespondWith(200, marshalProto(&garden.BandwidthLimits{ 356 RateInBytesPerSecond: 1, 357 BurstRateInBytesPerSecond: 2, 358 })), 359 ), 360 ) 361 }) 362 363 It("gets the bandwidth limit", func() { 364 limits, err := connection.CurrentBandwidthLimits("foo") 365 Expect(err).ToNot(HaveOccurred()) 366 367 Expect(limits.RateInBytesPerSecond).To(BeNumerically("==", 1)) 368 Expect(limits.BurstRateInBytesPerSecond).To(BeNumerically("==", 2)) 369 }) 370 }) 371 }) 372 373 Describe("NetIn", func() { 374 BeforeEach(func() { 375 server.AppendHandlers( 376 ghttp.CombineHandlers( 377 ghttp.VerifyRequest("POST", "/containers/foo-handle/net/in"), 378 verifyRequestBody(map[string]interface{}{ 379 "handle": "foo-handle", 380 "host_port": float64(8080), 381 "container_port": float64(8081), 382 }, make(map[string]interface{})), 383 ghttp.RespondWith(200, marshalProto(map[string]interface{}{ 384 "host_port": 1234, 385 "container_port": 1235, 386 })))) 387 }) 388 389 It("should return the allocated ports", func() { 390 hostPort, containerPort, err := connection.NetIn("foo-handle", 8080, 8081) 391 Expect(err).ToNot(HaveOccurred()) 392 Expect(hostPort).To(Equal(uint32(1234))) 393 Expect(containerPort).To(Equal(uint32(1235))) 394 }) 395 }) 396 397 Describe("NetOut", func() { 398 var ( 399 rule garden.NetOutRule 400 handle string 401 ) 402 403 BeforeEach(func() { 404 handle = "foo-handle" 405 }) 406 407 JustBeforeEach(func() { 408 server.AppendHandlers( 409 ghttp.CombineHandlers( 410 ghttp.VerifyRequest("POST", fmt.Sprintf("/containers/%s/net/out", handle)), 411 verifyRequestBody(&rule, &garden.NetOutRule{}), 412 ghttp.RespondWith(200, "{}"))) 413 }) 414 415 Context("when a NetOutRule is passed", func() { 416 BeforeEach(func() { 417 rule = garden.NetOutRule{ 418 Protocol: garden.ProtocolICMP, 419 Networks: []garden.IPRange{garden.IPRangeFromIP(net.ParseIP("1.2.3.4"))}, 420 Ports: []garden.PortRange{garden.PortRangeFromPort(2), garden.PortRangeFromPort(4)}, 421 ICMPs: &garden.ICMPControl{Type: 3, Code: garden.ICMPControlCode(3)}, 422 Log: true, 423 } 424 }) 425 426 It("should send the rule over the wire", func() { 427 Expect(connection.NetOut(handle, rule)).To(Succeed()) 428 }) 429 }) 430 }) 431 432 Describe("Listing containers", func() { 433 BeforeEach(func() { 434 server.AppendHandlers( 435 ghttp.CombineHandlers( 436 ghttp.VerifyRequest("GET", "/containers", "foo=bar"), 437 ghttp.RespondWith(200, marshalProto(&struct { 438 Handles []string `json:"handles"` 439 }{ 440 []string{"container1", "container2", "container3"}, 441 })))) 442 }) 443 444 It("should return the list of containers", func() { 445 handles, err := connection.List(map[string]string{"foo": "bar"}) 446 447 Expect(err).ToNot(HaveOccurred()) 448 Expect(handles).To(Equal([]string{"container1", "container2", "container3"})) 449 }) 450 }) 451 452 Describe("Getting container properties", func() { 453 handle := "container-handle" 454 var status int 455 456 BeforeEach(func() { 457 status = 200 458 }) 459 460 JustBeforeEach(func() { 461 server.AppendHandlers( 462 ghttp.CombineHandlers( 463 ghttp.VerifyRequest("GET", fmt.Sprintf("/containers/%s/properties", handle)), 464 ghttp.RespondWith(status, "{\"foo\": \"bar\"}"))) 465 }) 466 467 It("returns the map of properties", func() { 468 properties, err := connection.Properties(handle) 469 470 Expect(err).ToNot(HaveOccurred()) 471 Expect(properties).To( 472 Equal(garden.Properties{ 473 "foo": "bar", 474 }), 475 ) 476 }) 477 478 Context("when getting container properties fails", func() { 479 BeforeEach(func() { 480 status = 400 481 }) 482 483 It("returns an error", func() { 484 _, err := connection.Properties(handle) 485 Expect(err).To(HaveOccurred()) 486 }) 487 }) 488 }) 489 490 Describe("Get container property", func() { 491 492 handle := "container-handle" 493 propertyName := "property_name" 494 propertyValue := "property_value" 495 var status int 496 497 BeforeEach(func() { 498 status = 200 499 }) 500 501 JustBeforeEach(func() { 502 server.AppendHandlers( 503 ghttp.CombineHandlers( 504 ghttp.VerifyRequest("GET", fmt.Sprintf("/containers/%s/properties/%s", handle, propertyName)), 505 ghttp.RespondWith(status, fmt.Sprintf("{\"value\": \"%s\"}", propertyValue)))) 506 }) 507 508 It("returns the property", func() { 509 property, err := connection.Property(handle, propertyName) 510 511 Expect(err).ToNot(HaveOccurred()) 512 Expect(property).To(Equal(propertyValue)) 513 }) 514 515 Context("when getting container property fails", func() { 516 BeforeEach(func() { 517 status = 400 518 }) 519 520 It("returns an error", func() { 521 _, err := connection.Property(handle, propertyName) 522 Expect(err).To(HaveOccurred()) 523 }) 524 }) 525 526 }) 527 528 Describe("Getting container metrics", func() { 529 handle := "container-handle" 530 metrics := garden.Metrics{ 531 MemoryStat: garden.ContainerMemoryStat{ 532 Cache: 1, 533 Rss: 2, 534 MappedFile: 3, 535 Pgpgin: 4, 536 Pgpgout: 5, 537 Swap: 6, 538 Pgfault: 7, 539 Pgmajfault: 8, 540 InactiveAnon: 9, 541 ActiveAnon: 10, 542 InactiveFile: 11, 543 ActiveFile: 12, 544 Unevictable: 13, 545 HierarchicalMemoryLimit: 14, 546 HierarchicalMemswLimit: 15, 547 TotalCache: 16, 548 TotalRss: 17, 549 TotalMappedFile: 18, 550 TotalPgpgin: 19, 551 TotalPgpgout: 20, 552 TotalSwap: 21, 553 TotalPgfault: 22, 554 TotalPgmajfault: 23, 555 TotalInactiveAnon: 24, 556 TotalActiveAnon: 25, 557 TotalInactiveFile: 26, 558 TotalActiveFile: 27, 559 TotalUnevictable: 28, 560 TotalUsageTowardLimit: 7, // TotalRss+(TotalCache-TotalInactiveFile) 561 }, 562 CPUStat: garden.ContainerCPUStat{ 563 Usage: 1, 564 User: 2, 565 System: 3, 566 }, 567 568 DiskStat: garden.ContainerDiskStat{ 569 TotalBytesUsed: 11, 570 TotalInodesUsed: 12, 571 ExclusiveBytesUsed: 13, 572 ExclusiveInodesUsed: 14, 573 }, 574 } 575 var status int 576 577 BeforeEach(func() { 578 status = 200 579 }) 580 581 JustBeforeEach(func() { 582 server.AppendHandlers( 583 ghttp.CombineHandlers( 584 ghttp.VerifyRequest("GET", fmt.Sprintf("/containers/%s/metrics", handle)), 585 ghttp.RespondWith(status, marshalProto(metrics)))) 586 }) 587 588 It("returns the MemoryStat, CPUStat and DiskStat", func() { 589 returnedMetrics, err := connection.Metrics(handle) 590 591 Expect(err).ToNot(HaveOccurred()) 592 Expect(returnedMetrics).To(Equal(metrics)) 593 }) 594 595 Context("when getting container metrics fails", func() { 596 BeforeEach(func() { 597 status = 400 598 }) 599 600 It("returns an error", func() { 601 _, err := connection.Metrics(handle) 602 Expect(err).To(HaveOccurred()) 603 }) 604 }) 605 }) 606 607 Describe("Setting the grace time", func() { 608 var ( 609 status int 610 handle string 611 graceTime time.Duration 612 ) 613 614 BeforeEach(func() { 615 handle = "container-handle" 616 graceTime = time.Second * 5 617 status = 200 618 }) 619 620 JustBeforeEach(func() { 621 server.AppendHandlers( 622 ghttp.CombineHandlers( 623 ghttp.VerifyRequest("PUT", fmt.Sprintf("/containers/%s/grace_time", handle)), 624 // interface{} confusion: the JSON decoder decodes numberics to float64... 625 verifyRequestBody(float64(graceTime), float64(0)), 626 ghttp.RespondWith(status, "{}"), 627 ), 628 ) 629 }) 630 631 It("send SetGraceTime request", func() { 632 Expect(connection.SetGraceTime(handle, graceTime)).To(Succeed()) 633 }) 634 635 Context("when setting grace time fails", func() { 636 BeforeEach(func() { 637 status = 400 638 }) 639 640 It("returns an error", func() { 641 Expect(connection.SetGraceTime(handle, graceTime)).ToNot(Succeed()) 642 }) 643 }) 644 }) 645 646 Describe("Getting container info", func() { 647 var infoResponse garden.ContainerInfo 648 649 JustBeforeEach(func() { 650 infoResponse = garden.ContainerInfo{ 651 State: "chilling out", 652 Events: []string{ 653 "maxing", 654 "relaxing all cool", 655 }, 656 HostIP: "host-ip", 657 ContainerIP: "container-ip", 658 ContainerPath: "container-path", 659 ProcessIDs: []string{"process-handle-1", "process-handle-2"}, 660 Properties: garden.Properties{ 661 "prop-key": "prop-value", 662 }, 663 MappedPorts: []garden.PortMapping{ 664 {HostPort: 1234, ContainerPort: 5678}, 665 {HostPort: 1235, ContainerPort: 5679}, 666 }, 667 } 668 669 server.AppendHandlers( 670 ghttp.CombineHandlers( 671 ghttp.VerifyRequest("GET", "/containers/some-handle/info"), 672 ghttp.RespondWith(200, marshalProto(infoResponse)))) 673 }) 674 675 It("should return the container's info", func() { 676 info, err := connection.Info("some-handle") 677 Expect(err).ToNot(HaveOccurred()) 678 679 Expect(info).To(Equal(infoResponse)) 680 }) 681 }) 682 683 Describe("BulkInfo", func() { 684 685 expectedBulkInfo := map[string]garden.ContainerInfoEntry{ 686 "handle1": garden.ContainerInfoEntry{ 687 Info: garden.ContainerInfo{ 688 State: "container1state", 689 }, 690 }, 691 "handle2": garden.ContainerInfoEntry{ 692 Info: garden.ContainerInfo{ 693 State: "container2state", 694 }, 695 }, 696 } 697 698 handles := []string{"handle1", "handle2"} 699 queryParams := "handles=" + strings.Join(handles, "%2C") 700 701 Context("when the response is successful", func() { 702 JustBeforeEach(func() { 703 server.AppendHandlers( 704 ghttp.CombineHandlers( 705 ghttp.VerifyRequest("GET", "/containers/bulk_info", queryParams), 706 ghttp.RespondWith(200, marshalProto(expectedBulkInfo)))) 707 }) 708 709 It("returns info about containers", func() { 710 bulkInfo, err := connection.BulkInfo(handles) 711 Expect(err).ToNot(HaveOccurred()) 712 Expect(bulkInfo).To(Equal(expectedBulkInfo)) 713 }) 714 }) 715 716 Context("when the request fails", func() { 717 JustBeforeEach(func() { 718 server.AppendHandlers( 719 ghttp.CombineHandlers( 720 ghttp.VerifyRequest("GET", "/containers/bulk_info", queryParams), 721 ghttp.RespondWith(500, ""), 722 ), 723 ) 724 }) 725 726 It("returns the error", func() { 727 _, err := connection.BulkInfo(handles) 728 Expect(err).To(HaveOccurred()) 729 }) 730 }) 731 732 Context("when a container is in error state", func() { 733 It("returns the error for the container", func() { 734 735 expectedBulkInfo := map[string]garden.ContainerInfoEntry{ 736 "error": garden.ContainerInfoEntry{ 737 Err: &garden.Error{Err: errors.New("Oopps")}, 738 }, 739 "success": garden.ContainerInfoEntry{ 740 Info: garden.ContainerInfo{ 741 State: "container2state", 742 }, 743 }, 744 } 745 746 server.AppendHandlers( 747 ghttp.CombineHandlers( 748 ghttp.VerifyRequest("GET", "/containers/bulk_info", queryParams), 749 ghttp.RespondWith(200, marshalProto(expectedBulkInfo)))) 750 751 bulkInfo, err := connection.BulkInfo(handles) 752 Expect(err).ToNot(HaveOccurred()) 753 Expect(bulkInfo).To(Equal(expectedBulkInfo)) 754 }) 755 }) 756 }) 757 758 Describe("BulkMetrics", func() { 759 760 expectedBulkMetrics := map[string]garden.ContainerMetricsEntry{ 761 "handle1": garden.ContainerMetricsEntry{ 762 Metrics: garden.Metrics{ 763 DiskStat: garden.ContainerDiskStat{ 764 TotalInodesUsed: 4, 765 TotalBytesUsed: 3, 766 ExclusiveBytesUsed: 2, 767 ExclusiveInodesUsed: 1, 768 }, 769 }, 770 }, 771 "handle2": garden.ContainerMetricsEntry{ 772 Metrics: garden.Metrics{ 773 DiskStat: garden.ContainerDiskStat{ 774 TotalInodesUsed: 5, 775 TotalBytesUsed: 6, 776 ExclusiveBytesUsed: 7, 777 ExclusiveInodesUsed: 8, 778 }, 779 }, 780 }, 781 } 782 783 handles := []string{"handle1", "handle2"} 784 queryParams := "handles=" + strings.Join(handles, "%2C") 785 786 Context("when the response is successful", func() { 787 JustBeforeEach(func() { 788 server.AppendHandlers( 789 ghttp.CombineHandlers( 790 ghttp.VerifyRequest("GET", "/containers/bulk_metrics", queryParams), 791 ghttp.RespondWith(200, marshalProto(expectedBulkMetrics)))) 792 }) 793 794 It("returns info about containers", func() { 795 bulkMetrics, err := connection.BulkMetrics(handles) 796 Expect(err).ToNot(HaveOccurred()) 797 Expect(bulkMetrics).To(Equal(expectedBulkMetrics)) 798 }) 799 }) 800 801 Context("when the request fails", func() { 802 JustBeforeEach(func() { 803 server.AppendHandlers( 804 ghttp.CombineHandlers( 805 ghttp.VerifyRequest("GET", "/containers/bulk_metrics", queryParams), 806 ghttp.RespondWith(500, ""), 807 ), 808 ) 809 }) 810 811 It("returns the error", func() { 812 _, err := connection.BulkMetrics(handles) 813 Expect(err).To(HaveOccurred()) 814 }) 815 }) 816 817 Context("when a container has an error", func() { 818 It("returns the error for the container", func() { 819 820 errorBulkMetrics := map[string]garden.ContainerMetricsEntry{ 821 "error": garden.ContainerMetricsEntry{ 822 Err: &garden.Error{Err: errors.New("Oh noes!")}, 823 }, 824 "success": garden.ContainerMetricsEntry{ 825 Metrics: garden.Metrics{ 826 DiskStat: garden.ContainerDiskStat{ 827 TotalInodesUsed: 1, 828 }, 829 }, 830 }, 831 } 832 833 server.AppendHandlers( 834 ghttp.CombineHandlers( 835 ghttp.VerifyRequest("GET", "/containers/bulk_metrics", queryParams), 836 ghttp.RespondWith(200, marshalProto(errorBulkMetrics)))) 837 838 bulkMetrics, err := connection.BulkMetrics(handles) 839 Expect(err).ToNot(HaveOccurred()) 840 Expect(bulkMetrics).To(Equal(errorBulkMetrics)) 841 }) 842 }) 843 }) 844 845 Describe("Streaming in", func() { 846 Context("when streaming in succeeds", func() { 847 BeforeEach(func() { 848 server.AppendHandlers( 849 ghttp.CombineHandlers( 850 ghttp.VerifyRequest("PUT", "/containers/foo-handle/files", "user=alice&destination=%2Fbar"), 851 func(w http.ResponseWriter, r *http.Request) { 852 body, err := ioutil.ReadAll(r.Body) 853 Expect(err).ToNot(HaveOccurred()) 854 855 Expect(string(body)).To(Equal("chunk-1chunk-2")) 856 }, 857 ), 858 ) 859 }) 860 861 It("tells garden.to stream, and then streams the content as a series of chunks", func() { 862 buffer := bytes.NewBufferString("chunk-1chunk-2") 863 864 err := connection.StreamIn("foo-handle", garden.StreamInSpec{User: "alice", Path: "/bar", TarStream: buffer}) 865 Expect(err).ToNot(HaveOccurred()) 866 867 Expect(server.ReceivedRequests()).To(HaveLen(1)) 868 }) 869 }) 870 871 Context("when streaming in returns an error response", func() { 872 BeforeEach(func() { 873 server.AppendHandlers( 874 ghttp.CombineHandlers( 875 ghttp.VerifyRequest("PUT", "/containers/foo-handle/files", "user=bob&destination=%2Fbar"), 876 ghttp.RespondWith(http.StatusInternalServerError, "no."), 877 ), 878 ) 879 }) 880 881 It("returns an error on close", func() { 882 buffer := bytes.NewBufferString("chunk-1chunk-2") 883 err := connection.StreamIn("foo-handle", garden.StreamInSpec{User: "bob", Path: "/bar", TarStream: buffer}) 884 Expect(err).To(HaveOccurred()) 885 886 Expect(server.ReceivedRequests()).To(HaveLen(1)) 887 }) 888 }) 889 890 Context("when streaming in fails hard", func() { 891 BeforeEach(func() { 892 server.AppendHandlers( 893 ghttp.CombineHandlers( 894 ghttp.VerifyRequest("PUT", "/containers/foo-handle/files", "user=bob&destination=%2Fbar"), 895 ghttp.RespondWith(http.StatusInternalServerError, "no."), 896 func(w http.ResponseWriter, r *http.Request) { 897 server.CloseClientConnections() 898 }, 899 ), 900 ) 901 }) 902 903 It("returns an error on close", func() { 904 buffer := bytes.NewBufferString("chunk-1chunk-2") 905 906 err := connection.StreamIn("foo-handle", garden.StreamInSpec{User: "bob", Path: "/bar", TarStream: buffer}) 907 Expect(err).To(HaveOccurred()) 908 909 Expect(server.ReceivedRequests()).To(HaveLen(1)) 910 }) 911 }) 912 }) 913 914 Describe("Streaming Out", func() { 915 Context("when streaming succeeds", func() { 916 BeforeEach(func() { 917 server.AppendHandlers( 918 ghttp.CombineHandlers( 919 ghttp.VerifyRequest("GET", "/containers/foo-handle/files", "user=frank&source=%2Fbar"), 920 ghttp.RespondWith(200, "hello-world!"), 921 ), 922 ) 923 }) 924 925 It("asks garden.for the given file, then reads its content", func() { 926 reader, err := connection.StreamOut("foo-handle", garden.StreamOutSpec{User: "frank", Path: "/bar"}) 927 Expect(err).ToNot(HaveOccurred()) 928 929 readBytes, err := ioutil.ReadAll(reader) 930 Expect(err).ToNot(HaveOccurred()) 931 Expect(readBytes).To(Equal([]byte("hello-world!"))) 932 933 reader.Close() 934 }) 935 }) 936 937 Context("when streaming fails", func() { 938 BeforeEach(func() { 939 server.AppendHandlers( 940 ghttp.CombineHandlers( 941 ghttp.VerifyRequest("GET", "/containers/foo-handle/files", "user=deandra&source=%2Fbar"), 942 func(w http.ResponseWriter, r *http.Request) { 943 w.Header().Set("Content-Length", "500") 944 }, 945 ), 946 ) 947 }) 948 949 It("asks garden.for the given file, then reads its content", func() { 950 reader, err := connection.StreamOut("foo-handle", garden.StreamOutSpec{User: "deandra", Path: "/bar"}) 951 Expect(err).ToNot(HaveOccurred()) 952 953 _, err = ioutil.ReadAll(reader) 954 reader.Close() 955 Expect(err).To(HaveOccurred()) 956 }) 957 }) 958 }) 959 960 Describe("Running", func() { 961 var ( 962 spec garden.ProcessSpec 963 stdInContent chan string 964 ) 965 966 Context("when streaming succeeds to completion", func() { 967 BeforeEach(func() { 968 spec = garden.ProcessSpec{ 969 Path: "lol", 970 Args: []string{"arg1", "arg2"}, 971 Dir: "/some/dir", 972 User: "root", 973 Limits: resourceLimits, 974 } 975 stdInContent = make(chan string) 976 977 server.AppendHandlers( 978 ghttp.CombineHandlers( 979 ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"), 980 ghttp.VerifyJSONRepresenting(spec), 981 func(w http.ResponseWriter, r *http.Request) { 982 w.WriteHeader(http.StatusOK) 983 984 conn, br, err := w.(http.Hijacker).Hijack() 985 Expect(err).ToNot(HaveOccurred()) 986 987 defer conn.Close() 988 989 decoder := json.NewDecoder(br) 990 991 transport.WriteMessage(conn, map[string]interface{}{ 992 "process_id": "process-handle", 993 "stream_id": "123", 994 }) 995 996 var payload map[string]interface{} 997 err = decoder.Decode(&payload) 998 Expect(err).ToNot(HaveOccurred()) 999 1000 Expect(payload).To(Equal(map[string]interface{}{ 1001 "process_id": "process-handle", 1002 "source": float64(transport.Stdin), 1003 "data": "stdin data", 1004 })) 1005 stdInContent <- payload["data"].(string) 1006 1007 transport.WriteMessage(conn, map[string]interface{}{ 1008 "process_id": "process-handle", 1009 "exit_status": 3, 1010 }) 1011 }, 1012 ), 1013 stdoutStream("foo-handle", "process-handle", 123, func(conn net.Conn) { 1014 conn.Write([]byte("stdout data")) 1015 conn.Write([]byte(fmt.Sprintf("roundtripped %s", <-stdInContent))) 1016 }), 1017 stderrStream("foo-handle", "process-handle", 123, func(conn net.Conn) { 1018 conn.Write([]byte("stderr data")) 1019 }), 1020 ) 1021 }) 1022 1023 It("streams the data, closes the destinations, and notifies of exit", func() { 1024 stdout := gbytes.NewBuffer() 1025 stderr := gbytes.NewBuffer() 1026 1027 process, err := connection.Run(context.TODO(), "foo-handle", spec, garden.ProcessIO{ 1028 Stdin: bytes.NewBufferString("stdin data"), 1029 Stdout: stdout, 1030 Stderr: stderr, 1031 }) 1032 1033 Expect(err).ToNot(HaveOccurred()) 1034 Expect(process.ID()).To(Equal("process-handle")) 1035 1036 Eventually(stdout).Should(gbytes.Say("stdout data")) 1037 Eventually(stdout).Should(gbytes.Say("roundtripped stdin data")) 1038 Eventually(stderr).Should(gbytes.Say("stderr data")) 1039 1040 status, err := process.Wait() 1041 Expect(err).ToNot(HaveOccurred()) 1042 Expect(status).To(Equal(3)) 1043 }) 1044 1045 It("finishes streaming stdout and stderr before returning from .Wait", func() { 1046 stdout := gbytes.NewBuffer() 1047 stderr := gbytes.NewBuffer() 1048 1049 process, err := connection.Run(context.TODO(), "foo-handle", spec, garden.ProcessIO{ 1050 Stdin: bytes.NewBufferString("stdin data"), 1051 Stdout: stdout, 1052 Stderr: stderr, 1053 }) 1054 Expect(err).ToNot(HaveOccurred()) 1055 1056 process.Wait() 1057 Expect(stdout).To(gbytes.Say("roundtripped stdin data")) 1058 Expect(stderr).To(gbytes.Say("stderr data")) 1059 }) 1060 1061 Describe("connection leak avoidance", func() { 1062 var fakeHijacker *connectionfakes.FakeHijackStreamer 1063 var wrappedConnections []*wrappedConnection 1064 1065 BeforeEach(func() { 1066 wrappedConnections = []*wrappedConnection{} 1067 netHijacker := hijacker 1068 fakeHijacker = new(connectionfakes.FakeHijackStreamer) 1069 fakeHijacker.HijackStub = func(ctx context.Context, handler string, body io.Reader, params rata.Params, query url.Values, contentType string) (net.Conn, *bufio.Reader, error) { 1070 conn, resp, err := netHijacker.Hijack(ctx, handler, body, params, query, contentType) 1071 wc := &wrappedConnection{Conn: conn} 1072 wrappedConnections = append(wrappedConnections, wc) 1073 return wc, resp, err 1074 } 1075 1076 hijacker = fakeHijacker 1077 }) 1078 1079 It("should not leak net.Conn from Run", func() { 1080 stdout := gbytes.NewBuffer() 1081 stderr := gbytes.NewBuffer() 1082 1083 process, err := connection.Run(context.TODO(), "foo-handle", spec, garden.ProcessIO{ 1084 Stdin: bytes.NewBufferString("stdin data"), 1085 Stdout: stdout, 1086 Stderr: stderr, 1087 }) 1088 Expect(err).ToNot(HaveOccurred()) 1089 1090 process.Wait() 1091 Expect(stdout).To(gbytes.Say("roundtripped stdin data")) 1092 Expect(stderr).To(gbytes.Say("stderr data")) 1093 1094 for _, wc := range wrappedConnections { 1095 Eventually(wc.isClosed).Should(BeTrue()) 1096 } 1097 }) 1098 }) 1099 }) 1100 1101 Context("when the process is terminated", func() { 1102 BeforeEach(func() { 1103 server.AppendHandlers( 1104 ghttp.CombineHandlers( 1105 ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"), 1106 func(w http.ResponseWriter, r *http.Request) { 1107 w.WriteHeader(http.StatusOK) 1108 1109 conn, br, err := w.(http.Hijacker).Hijack() 1110 Expect(err).ToNot(HaveOccurred()) 1111 1112 defer conn.Close() 1113 1114 decoder := json.NewDecoder(br) 1115 1116 transport.WriteMessage(conn, map[string]interface{}{ 1117 "process_id": "process-handle", 1118 "stream_id": "123", 1119 }) 1120 1121 var payload map[string]interface{} 1122 err = decoder.Decode(&payload) 1123 Expect(err).ToNot(HaveOccurred()) 1124 1125 Expect(payload).To(Equal(map[string]interface{}{ 1126 "process_id": "process-handle", 1127 "signal": float64(garden.SignalTerminate), 1128 })) 1129 1130 transport.WriteMessage(conn, map[string]interface{}{ 1131 "process_id": "process-handle", 1132 "exit_status": 3, 1133 }) 1134 }, 1135 ), 1136 stdoutStream("foo-handle", "process-handle", 123, func(conn net.Conn) { 1137 conn.Write([]byte("stdout data")) 1138 conn.Write([]byte(fmt.Sprintf("roundtripped %s", <-stdInContent))) 1139 }), 1140 emptyStderrStream("foo-handle", "process-handle", 123), 1141 ) 1142 }) 1143 1144 It("sends the appropriate protocol message", func() { 1145 process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{}, garden.ProcessIO{}) 1146 1147 Expect(err).ToNot(HaveOccurred()) 1148 Expect(process.ID()).To(Equal("process-handle")) 1149 1150 err = process.Signal(garden.SignalTerminate) 1151 Expect(err).ToNot(HaveOccurred()) 1152 1153 status, err := process.Wait() 1154 Expect(err).ToNot(HaveOccurred()) 1155 Expect(status).To(Equal(3)) 1156 }) 1157 }) 1158 1159 Context("when the process is killed", func() { 1160 BeforeEach(func() { 1161 server.AppendHandlers( 1162 ghttp.CombineHandlers( 1163 ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"), 1164 func(w http.ResponseWriter, r *http.Request) { 1165 w.WriteHeader(http.StatusOK) 1166 1167 conn, br, err := w.(http.Hijacker).Hijack() 1168 Expect(err).ToNot(HaveOccurred()) 1169 1170 defer conn.Close() 1171 1172 decoder := json.NewDecoder(br) 1173 1174 transport.WriteMessage(conn, map[string]interface{}{ 1175 "process_id": "process-handle", 1176 "stream_id": "123", 1177 }) 1178 1179 var payload map[string]interface{} 1180 err = decoder.Decode(&payload) 1181 Expect(err).ToNot(HaveOccurred()) 1182 1183 Expect(payload).To(Equal(map[string]interface{}{ 1184 "process_id": "process-handle", 1185 "signal": float64(garden.SignalKill), 1186 })) 1187 1188 transport.WriteMessage(conn, map[string]interface{}{ 1189 "process_id": "process-handle", 1190 "exit_status": 3, 1191 }) 1192 }, 1193 ), 1194 emptyStdoutStream("foo-handle", "process-handle", 123), 1195 emptyStderrStream("foo-handle", "process-handle", 123), 1196 ) 1197 }) 1198 1199 It("sends the appropriate protocol message", func() { 1200 process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{}, garden.ProcessIO{}) 1201 1202 Expect(err).ToNot(HaveOccurred()) 1203 Expect(process.ID()).To(Equal("process-handle")) 1204 1205 err = process.Signal(garden.SignalKill) 1206 Expect(err).ToNot(HaveOccurred()) 1207 1208 status, err := process.Wait() 1209 Expect(err).ToNot(HaveOccurred()) 1210 Expect(status).To(Equal(3)) 1211 }) 1212 }) 1213 1214 Context("when the process's window is resized", func() { 1215 var spec garden.ProcessSpec 1216 BeforeEach(func() { 1217 spec = garden.ProcessSpec{ 1218 Path: "lol", 1219 Args: []string{"arg1", "arg2"}, 1220 TTY: &garden.TTYSpec{ 1221 WindowSize: &garden.WindowSize{ 1222 Columns: 100, 1223 Rows: 200, 1224 }, 1225 }, 1226 } 1227 1228 server.AppendHandlers( 1229 ghttp.CombineHandlers( 1230 ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"), 1231 ghttp.VerifyJSONRepresenting(spec), 1232 func(w http.ResponseWriter, r *http.Request) { 1233 w.WriteHeader(http.StatusOK) 1234 1235 conn, br, err := w.(http.Hijacker).Hijack() 1236 Expect(err).ToNot(HaveOccurred()) 1237 1238 defer conn.Close() 1239 1240 decoder := json.NewDecoder(br) 1241 1242 transport.WriteMessage(conn, map[string]interface{}{ 1243 "process_id": "process-handle", 1244 "stream_id": "123", 1245 }) 1246 1247 // the stdin data may come in before or after the tty message 1248 Eventually(func() interface{} { 1249 var payload map[string]interface{} 1250 err = decoder.Decode(&payload) 1251 Expect(err).ToNot(HaveOccurred()) 1252 1253 return payload 1254 }).Should(Equal(map[string]interface{}{ 1255 "process_id": "process-handle", 1256 "tty": map[string]interface{}{ 1257 "window_size": map[string]interface{}{ 1258 "columns": float64(80), 1259 "rows": float64(24), 1260 }, 1261 }, 1262 })) 1263 1264 transport.WriteMessage(conn, map[string]interface{}{ 1265 "process_id": "process-handle", 1266 "exit_status": 3, 1267 }) 1268 }, 1269 ), 1270 emptyStdoutStream("foo-handle", "process-handle", 123), 1271 emptyStderrStream("foo-handle", "process-handle", 123), 1272 ) 1273 }) 1274 1275 It("sends the appropriate protocol message", func() { 1276 process, err := connection.Run(context.TODO(), "foo-handle", spec, garden.ProcessIO{ 1277 Stdin: bytes.NewBufferString("stdin data"), 1278 Stdout: gbytes.NewBuffer(), 1279 Stderr: gbytes.NewBuffer(), 1280 }) 1281 1282 Expect(err).ToNot(HaveOccurred()) 1283 Expect(process.ID()).To(Equal("process-handle")) 1284 1285 err = process.SetTTY(garden.TTYSpec{ 1286 WindowSize: &garden.WindowSize{ 1287 Columns: 80, 1288 Rows: 24, 1289 }, 1290 }) 1291 Expect(err).ToNot(HaveOccurred()) 1292 1293 status, err := process.Wait() 1294 Expect(err).ToNot(HaveOccurred()) 1295 Expect(status).To(Equal(3)) 1296 }) 1297 }) 1298 1299 Context("when the connection breaks while attaching to the streams", func() { 1300 BeforeEach(func() { 1301 server.AppendHandlers( 1302 ghttp.CombineHandlers( 1303 ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"), 1304 func(w http.ResponseWriter, r *http.Request) { 1305 w.WriteHeader(http.StatusOK) 1306 1307 conn, _, err := w.(http.Hijacker).Hijack() 1308 Expect(err).ToNot(HaveOccurred()) 1309 1310 defer conn.Close() 1311 1312 transport.WriteMessage(conn, map[string]interface{}{ 1313 "process_id": "process-handle", 1314 "stream_id": "123", 1315 }) 1316 }, 1317 ), 1318 ghttp.CombineHandlers( 1319 ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle/attaches/123/stdout"), 1320 1321 func(w http.ResponseWriter, r *http.Request) { 1322 w.WriteHeader(http.StatusInternalServerError) 1323 1324 conn, _, err := w.(http.Hijacker).Hijack() 1325 Expect(err).ToNot(HaveOccurred()) 1326 defer conn.Close() 1327 }, 1328 ), 1329 ) 1330 }) 1331 1332 Describe("waiting on the process", func() { 1333 It("returns an error", func(done Done) { 1334 process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{ 1335 Path: "lol", 1336 Args: []string{"arg1", "arg2"}, 1337 Dir: "/some/dir", 1338 }, garden.ProcessIO{Stdout: GinkgoWriter}) 1339 1340 Expect(err).ToNot(HaveOccurred()) 1341 1342 _, err = process.Wait() 1343 Expect(err).To(MatchError(ContainSubstring("connection: failed to hijack stream "))) 1344 1345 close(done) 1346 }) 1347 }) 1348 }) 1349 1350 Context("when the connection breaks before an exit status is received", func() { 1351 BeforeEach(func() { 1352 server.AppendHandlers( 1353 ghttp.CombineHandlers( 1354 ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"), 1355 func(w http.ResponseWriter, r *http.Request) { 1356 w.WriteHeader(http.StatusOK) 1357 1358 conn, _, err := w.(http.Hijacker).Hijack() 1359 Expect(err).ToNot(HaveOccurred()) 1360 1361 defer conn.Close() 1362 1363 transport.WriteMessage(conn, map[string]interface{}{ 1364 "process_id": "process-handle", 1365 "stream_id": "123", 1366 }) 1367 }, 1368 ), 1369 emptyStdoutStream("foo-handle", "process-handle", 123), 1370 emptyStderrStream("foo-handle", "process-handle", 123), 1371 ) 1372 }) 1373 1374 Describe("waiting on the process", func() { 1375 It("returns an error", func() { 1376 process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{ 1377 Path: "lol", 1378 Args: []string{"arg1", "arg2"}, 1379 Dir: "/some/dir", 1380 }, garden.ProcessIO{}) 1381 1382 Expect(err).ToNot(HaveOccurred()) 1383 1384 _, err = process.Wait() 1385 Expect(err).To(HaveOccurred()) 1386 }) 1387 }) 1388 }) 1389 1390 Context("when the connection returns an error payload", func() { 1391 BeforeEach(func() { 1392 server.AppendHandlers( 1393 ghttp.CombineHandlers( 1394 ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"), 1395 ghttp.RespondWith(200, marshalProto(map[string]interface{}{ 1396 "process_id": "process-handle", 1397 "stream_id": "123", 1398 }, 1399 map[string]interface{}{ 1400 "process_id": "process-handle", 1401 "source": transport.Stderr, 1402 "data": "stderr data", 1403 }, 1404 map[string]interface{}{ 1405 "process_id": "process-handle", 1406 "error": "oh no!", 1407 }, 1408 )), 1409 ), 1410 emptyStdoutStream("foo-handle", "process-handle", 123), 1411 emptyStderrStream("foo-handle", "process-handle", 123), 1412 ) 1413 }) 1414 1415 Describe("waiting on the process", func() { 1416 It("returns an error", func() { 1417 process, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{ 1418 Path: "lol", 1419 Args: []string{"arg1", "arg2"}, 1420 Dir: "/some/dir", 1421 }, garden.ProcessIO{}) 1422 1423 Expect(err).ToNot(HaveOccurred()) 1424 1425 _, err = process.Wait() 1426 Expect(err).To(HaveOccurred()) 1427 Expect(err.Error()).To(ContainSubstring("oh no!")) 1428 }) 1429 }) 1430 }) 1431 1432 Context("when the connection returns an error status", func() { 1433 BeforeEach(func() { 1434 server.AppendHandlers(ghttp.CombineHandlers( 1435 ghttp.VerifyRequest("POST", "/containers/foo-handle/processes"), 1436 ghttp.RespondWith(500, "an error occurred!"), 1437 )) 1438 }) 1439 1440 It("returns an error", func() { 1441 _, err := connection.Run(context.TODO(), "foo-handle", garden.ProcessSpec{ 1442 Path: "lol", 1443 Args: []string{"arg1", "arg2"}, 1444 Dir: "/some/dir", 1445 }, garden.ProcessIO{}) 1446 1447 Expect(err).To(MatchError(ContainSubstring("an error occurred!"))) 1448 }) 1449 }) 1450 }) 1451 1452 Describe("Attaching", func() { 1453 Context("when streaming succeeds to completion", func() { 1454 BeforeEach(func() { 1455 expectedRoundtrip := make(chan string) 1456 server.AppendHandlers( 1457 ghttp.CombineHandlers( 1458 ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"), 1459 func(w http.ResponseWriter, r *http.Request) { 1460 w.WriteHeader(http.StatusOK) 1461 1462 conn, br, err := w.(http.Hijacker).Hijack() 1463 Expect(err).ToNot(HaveOccurred()) 1464 1465 defer conn.Close() 1466 1467 transport.WriteMessage(conn, map[string]interface{}{ 1468 "process_id": "process-handle", 1469 "stream_id": "123", 1470 }) 1471 1472 var payload map[string]interface{} 1473 err = json.NewDecoder(br).Decode(&payload) 1474 Expect(err).ToNot(HaveOccurred()) 1475 1476 Expect(payload).To(Equal(map[string]interface{}{ 1477 "process_id": "process-handle", 1478 "source": float64(transport.Stdin), 1479 "data": "stdin data", 1480 })) 1481 expectedRoundtrip <- payload["data"].(string) 1482 1483 transport.WriteMessage(conn, map[string]interface{}{ 1484 "process_id": "process-handle", 1485 "exit_status": 3, 1486 }) 1487 }, 1488 ), 1489 stdoutStream("foo-handle", "process-handle", 123, func(conn net.Conn) { 1490 conn.Write([]byte("stdout data")) 1491 conn.Write([]byte(fmt.Sprintf("roundtripped %s", <-expectedRoundtrip))) 1492 }), 1493 stderrStream("foo-handle", "process-handle", 123, func(conn net.Conn) { 1494 conn.Write([]byte("stderr data")) 1495 }), 1496 ) 1497 }) 1498 1499 It("should stream", func() { 1500 stdout := gbytes.NewBuffer() 1501 stderr := gbytes.NewBuffer() 1502 1503 process, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{ 1504 Stdin: bytes.NewBufferString("stdin data"), 1505 Stdout: stdout, 1506 Stderr: stderr, 1507 }) 1508 1509 Expect(err).ToNot(HaveOccurred()) 1510 Expect(process.ID()).To(Equal("process-handle")) 1511 1512 Eventually(stdout).Should(gbytes.Say("stdout data")) 1513 Eventually(stderr).Should(gbytes.Say("stderr data")) 1514 Eventually(stdout).Should(gbytes.Say("roundtripped stdin data")) 1515 1516 status, err := process.Wait() 1517 Expect(err).ToNot(HaveOccurred()) 1518 Expect(status).To(Equal(3)) 1519 }) 1520 1521 It("finishes streaming stdout and stderr before returning from .Wait", func() { 1522 stdout := gbytes.NewBuffer() 1523 stderr := gbytes.NewBuffer() 1524 1525 process, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{ 1526 Stdin: bytes.NewBufferString("stdin data"), 1527 Stdout: stdout, 1528 Stderr: stderr, 1529 }) 1530 1531 Expect(err).ToNot(HaveOccurred()) 1532 1533 process.Wait() 1534 Expect(stdout).To(gbytes.Say("roundtripped stdin data")) 1535 Expect(stderr).To(gbytes.Say("stderr data")) 1536 }) 1537 1538 }) 1539 1540 Context("when ctx is done", func() { 1541 1542 var fakeHijacker *connectionfakes.FakeHijackStreamer 1543 var wrappedConnections []*wrappedConnection 1544 var streamCancelFunc context.CancelFunc 1545 var streamContext context.Context 1546 1547 BeforeEach(func() { 1548 streamContext, streamCancelFunc = context.WithCancel(context.Background()) 1549 server.AppendHandlers( 1550 ghttp.CombineHandlers( 1551 ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"), 1552 func(w http.ResponseWriter, r *http.Request) { 1553 w.WriteHeader(http.StatusOK) 1554 1555 conn, _, err := w.(http.Hijacker).Hijack() 1556 Expect(err).ToNot(HaveOccurred()) 1557 1558 defer conn.Close() 1559 1560 transport.WriteMessage(conn, map[string]interface{}{ 1561 "process_id": "process-handle", 1562 "stream_id": "123", 1563 }) 1564 }, 1565 ), 1566 stdoutStream("foo-handle", "process-handle", 123, func(conn net.Conn) { 1567 <-streamContext.Done() 1568 conn.Write([]byte("stdout data")) 1569 }), 1570 emptyStderrStream("foo-handle", "process-handle", 123), 1571 ) 1572 wrappedConnections = []*wrappedConnection{} 1573 netHijacker := hijacker 1574 fakeHijacker = new(connectionfakes.FakeHijackStreamer) 1575 fakeHijacker.HijackStub = func(ctx context.Context, handler string, body io.Reader, params rata.Params, query url.Values, contentType string) (net.Conn, *bufio.Reader, error) { 1576 conn, resp, err := netHijacker.Hijack(ctx, handler, body, params, query, contentType) 1577 wc := &wrappedConnection{Conn: conn} 1578 wrappedConnections = append(wrappedConnections, wc) 1579 return wc, resp, err 1580 } 1581 1582 hijacker = fakeHijacker 1583 }) 1584 AfterEach(func() { 1585 streamCancelFunc() 1586 }) 1587 1588 It("should close all net.Conn from Attach and return from .Wait", func() { 1589 ctx, cancelFunc := context.WithCancel(context.Background()) 1590 process, err := connection.Attach(ctx, "foo-handle", "process-handle", garden.ProcessIO{ 1591 Stdin: bytes.NewBufferString("stdin data"), 1592 Stdout: gbytes.NewBuffer(), 1593 Stderr: gbytes.NewBuffer(), 1594 }) 1595 Expect(err).ToNot(HaveOccurred()) 1596 go func() { 1597 cancelFunc() 1598 }() 1599 process.Wait() 1600 1601 for _, wc := range wrappedConnections { 1602 Eventually(wc.isClosed).Should(BeTrue()) 1603 } 1604 }) 1605 }) 1606 1607 Context("when an error occurs while reading the given stdin stream", func() { 1608 It("does not send an EOF to close the process's stdin", func() { 1609 finishedReq := make(chan struct{}) 1610 1611 server.AppendHandlers( 1612 ghttp.CombineHandlers( 1613 ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"), 1614 func(w http.ResponseWriter, r *http.Request) { 1615 w.WriteHeader(http.StatusOK) 1616 1617 conn, br, err := w.(http.Hijacker).Hijack() 1618 Expect(err).ToNot(HaveOccurred()) 1619 defer conn.Close() 1620 1621 transport.WriteMessage(conn, map[string]interface{}{ 1622 "process_id": "process-handle", 1623 "stream_id": "123", 1624 }) 1625 1626 decoder := json.NewDecoder(br) 1627 1628 var payload map[string]interface{} 1629 err = decoder.Decode(&payload) 1630 Expect(err).ToNot(HaveOccurred()) 1631 1632 Expect(payload).To(Equal(map[string]interface{}{ 1633 "process_id": "process-handle", 1634 "source": float64(transport.Stdin), 1635 "data": "stdin data", 1636 })) 1637 1638 close(finishedReq) 1639 }, 1640 ), 1641 emptyStdoutStream("foo-handle", "process-handle", 123), 1642 emptyStderrStream("foo-handle", "process-handle", 123), 1643 ) 1644 1645 stdinR, stdinW := io.Pipe() 1646 1647 _, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{ 1648 Stdin: stdinR, 1649 }) 1650 Expect(err).ToNot(HaveOccurred()) 1651 1652 stdinW.Write([]byte("stdin data")) 1653 stdinW.CloseWithError(errors.New("connection broke")) 1654 1655 Eventually(finishedReq).Should(BeClosed()) 1656 }) 1657 }) 1658 1659 Context("when the connection returns an error payload", func() { 1660 BeforeEach(func() { 1661 server.AppendHandlers( 1662 ghttp.CombineHandlers( 1663 ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"), 1664 ghttp.RespondWith(200, marshalProto(map[string]interface{}{ 1665 "process_id": "process-handle", 1666 "stream_id": "123", 1667 }, 1668 map[string]interface{}{ 1669 "process_id": "process-handle", 1670 }, 1671 map[string]interface{}{ 1672 "process_id": "process-handle", 1673 "source": transport.Stdout, 1674 "data": "stdout data", 1675 }, 1676 map[string]interface{}{ 1677 "process_id": "process-handle", 1678 "source": transport.Stderr, 1679 "data": "stderr data", 1680 }, 1681 map[string]interface{}{ 1682 "process_id": "process-handle", 1683 "error": "oh no!", 1684 }, 1685 )), 1686 ), 1687 emptyStdoutStream("foo-handle", "process-handle", 123), 1688 emptyStderrStream("foo-handle", "process-handle", 123), 1689 ) 1690 }) 1691 1692 Describe("waiting on the process", func() { 1693 It("returns an error", func() { 1694 process, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{}) 1695 1696 Expect(err).ToNot(HaveOccurred()) 1697 Expect(process.ID()).To(Equal("process-handle")) 1698 1699 _, err = process.Wait() 1700 Expect(err).To(HaveOccurred()) 1701 Expect(err.Error()).To(ContainSubstring("oh no!")) 1702 }) 1703 }) 1704 }) 1705 1706 Context("when the connection breaks before an exit status is received", func() { 1707 BeforeEach(func() { 1708 server.AppendHandlers( 1709 ghttp.CombineHandlers( 1710 ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/process-handle"), 1711 func(w http.ResponseWriter, r *http.Request) { 1712 w.WriteHeader(http.StatusOK) 1713 1714 conn, _, err := w.(http.Hijacker).Hijack() 1715 Expect(err).ToNot(HaveOccurred()) 1716 1717 defer conn.Close() 1718 1719 transport.WriteMessage(conn, map[string]interface{}{ 1720 "process_id": "process-handle", 1721 "stream_id": "123", 1722 }) 1723 1724 transport.WriteMessage(conn, map[string]interface{}{ 1725 "process_id": "process-handle", 1726 "source": transport.Stdout, 1727 "data": "stdout data", 1728 }) 1729 1730 transport.WriteMessage(conn, map[string]interface{}{ 1731 "process_id": "process-handle", 1732 "source": transport.Stderr, 1733 "data": "stderr data", 1734 }) 1735 }, 1736 ), 1737 emptyStdoutStream("foo-handle", "process-handle", 123), 1738 emptyStderrStream("foo-handle", "process-handle", 123), 1739 ) 1740 }) 1741 1742 Describe("waiting on the process", func() { 1743 It("returns an error", func() { 1744 process, err := connection.Attach(context.TODO(), "foo-handle", "process-handle", garden.ProcessIO{}) 1745 1746 Expect(err).ToNot(HaveOccurred()) 1747 Expect(process.ID()).To(Equal("process-handle")) 1748 1749 _, err = process.Wait() 1750 Expect(err).To(HaveOccurred()) 1751 }) 1752 }) 1753 }) 1754 1755 Context("when the server returns HTTP 404", func() { 1756 BeforeEach(func() { 1757 gardenErr := garden.Error{Err: garden.ProcessNotFoundError{ProcessID: "idontexist"}} 1758 respBody, err := gardenErr.MarshalJSON() 1759 Expect(err).NotTo(HaveOccurred()) 1760 server.AppendHandlers( 1761 ghttp.CombineHandlers( 1762 ghttp.VerifyRequest("GET", "/containers/foo-handle/processes/idontexist"), 1763 ghttp.RespondWith(http.StatusNotFound, respBody), 1764 ), 1765 ) 1766 }) 1767 1768 It("returns a ProcessNotFoundError", func() { 1769 _, err := connection.Attach(context.TODO(), "foo-handle", "idontexist", garden.ProcessIO{}) 1770 Expect(err).To(MatchError(garden.ProcessNotFoundError{ 1771 ProcessID: "idontexist", 1772 })) 1773 }) 1774 }) 1775 }) 1776 }) 1777 1778 func verifyRequestBody(expectedMessage interface{}, emptyType interface{}) http.HandlerFunc { 1779 return func(resp http.ResponseWriter, req *http.Request) { 1780 defer GinkgoRecover() 1781 1782 decoder := json.NewDecoder(req.Body) 1783 1784 received := emptyType 1785 err := decoder.Decode(&received) 1786 Expect(err).ToNot(HaveOccurred()) 1787 1788 Expect(received).To(Equal(expectedMessage)) 1789 } 1790 } 1791 1792 func marshalProto(messages ...interface{}) string { 1793 result := new(bytes.Buffer) 1794 for _, msg := range messages { 1795 err := transport.WriteMessage(result, msg) 1796 Expect(err).ToNot(HaveOccurred()) 1797 } 1798 1799 return result.String() 1800 } 1801 1802 func emptyStdoutStream(handle, processid string, attachid int) http.HandlerFunc { 1803 return stdoutStream(handle, processid, attachid, func(net.Conn) {}) 1804 } 1805 1806 func emptyStderrStream(handle, processid string, attachid int) http.HandlerFunc { 1807 return stderrStream(handle, processid, attachid, func(net.Conn) {}) 1808 } 1809 1810 func stderrStream(handle, processid string, attachid int, fn func(net.Conn)) http.HandlerFunc { 1811 return stream(handle, "stderr", processid, attachid, fn) 1812 } 1813 1814 func stdoutStream(handle, processid string, attachid int, fn func(net.Conn)) http.HandlerFunc { 1815 return stream(handle, "stdout", processid, attachid, fn) 1816 } 1817 1818 func stream(handle, route, processid string, attachid int, fn func(net.Conn)) http.HandlerFunc { 1819 return ghttp.CombineHandlers( 1820 ghttp.VerifyRequest("GET", 1821 fmt.Sprintf("/containers/%s/processes/%s/attaches/%d/%s", 1822 handle, 1823 processid, 1824 attachid, 1825 route, 1826 )), 1827 1828 func(w http.ResponseWriter, r *http.Request) { 1829 w.WriteHeader(http.StatusOK) 1830 1831 conn, _, err := w.(http.Hijacker).Hijack() 1832 Expect(err).ToNot(HaveOccurred()) 1833 defer conn.Close() 1834 1835 fn(conn) 1836 }, 1837 ) 1838 }