github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/api/containers_test.go (about) 1 package api_test 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "net/url" 13 "strconv" 14 "time" 15 16 "code.cloudfoundry.org/garden" 17 gfakes "code.cloudfoundry.org/garden/gardenfakes" 18 "github.com/pf-qiu/concourse/v6/atc" 19 "github.com/pf-qiu/concourse/v6/atc/db" 20 "github.com/pf-qiu/concourse/v6/atc/db/dbfakes" 21 . "github.com/pf-qiu/concourse/v6/atc/testhelpers" 22 "github.com/pf-qiu/concourse/v6/atc/worker/workerfakes" 23 "github.com/gorilla/websocket" 24 . "github.com/onsi/ginkgo" 25 . "github.com/onsi/gomega" 26 ) 27 28 var _ = Describe("Containers API", func() { 29 var ( 30 stepType = db.ContainerTypeTask 31 stepName = "some-step" 32 pipelineID = 1111 33 jobID = 2222 34 buildID = 3333 35 workingDirectory = "/tmp/build/my-favorite-guid" 36 attempt = "1.5" 37 user = "snoopy" 38 39 req *http.Request 40 41 fakeContainer1 *dbfakes.FakeContainer 42 fakeContainer2 *dbfakes.FakeContainer 43 ) 44 45 BeforeEach(func() { 46 fakeContainer1 = new(dbfakes.FakeContainer) 47 fakeContainer1.HandleReturns("some-handle") 48 fakeContainer1.StateReturns("container-state") 49 fakeContainer1.WorkerNameReturns("some-worker-name") 50 fakeContainer1.MetadataReturns(db.ContainerMetadata{ 51 Type: stepType, 52 53 StepName: stepName, 54 Attempt: attempt, 55 56 PipelineID: pipelineID, 57 JobID: jobID, 58 BuildID: buildID, 59 60 WorkingDirectory: workingDirectory, 61 User: user, 62 }) 63 64 fakeContainer2 = new(dbfakes.FakeContainer) 65 fakeContainer2.HandleReturns("some-other-handle") 66 fakeContainer2.WorkerNameReturns("some-other-worker-name") 67 fakeContainer2.MetadataReturns(db.ContainerMetadata{ 68 Type: stepType, 69 70 StepName: stepName + "-other", 71 Attempt: attempt + ".1", 72 73 PipelineID: pipelineID + 1, 74 JobID: jobID + 1, 75 BuildID: buildID + 1, 76 77 WorkingDirectory: workingDirectory + "/other", 78 User: user + "-other", 79 }) 80 }) 81 82 Describe("GET /api/v1/teams/a-team/containers", func() { 83 BeforeEach(func() { 84 var err error 85 req, err = http.NewRequest("GET", server.URL+"/api/v1/teams/a-team/containers", nil) 86 Expect(err).NotTo(HaveOccurred()) 87 req.Header.Set("Content-Type", "application/json") 88 }) 89 90 Context("when not authenticated", func() { 91 BeforeEach(func() { 92 fakeAccess.IsAuthenticatedReturns(false) 93 }) 94 95 It("returns 401 Unauthorized", func() { 96 response, err := client.Do(req) 97 Expect(err).NotTo(HaveOccurred()) 98 99 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 100 }) 101 }) 102 103 Context("when authenticated", func() { 104 BeforeEach(func() { 105 fakeAccess.IsAuthenticatedReturns(true) 106 fakeAccess.IsAuthorizedReturns(true) 107 }) 108 109 Context("with no params", func() { 110 Context("when no errors are returned", func() { 111 BeforeEach(func() { 112 dbTeam.ContainersReturns([]db.Container{fakeContainer1, fakeContainer2}, nil) 113 }) 114 115 It("returns 200", func() { 116 response, err := client.Do(req) 117 Expect(err).NotTo(HaveOccurred()) 118 119 Expect(response.StatusCode).To(Equal(http.StatusOK)) 120 }) 121 122 It("returns Content-Type application/json", func() { 123 response, err := client.Do(req) 124 Expect(err).NotTo(HaveOccurred()) 125 126 expectedHeaderEntries := map[string]string{ 127 "Content-Type": "application/json", 128 } 129 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 130 }) 131 132 It("returns all containers", func() { 133 response, err := client.Do(req) 134 Expect(err).NotTo(HaveOccurred()) 135 136 body, err := ioutil.ReadAll(response.Body) 137 Expect(err).NotTo(HaveOccurred()) 138 139 Expect(body).To(MatchJSON(` 140 [ 141 { 142 "id": "some-handle", 143 "worker_name": "some-worker-name", 144 "type": "task", 145 "step_name": "some-step", 146 "attempt": "1.5", 147 "pipeline_id": 1111, 148 "job_id": 2222, 149 "state": "container-state", 150 "build_id": 3333, 151 "working_directory": "/tmp/build/my-favorite-guid", 152 "user": "snoopy" 153 }, 154 { 155 "id": "some-other-handle", 156 "worker_name": "some-other-worker-name", 157 "type": "task", 158 "step_name": "some-step-other", 159 "attempt": "1.5.1", 160 "pipeline_id": 1112, 161 "job_id": 2223, 162 "build_id": 3334, 163 "working_directory": "/tmp/build/my-favorite-guid/other", 164 "user": "snoopy-other" 165 } 166 ] 167 `)) 168 }) 169 }) 170 171 Context("when no containers are found", func() { 172 BeforeEach(func() { 173 dbTeam.ContainersReturns([]db.Container{}, nil) 174 }) 175 176 It("returns 200", func() { 177 response, err := client.Do(req) 178 Expect(err).NotTo(HaveOccurred()) 179 180 Expect(response.StatusCode).To(Equal(http.StatusOK)) 181 }) 182 183 It("returns an empty array", func() { 184 response, err := client.Do(req) 185 Expect(err).NotTo(HaveOccurred()) 186 187 body, err := ioutil.ReadAll(response.Body) 188 Expect(err).NotTo(HaveOccurred()) 189 190 Expect(body).To(MatchJSON(` 191 [] 192 `)) 193 }) 194 }) 195 196 Context("when there is an error", func() { 197 var ( 198 expectedErr error 199 ) 200 201 BeforeEach(func() { 202 expectedErr = errors.New("some error") 203 dbTeam.ContainersReturns(nil, expectedErr) 204 }) 205 206 It("returns 500", func() { 207 response, err := client.Do(req) 208 Expect(err).NotTo(HaveOccurred()) 209 210 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 211 }) 212 }) 213 }) 214 215 Describe("querying with pipeline id", func() { 216 BeforeEach(func() { 217 req.URL.RawQuery = url.Values{ 218 "pipeline_id": []string{strconv.Itoa(pipelineID)}, 219 }.Encode() 220 }) 221 222 It("queries with it in the metadata", func() { 223 _, err := client.Do(req) 224 Expect(err).NotTo(HaveOccurred()) 225 226 Expect(dbTeam.FindContainersByMetadataCallCount()).To(Equal(1)) 227 228 meta := dbTeam.FindContainersByMetadataArgsForCall(0) 229 Expect(meta).To(Equal(db.ContainerMetadata{ 230 PipelineID: pipelineID, 231 })) 232 }) 233 }) 234 235 Describe("querying with job id", func() { 236 BeforeEach(func() { 237 req.URL.RawQuery = url.Values{ 238 "job_id": []string{strconv.Itoa(jobID)}, 239 }.Encode() 240 }) 241 242 It("queries with it in the metadata", func() { 243 _, err := client.Do(req) 244 Expect(err).NotTo(HaveOccurred()) 245 246 Expect(dbTeam.FindContainersByMetadataCallCount()).To(Equal(1)) 247 248 meta := dbTeam.FindContainersByMetadataArgsForCall(0) 249 Expect(meta).To(Equal(db.ContainerMetadata{ 250 JobID: jobID, 251 })) 252 }) 253 }) 254 255 Describe("querying with type", func() { 256 BeforeEach(func() { 257 req.URL.RawQuery = url.Values{ 258 "type": []string{string(stepType)}, 259 }.Encode() 260 }) 261 262 It("queries with it in the metadata", func() { 263 _, err := client.Do(req) 264 Expect(err).NotTo(HaveOccurred()) 265 266 meta := dbTeam.FindContainersByMetadataArgsForCall(0) 267 Expect(meta).To(Equal(db.ContainerMetadata{ 268 Type: stepType, 269 })) 270 }) 271 }) 272 273 Describe("querying with step name", func() { 274 BeforeEach(func() { 275 req.URL.RawQuery = url.Values{ 276 "step_name": []string{stepName}, 277 }.Encode() 278 }) 279 280 It("queries with it in the metadata", func() { 281 _, err := client.Do(req) 282 Expect(err).NotTo(HaveOccurred()) 283 284 meta := dbTeam.FindContainersByMetadataArgsForCall(0) 285 Expect(meta).To(Equal(db.ContainerMetadata{ 286 StepName: stepName, 287 })) 288 }) 289 }) 290 291 Describe("querying with build id", func() { 292 Context("when the buildID can be parsed as an int", func() { 293 BeforeEach(func() { 294 buildIDString := strconv.Itoa(buildID) 295 296 req.URL.RawQuery = url.Values{ 297 "build_id": []string{buildIDString}, 298 }.Encode() 299 }) 300 301 It("queries with it in the metadata", func() { 302 _, err := client.Do(req) 303 Expect(err).NotTo(HaveOccurred()) 304 305 meta := dbTeam.FindContainersByMetadataArgsForCall(0) 306 Expect(meta).To(Equal(db.ContainerMetadata{ 307 BuildID: buildID, 308 })) 309 }) 310 311 Context("when the buildID fails to be parsed as an int", func() { 312 BeforeEach(func() { 313 req.URL.RawQuery = url.Values{ 314 "build_id": []string{"not-an-int"}, 315 }.Encode() 316 }) 317 318 It("returns 400 Bad Request", func() { 319 response, _ := client.Do(req) 320 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 321 }) 322 323 It("does not lookup containers", func() { 324 _, _ = client.Do(req) 325 Expect(dbTeam.FindContainersByMetadataCallCount()).To(Equal(0)) 326 }) 327 }) 328 }) 329 }) 330 331 Describe("querying with attempts", func() { 332 Context("when the attempts can be parsed as a slice of int", func() { 333 BeforeEach(func() { 334 req.URL.RawQuery = url.Values{ 335 "attempt": []string{attempt}, 336 }.Encode() 337 }) 338 339 It("queries with it in the metadata", func() { 340 _, err := client.Do(req) 341 Expect(err).NotTo(HaveOccurred()) 342 343 meta := dbTeam.FindContainersByMetadataArgsForCall(0) 344 Expect(meta).To(Equal(db.ContainerMetadata{ 345 Attempt: attempt, 346 })) 347 }) 348 }) 349 }) 350 351 Describe("querying with type 'check'", func() { 352 BeforeEach(func() { 353 rawInstanceVars, _ := json.Marshal(atc.InstanceVars{"branch": "master"}) 354 req.URL.RawQuery = url.Values{ 355 "type": []string{"check"}, 356 "resource_name": []string{"some-resource"}, 357 "pipeline_name": []string{"some-pipeline"}, 358 "instance_vars": []string{string(rawInstanceVars)}, 359 }.Encode() 360 }) 361 362 It("queries with check properties", func() { 363 _, err := client.Do(req) 364 Expect(err).NotTo(HaveOccurred()) 365 366 _, pipelineRef, resourceName, secretManager, varSourcePool := dbTeam.FindCheckContainersArgsForCall(0) 367 Expect(pipelineRef).To(Equal(atc.PipelineRef{Name: "some-pipeline", InstanceVars: atc.InstanceVars{"branch": "master"}})) 368 Expect(resourceName).To(Equal("some-resource")) 369 Expect(secretManager).To(Equal(fakeSecretManager)) 370 Expect(varSourcePool).To(Equal(fakeVarSourcePool)) 371 }) 372 }) 373 }) 374 }) 375 376 Describe("GET /api/v1/containers/:id", func() { 377 var handle = "some-handle" 378 379 BeforeEach(func() { 380 dbTeam.FindContainerByHandleReturns(fakeContainer1, true, nil) 381 382 var err error 383 req, err = http.NewRequest("GET", server.URL+"/api/v1/teams/a-team/containers/"+handle, nil) 384 Expect(err).NotTo(HaveOccurred()) 385 req.Header.Set("Content-Type", "application/json") 386 }) 387 388 Context("when not authenticated", func() { 389 BeforeEach(func() { 390 fakeAccess.IsAuthenticatedReturns(false) 391 }) 392 393 It("returns 401 Unauthorized", func() { 394 response, err := client.Do(req) 395 Expect(err).NotTo(HaveOccurred()) 396 397 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 398 }) 399 }) 400 401 Context("when authenticated", func() { 402 BeforeEach(func() { 403 fakeAccess.IsAuthenticatedReturns(true) 404 fakeAccess.IsAuthorizedReturns(true) 405 }) 406 407 Context("when the container is not found", func() { 408 BeforeEach(func() { 409 dbTeam.FindContainerByHandleReturns(nil, false, nil) 410 }) 411 412 It("returns 404 Not Found", func() { 413 response, err := client.Do(req) 414 Expect(err).NotTo(HaveOccurred()) 415 416 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 417 }) 418 }) 419 420 Context("when the container is found", func() { 421 BeforeEach(func() { 422 dbTeam.FindContainerByHandleReturns(fakeContainer1, true, nil) 423 }) 424 425 Context("when the container is within the team", func() { 426 BeforeEach(func() { 427 dbTeam.IsCheckContainerReturns(false, nil) 428 dbTeam.IsContainerWithinTeamReturns(true, nil) 429 }) 430 431 It("returns 200 OK", func() { 432 response, err := client.Do(req) 433 Expect(err).NotTo(HaveOccurred()) 434 435 Expect(response.StatusCode).To(Equal(http.StatusOK)) 436 }) 437 438 It("returns Content-Type application/json", func() { 439 response, err := client.Do(req) 440 Expect(err).NotTo(HaveOccurred()) 441 442 expectedHeaderEntries := map[string]string{ 443 "Content-Type": "application/json", 444 } 445 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 446 }) 447 448 It("performs lookup by id", func() { 449 _, err := client.Do(req) 450 Expect(err).NotTo(HaveOccurred()) 451 452 Expect(dbTeam.FindContainerByHandleCallCount()).To(Equal(1)) 453 Expect(dbTeam.FindContainerByHandleArgsForCall(0)).To(Equal(handle)) 454 }) 455 456 It("returns the container", func() { 457 response, err := client.Do(req) 458 Expect(err).NotTo(HaveOccurred()) 459 460 body, err := ioutil.ReadAll(response.Body) 461 Expect(err).NotTo(HaveOccurred()) 462 463 Expect(body).To(MatchJSON(` 464 { 465 "id": "some-handle", 466 "state": "container-state", 467 "worker_name": "some-worker-name", 468 "type": "task", 469 "step_name": "some-step", 470 "attempt": "1.5", 471 "pipeline_id": 1111, 472 "job_id": 2222, 473 "build_id": 3333, 474 "working_directory": "/tmp/build/my-favorite-guid", 475 "user": "snoopy" 476 } 477 `)) 478 }) 479 }) 480 481 Context("when the container is not within the team", func() { 482 BeforeEach(func() { 483 dbTeam.IsCheckContainerReturns(false, nil) 484 dbTeam.IsContainerWithinTeamReturns(false, nil) 485 }) 486 487 It("returns 404 Not Found", func() { 488 response, err := client.Do(req) 489 Expect(err).NotTo(HaveOccurred()) 490 491 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 492 }) 493 }) 494 }) 495 496 Context("when there is an error", func() { 497 var ( 498 expectedErr error 499 ) 500 501 BeforeEach(func() { 502 expectedErr = errors.New("some error") 503 dbTeam.FindContainerByHandleReturns(nil, false, expectedErr) 504 }) 505 506 It("returns 500", func() { 507 response, err := client.Do(req) 508 Expect(err).NotTo(HaveOccurred()) 509 510 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 511 }) 512 }) 513 }) 514 }) 515 516 Describe("GET /api/v1/containers/:id/hijack", func() { 517 var ( 518 handle = "some-handle" 519 520 requestPayload string 521 522 conn *websocket.Conn 523 response *http.Response 524 525 expectBadHandshake bool 526 ) 527 528 BeforeEach(func() { 529 expectBadHandshake = false 530 requestPayload = `{"path":"ls", "user": "snoopy"}` 531 }) 532 533 JustBeforeEach(func() { 534 wsURL, err := url.Parse(server.URL) 535 Expect(err).NotTo(HaveOccurred()) 536 537 wsURL.Scheme = "ws" 538 wsURL.Path = "/api/v1/teams/a-team/containers/" + handle + "/hijack" 539 540 dialer := websocket.Dialer{} 541 conn, response, err = dialer.Dial(wsURL.String(), nil) 542 if !expectBadHandshake { 543 Expect(err).NotTo(HaveOccurred()) 544 545 writer, err := conn.NextWriter(websocket.TextMessage) 546 Expect(err).NotTo(HaveOccurred()) 547 548 _, err = writer.Write([]byte(requestPayload)) 549 Expect(err).NotTo(HaveOccurred()) 550 551 err = writer.Close() 552 Expect(err).NotTo(HaveOccurred()) 553 } 554 }) 555 556 AfterEach(func() { 557 if !expectBadHandshake { 558 _ = conn.Close() 559 } 560 }) 561 562 Context("when authenticated", func() { 563 BeforeEach(func() { 564 fakeAccess.IsAuthenticatedReturns(true) 565 fakeAccess.IsAuthorizedReturns(true) 566 }) 567 568 Context("and the worker client returns a container", func() { 569 var ( 570 fakeDBContainer *dbfakes.FakeCreatedContainer 571 fakeContainer *workerfakes.FakeContainer 572 ) 573 574 BeforeEach(func() { 575 fakeDBContainer = new(dbfakes.FakeCreatedContainer) 576 dbTeam.FindContainerByHandleReturns(fakeDBContainer, true, nil) 577 fakeDBContainer.HandleReturns("some-handle") 578 579 fakeContainer = new(workerfakes.FakeContainer) 580 fakeWorkerClient.FindContainerReturns(fakeContainer, true, nil) 581 }) 582 583 Context("when the container is a check container", func() { 584 BeforeEach(func() { 585 dbTeam.IsCheckContainerReturns(true, nil) 586 }) 587 588 Context("when the user is not admin", func() { 589 BeforeEach(func() { 590 expectBadHandshake = true 591 592 fakeAccess.IsAdminReturns(false) 593 }) 594 595 It("returns Forbidden", func() { 596 Expect(response.StatusCode).To(Equal(http.StatusForbidden)) 597 }) 598 }) 599 600 Context("when the user is an admin", func() { 601 BeforeEach(func() { 602 fakeAccess.IsAdminReturns(true) 603 }) 604 605 Context("when the container is not within the team", func() { 606 BeforeEach(func() { 607 expectBadHandshake = true 608 609 dbTeam.IsContainerWithinTeamReturns(false, nil) 610 }) 611 612 It("returns 404 not found", func() { 613 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 614 }) 615 }) 616 617 Context("when the container is within the team", func() { 618 var ( 619 fakeProcess *gfakes.FakeProcess 620 processExit chan int 621 ) 622 623 BeforeEach(func() { 624 dbTeam.IsContainerWithinTeamReturns(true, nil) 625 626 exit := make(chan int) 627 processExit = exit 628 629 fakeProcess = new(gfakes.FakeProcess) 630 fakeProcess.WaitStub = func() (int, error) { 631 return <-exit, nil 632 } 633 634 fakeContainer.RunReturns(fakeProcess, nil) 635 }) 636 637 AfterEach(func() { 638 close(processExit) 639 }) 640 641 It("should try to hijack the container", func() { 642 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 643 }) 644 }) 645 }) 646 }) 647 648 Context("when the container is a build step container", func() { 649 BeforeEach(func() { 650 dbTeam.IsCheckContainerReturns(false, nil) 651 }) 652 653 Context("when the container is not within the team", func() { 654 BeforeEach(func() { 655 expectBadHandshake = true 656 657 dbTeam.IsContainerWithinTeamReturns(false, nil) 658 }) 659 660 It("returns 404 not found", func() { 661 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 662 }) 663 }) 664 665 Context("when the container is within the team", func() { 666 BeforeEach(func() { 667 dbTeam.IsContainerWithinTeamReturns(true, nil) 668 }) 669 670 Context("when the call to lookup the container returns an error", func() { 671 BeforeEach(func() { 672 expectBadHandshake = true 673 674 fakeWorkerClient.FindContainerReturns(nil, false, errors.New("nope")) 675 }) 676 677 It("returns 500 internal error", func() { 678 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 679 }) 680 }) 681 682 Context("when the container could not be found on the worker client", func() { 683 BeforeEach(func() { 684 expectBadHandshake = true 685 686 fakeWorkerClient.FindContainerReturns(nil, false, nil) 687 }) 688 689 It("returns 404 Not Found", func() { 690 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 691 }) 692 }) 693 694 Context("when the request payload is invalid", func() { 695 BeforeEach(func() { 696 requestPayload = "ß" 697 }) 698 699 It("closes the connection with an error", func() { 700 _, _, err := conn.ReadMessage() 701 702 Expect(websocket.IsCloseError(err, 1003)).To(BeTrue()) // unsupported data 703 Expect(err).To(MatchError(ContainSubstring("malformed process spec"))) 704 }) 705 }) 706 707 Context("when running the process fails", func() { 708 var containerRunError = errors.New("container-run-error") 709 710 BeforeEach(func() { 711 fakeContainer.RunReturns(nil, containerRunError) 712 }) 713 714 It("receives the error in the output", func() { 715 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 716 717 expectedHijackOutput := atc.HijackOutput{ 718 Error: containerRunError.Error(), 719 } 720 721 var hijackOutput atc.HijackOutput 722 err := conn.ReadJSON(&hijackOutput) 723 Expect(err).ToNot(HaveOccurred()) 724 Expect(hijackOutput).To(Equal(expectedHijackOutput)) 725 }) 726 }) 727 728 Context("when running the process succeeds", func() { 729 var ( 730 fakeProcess *gfakes.FakeProcess 731 processExit chan int 732 ) 733 734 BeforeEach(func() { 735 exit := make(chan int) 736 processExit = exit 737 738 fakeProcess = new(gfakes.FakeProcess) 739 fakeProcess.WaitStub = func() (int, error) { 740 return <-exit, nil 741 } 742 743 fakeContainer.RunReturns(fakeProcess, nil) 744 }) 745 746 AfterEach(func() { 747 close(processExit) 748 }) 749 750 It("did not check if the user is admin", func() { 751 Expect(fakeAccess.IsAdminCallCount()).To(Equal(0)) 752 }) 753 754 It("hijacks the build", func() { 755 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 756 757 _, lookedUpTeamID, lookedUpHandle := fakeWorkerClient.FindContainerArgsForCall(0) 758 Expect(lookedUpTeamID).To(Equal(734)) 759 Expect(lookedUpHandle).To(Equal(handle)) 760 761 _, spec, io := fakeContainer.RunArgsForCall(0) 762 Expect(spec).To(Equal(garden.ProcessSpec{ 763 Path: "ls", 764 User: "snoopy", 765 })) 766 767 Expect(io.Stdin).NotTo(BeNil()) 768 Expect(io.Stdout).NotTo(BeNil()) 769 Expect(io.Stderr).NotTo(BeNil()) 770 }) 771 772 It("updates the last hijack value", func() { 773 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 774 775 Expect(fakeContainer.UpdateLastHijackCallCount()).To(Equal(1)) 776 }) 777 778 Context("when the hijack timer elapses", func() { 779 JustBeforeEach(func() { 780 fakeClock.WaitForWatcherAndIncrement(time.Second) 781 }) 782 783 It("updates the last hijack value again", func() { 784 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 785 786 Eventually(fakeContainer.UpdateLastHijackCallCount).Should(Equal(2)) 787 }) 788 }) 789 790 Context("when stdin is sent over the API", func() { 791 JustBeforeEach(func() { 792 err := conn.WriteJSON(atc.HijackInput{ 793 Stdin: []byte("some stdin\n"), 794 }) 795 Expect(err).NotTo(HaveOccurred()) 796 }) 797 798 It("forwards the payload to the process", func() { 799 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 800 801 _, _, io := fakeContainer.RunArgsForCall(0) 802 Expect(bufio.NewReader(io.Stdin).ReadBytes('\n')).To(Equal([]byte("some stdin\n"))) 803 804 Expect(interceptTimeout.ResetCallCount()).To(Equal(1)) 805 }) 806 }) 807 808 Context("when stdin is closed via the API", func() { 809 JustBeforeEach(func() { 810 err := conn.WriteJSON(atc.HijackInput{ 811 Closed: true, 812 }) 813 Expect(err).NotTo(HaveOccurred()) 814 }) 815 816 It("closes the process's stdin", func() { 817 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 818 819 _, _, ioConfig := fakeContainer.RunArgsForCall(0) 820 _, err := ioConfig.Stdin.Read(make([]byte, 10)) 821 Expect(err).To(Equal(io.EOF)) 822 }) 823 }) 824 825 Context("when the process prints to stdout", func() { 826 JustBeforeEach(func() { 827 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 828 829 _, _, io := fakeContainer.RunArgsForCall(0) 830 831 _, err := fmt.Fprintf(io.Stdout, "some stdout\n") 832 Expect(err).NotTo(HaveOccurred()) 833 }) 834 835 It("forwards it to the response", func() { 836 var hijackOutput atc.HijackOutput 837 err := conn.ReadJSON(&hijackOutput) 838 Expect(err).NotTo(HaveOccurred()) 839 840 Expect(hijackOutput).To(Equal(atc.HijackOutput{ 841 Stdout: []byte("some stdout\n"), 842 })) 843 }) 844 }) 845 846 Context("when the process prints to stderr", func() { 847 JustBeforeEach(func() { 848 Eventually(fakeContainer.RunCallCount).Should(Equal(1)) 849 850 _, _, io := fakeContainer.RunArgsForCall(0) 851 852 _, err := fmt.Fprintf(io.Stderr, "some stderr\n") 853 Expect(err).NotTo(HaveOccurred()) 854 }) 855 856 It("forwards it to the response", func() { 857 var hijackOutput atc.HijackOutput 858 err := conn.ReadJSON(&hijackOutput) 859 Expect(err).NotTo(HaveOccurred()) 860 861 Expect(hijackOutput).To(Equal(atc.HijackOutput{ 862 Stderr: []byte("some stderr\n"), 863 })) 864 }) 865 }) 866 867 Context("when the process exits", func() { 868 JustBeforeEach(func() { 869 Eventually(processExit).Should(BeSent(123)) 870 }) 871 872 It("forwards its exit status to the response", func() { 873 var hijackOutput atc.HijackOutput 874 err := conn.ReadJSON(&hijackOutput) 875 Expect(err).NotTo(HaveOccurred()) 876 877 exitStatus := 123 878 Expect(hijackOutput).To(Equal(atc.HijackOutput{ 879 ExitStatus: &exitStatus, 880 })) 881 }) 882 883 It("closes the process' stdin pipe", func() { 884 _, _, io := fakeContainer.RunArgsForCall(0) 885 886 c := make(chan bool, 1) 887 888 go func() { 889 var b []byte 890 _, err := io.Stdin.Read(b) 891 if err != nil { 892 c <- true 893 } 894 }() 895 896 Eventually(c, 2*time.Second).Should(Receive()) 897 }) 898 }) 899 900 Context("when new tty settings are sent over the API", func() { 901 JustBeforeEach(func() { 902 err := conn.WriteJSON(atc.HijackInput{ 903 TTYSpec: &atc.HijackTTYSpec{ 904 WindowSize: atc.HijackWindowSize{ 905 Columns: 123, 906 Rows: 456, 907 }, 908 }, 909 }) 910 Expect(err).NotTo(HaveOccurred()) 911 }) 912 913 It("forwards it to the process", func() { 914 Eventually(fakeProcess.SetTTYCallCount).Should(Equal(1)) 915 916 Expect(fakeProcess.SetTTYArgsForCall(0)).To(Equal(garden.TTYSpec{ 917 WindowSize: &garden.WindowSize{ 918 Columns: 123, 919 Rows: 456, 920 }, 921 })) 922 }) 923 924 Context("and setting the TTY on the process fails", func() { 925 BeforeEach(func() { 926 fakeProcess.SetTTYReturns(errors.New("oh no!")) 927 }) 928 929 It("forwards the error to the response", func() { 930 var hijackOutput atc.HijackOutput 931 err := conn.ReadJSON(&hijackOutput) 932 Expect(err).NotTo(HaveOccurred()) 933 934 Expect(hijackOutput).To(Equal(atc.HijackOutput{ 935 Error: "oh no!", 936 })) 937 }) 938 }) 939 }) 940 941 Context("when waiting on the process fails", func() { 942 BeforeEach(func() { 943 fakeProcess.WaitReturns(0, errors.New("oh no!")) 944 }) 945 946 It("forwards the error to the response", func() { 947 var hijackOutput atc.HijackOutput 948 err := conn.ReadJSON(&hijackOutput) 949 Expect(err).NotTo(HaveOccurred()) 950 951 Expect(hijackOutput).To(Equal(atc.HijackOutput{ 952 Error: "oh no!", 953 })) 954 }) 955 }) 956 957 Context("when intercept timeout channel sends a value", func() { 958 var ( 959 interceptTimeoutChannel chan time.Time 960 ) 961 962 BeforeEach(func() { 963 interceptTimeoutChannel = make(chan time.Time) 964 interceptTimeout.ChannelReturns(interceptTimeoutChannel) 965 }) 966 967 It("exits with timeout error", func() { 968 interceptTimeout.ErrorReturns(errors.New("too slow")) 969 interceptTimeoutChannel <- time.Time{} 970 971 var hijackOutput atc.HijackOutput 972 err := conn.ReadJSON(&hijackOutput) 973 Expect(err).NotTo(HaveOccurred()) 974 975 Expect(hijackOutput.Error).To(Equal("too slow")) 976 }) 977 }) 978 }) 979 }) 980 }) 981 }) 982 }) 983 984 Context("when not authenticated", func() { 985 BeforeEach(func() { 986 expectBadHandshake = true 987 988 fakeAccess.IsAuthenticatedReturns(false) 989 }) 990 991 It("returns 401 Unauthorized", func() { 992 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 993 }) 994 }) 995 }) 996 997 Describe("GET /api/v1/containers/destroying", func() { 998 BeforeEach(func() { 999 var err error 1000 req, err = http.NewRequest("GET", server.URL+"/api/v1/containers/destroying", nil) 1001 Expect(err).NotTo(HaveOccurred()) 1002 req.Header.Set("Content-Type", "application/json") 1003 1004 fakeAccess.IsAuthenticatedReturns(true) 1005 }) 1006 1007 Context("when not authenticated", func() { 1008 BeforeEach(func() { 1009 fakeAccess.IsAuthenticatedReturns(false) 1010 }) 1011 1012 It("returns 401 Unauthorized", func() { 1013 response, err := client.Do(req) 1014 Expect(err).NotTo(HaveOccurred()) 1015 1016 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 1017 }) 1018 1019 It("does not attempt to find the worker", func() { 1020 Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero()) 1021 }) 1022 }) 1023 1024 Context("when authenticated as system", func() { 1025 BeforeEach(func() { 1026 fakeAccess.IsSystemReturns(true) 1027 }) 1028 1029 Context("with no params", func() { 1030 It("returns 400 Bad Request", func() { 1031 response, err := client.Do(req) 1032 Expect(err).NotTo(HaveOccurred()) 1033 1034 Expect(fakeContainerRepository.FindDestroyingContainersCallCount()).To(Equal(0)) 1035 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 1036 }) 1037 }) 1038 1039 Context("querying with worker name", func() { 1040 BeforeEach(func() { 1041 req.URL.RawQuery = url.Values{ 1042 "worker_name": []string{"some-worker-name"}, 1043 }.Encode() 1044 }) 1045 1046 Context("when there is an error", func() { 1047 BeforeEach(func() { 1048 fakeContainerRepository.FindDestroyingContainersReturns(nil, errors.New("some error")) 1049 }) 1050 1051 It("returns 500", func() { 1052 response, err := client.Do(req) 1053 Expect(err).NotTo(HaveOccurred()) 1054 1055 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 1056 }) 1057 }) 1058 1059 Context("when no containers are found", func() { 1060 BeforeEach(func() { 1061 fakeContainerRepository.FindDestroyingContainersReturns([]string{}, nil) 1062 }) 1063 1064 It("returns 200", func() { 1065 response, err := client.Do(req) 1066 Expect(err).NotTo(HaveOccurred()) 1067 1068 Expect(response.StatusCode).To(Equal(http.StatusOK)) 1069 }) 1070 1071 It("returns an empty array", func() { 1072 response, err := client.Do(req) 1073 Expect(err).NotTo(HaveOccurred()) 1074 1075 body, err := ioutil.ReadAll(response.Body) 1076 Expect(err).NotTo(HaveOccurred()) 1077 1078 Expect(body).To(MatchJSON(` 1079 [] 1080 `)) 1081 }) 1082 1083 Context("when containers are found", func() { 1084 BeforeEach(func() { 1085 fakeContainerRepository.FindDestroyingContainersReturns([]string{ 1086 "handle1", 1087 "handle2", 1088 }, nil) 1089 }) 1090 It("returns container handles array", func() { 1091 response, err := client.Do(req) 1092 Expect(err).NotTo(HaveOccurred()) 1093 1094 body, err := ioutil.ReadAll(response.Body) 1095 Expect(err).NotTo(HaveOccurred()) 1096 1097 Expect(body).To(MatchJSON(` 1098 ["handle1", "handle2"] 1099 `)) 1100 }) 1101 }) 1102 }) 1103 1104 It("queries with it in the worker name", func() { 1105 _, err := client.Do(req) 1106 Expect(err).NotTo(HaveOccurred()) 1107 1108 Expect(fakeContainerRepository.FindDestroyingContainersCallCount()).To(Equal(1)) 1109 1110 workerName := fakeContainerRepository.FindDestroyingContainersArgsForCall(0) 1111 Expect(workerName).To(Equal("some-worker-name")) 1112 }) 1113 }) 1114 }) 1115 }) 1116 1117 Describe("PUT /api/v1/containers/report", func() { 1118 var response *http.Response 1119 var body io.Reader 1120 var err error 1121 1122 BeforeEach(func() { 1123 body = bytes.NewBufferString(` 1124 [ 1125 "handle1", 1126 "handle2" 1127 ] 1128 `) 1129 }) 1130 1131 JustBeforeEach(func() { 1132 req, err = http.NewRequest("PUT", server.URL+"/api/v1/containers/report", body) 1133 Expect(err).NotTo(HaveOccurred()) 1134 req.Header.Set("Content-Type", "application/json") 1135 }) 1136 1137 Context("when not authenticated", func() { 1138 BeforeEach(func() { 1139 fakeAccess.IsAuthenticatedReturns(false) 1140 }) 1141 1142 It("returns 401 Unauthorized", func() { 1143 response, err = client.Do(req) 1144 Expect(err).NotTo(HaveOccurred()) 1145 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 1146 }) 1147 }) 1148 1149 Context("when authenticated as system", func() { 1150 BeforeEach(func() { 1151 fakeAccess.IsAuthenticatedReturns(true) 1152 fakeAccess.IsSystemReturns(true) 1153 }) 1154 1155 Context("with no params", func() { 1156 It("returns 404", func() { 1157 response, err = client.Do(req) 1158 Expect(err).NotTo(HaveOccurred()) 1159 Expect(fakeDestroyer.DestroyContainersCallCount()).To(Equal(0)) 1160 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 1161 }) 1162 1163 It("returns Content-Type application/json", func() { 1164 response, err = client.Do(req) 1165 Expect(err).NotTo(HaveOccurred()) 1166 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 1167 expectedHeaderEntries := map[string]string{ 1168 "Content-Type": "application/json", 1169 } 1170 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 1171 }) 1172 }) 1173 1174 Context("querying with worker name", func() { 1175 JustBeforeEach(func() { 1176 req.URL.RawQuery = url.Values{ 1177 "worker_name": []string{"some-worker-name"}, 1178 }.Encode() 1179 }) 1180 1181 Context("with invalid json", func() { 1182 BeforeEach(func() { 1183 body = bytes.NewBufferString(`{}`) 1184 }) 1185 1186 It("returns 400", func() { 1187 response, err = client.Do(req) 1188 Expect(err).NotTo(HaveOccurred()) 1189 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 1190 }) 1191 }) 1192 1193 Context("when there is an error", func() { 1194 BeforeEach(func() { 1195 fakeDestroyer.DestroyContainersReturns(errors.New("some error")) 1196 }) 1197 1198 It("returns 500", func() { 1199 response, err = client.Do(req) 1200 Expect(err).NotTo(HaveOccurred()) 1201 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 1202 }) 1203 }) 1204 1205 Context("when containers are destroyed", func() { 1206 BeforeEach(func() { 1207 fakeDestroyer.DestroyContainersReturns(nil) 1208 }) 1209 1210 It("returns 204", func() { 1211 response, err = client.Do(req) 1212 Expect(err).NotTo(HaveOccurred()) 1213 Expect(response.StatusCode).To(Equal(http.StatusNoContent)) 1214 }) 1215 }) 1216 1217 It("queries with it in the worker name", func() { 1218 _, err = client.Do(req) 1219 Expect(err).NotTo(HaveOccurred()) 1220 Expect(fakeDestroyer.DestroyContainersCallCount()).To(Equal(1)) 1221 1222 workerName, handles := fakeDestroyer.DestroyContainersArgsForCall(0) 1223 Expect(workerName).To(Equal("some-worker-name")) 1224 Expect(handles).To(Equal([]string{"handle1", "handle2"})) 1225 }) 1226 1227 It("marks containers as missing", func() { 1228 _, err = client.Do(req) 1229 Expect(err).NotTo(HaveOccurred()) 1230 Expect(fakeContainerRepository.UpdateContainersMissingSinceCallCount()).To(Equal(1)) 1231 1232 workerName, handles := fakeContainerRepository.UpdateContainersMissingSinceArgsForCall(0) 1233 Expect(workerName).To(Equal("some-worker-name")) 1234 Expect(handles).To(Equal([]string{"handle1", "handle2"})) 1235 }) 1236 }) 1237 }) 1238 }) 1239 })