1package api_test 2 3import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "io/ioutil" 8 "net/http" 9 "time" 10 11 "github.com/concourse/concourse/atc" 12 "github.com/concourse/concourse/atc/db" 13 "github.com/concourse/concourse/atc/db/dbfakes" 14 . "github.com/concourse/concourse/atc/testhelpers" 15 . "github.com/onsi/ginkgo" 16 . "github.com/onsi/gomega" 17) 18 19var _ = Describe("Workers API", func() { 20 21 Describe("GET /api/v1/workers", func() { 22 var response *http.Response 23 24 JustBeforeEach(func() { 25 req, err := http.NewRequest("GET", server.URL+"/api/v1/workers", nil) 26 Expect(err).NotTo(HaveOccurred()) 27 28 response, err = client.Do(req) 29 Expect(err).NotTo(HaveOccurred()) 30 }) 31 32 Context("when authenticated", func() { 33 var ( 34 teamWorker1 *dbfakes.FakeWorker 35 teamWorker2 *dbfakes.FakeWorker 36 ) 37 38 BeforeEach(func() { 39 fakeAccess.IsAuthenticatedReturns(true) 40 fakeAccess.IsAuthorizedReturns(true) 41 fakeAccess.TeamNamesReturns([]string{"some-team"}) 42 dbWorkerFactory.VisibleWorkersReturns(nil, nil) 43 44 teamWorker1 = new(dbfakes.FakeWorker) 45 gardenAddr1 := "1.2.3.4:7777" 46 teamWorker1.GardenAddrReturns(&gardenAddr1) 47 bcURL1 := "1.2.3.4:8888" 48 teamWorker1.BaggageclaimURLReturns(&bcURL1) 49 50 teamWorker2 = new(dbfakes.FakeWorker) 51 gardenAddr2 := "5.6.7.8:7777" 52 teamWorker2.GardenAddrReturns(&gardenAddr2) 53 bcURL2 := "5.6.7.8:8888" 54 teamWorker2.BaggageclaimURLReturns(&bcURL2) 55 }) 56 57 It("fetches workers by team name from worker user context", func() { 58 Expect(dbWorkerFactory.VisibleWorkersCallCount()).To(Equal(1)) 59 60 teamNames := dbWorkerFactory.VisibleWorkersArgsForCall(0) 61 Expect(teamNames).To(ConsistOf("some-team")) 62 }) 63 64 Context("when user is an admin", func() { 65 BeforeEach(func() { 66 fakeAccess.IsAdminReturns(true) 67 dbWorkerFactory.WorkersReturns([]db.Worker{ 68 teamWorker1, 69 teamWorker2, 70 }, nil) 71 }) 72 73 It("returns all the workers", func() { 74 Expect(response.StatusCode).To(Equal(http.StatusOK)) 75 expectedHeaderEntries := map[string]string{ 76 "Content-Type": "application/json", 77 } 78 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 79 80 Expect(dbWorkerFactory.WorkersCallCount()).To(Equal(1)) 81 82 var returnedWorkers []atc.Worker 83 err := json.NewDecoder(response.Body).Decode(&returnedWorkers) 84 Expect(err).NotTo(HaveOccurred()) 85 86 Expect(returnedWorkers).To(Equal([]atc.Worker{ 87 { 88 GardenAddr: "1.2.3.4:7777", 89 BaggageclaimURL: "1.2.3.4:8888", 90 }, 91 { 92 GardenAddr: "5.6.7.8:7777", 93 BaggageclaimURL: "5.6.7.8:8888", 94 }, 95 })) 96 }) 97 }) 98 99 Context("when the workers can be listed", func() { 100 BeforeEach(func() { 101 dbWorkerFactory.VisibleWorkersReturns([]db.Worker{ 102 teamWorker1, 103 teamWorker2, 104 }, nil) 105 }) 106 107 It("returns 200", func() { 108 Expect(response.StatusCode).To(Equal(http.StatusOK)) 109 }) 110 111 It("returns Content-Type 'application/json'", func() { 112 expectedHeaderEntries := map[string]string{ 113 "Content-Type": "application/json", 114 } 115 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 116 }) 117 118 It("returns the workers", func() { 119 var returnedWorkers []atc.Worker 120 err := json.NewDecoder(response.Body).Decode(&returnedWorkers) 121 Expect(err).NotTo(HaveOccurred()) 122 123 Expect(dbWorkerFactory.VisibleWorkersCallCount()).To(Equal(1)) 124 125 Expect(returnedWorkers).To(Equal([]atc.Worker{ 126 { 127 GardenAddr: "1.2.3.4:7777", 128 BaggageclaimURL: "1.2.3.4:8888", 129 }, 130 { 131 GardenAddr: "5.6.7.8:7777", 132 BaggageclaimURL: "5.6.7.8:8888", 133 }, 134 })) 135 136 }) 137 }) 138 139 Context("when getting the workers fails", func() { 140 BeforeEach(func() { 141 dbWorkerFactory.VisibleWorkersReturns(nil, errors.New("error!")) 142 }) 143 144 It("returns 500", func() { 145 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 146 }) 147 }) 148 }) 149 150 Context("when not authenticated", func() { 151 BeforeEach(func() { 152 fakeAccess.IsAuthenticatedReturns(false) 153 }) 154 155 It("returns 401", func() { 156 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 157 }) 158 }) 159 }) 160 161 Describe("POST /api/v1/workers", func() { 162 var ( 163 worker atc.Worker 164 ttl string 165 certsPath string 166 167 response *http.Response 168 ) 169 170 BeforeEach(func() { 171 certsPath = "/some/certs/path" 172 worker = atc.Worker{ 173 Name: "worker-name", 174 GardenAddr: "1.2.3.4:7777", 175 BaggageclaimURL: "5.6.7.8:7788", 176 CertsPath: &certsPath, 177 HTTPProxyURL: "http://example.com", 178 HTTPSProxyURL: "https://example.com", 179 NoProxy: "example.com,127.0.0.1,localhost", 180 ActiveContainers: 2, 181 ActiveVolumes: 10, 182 ActiveTasks: 42, 183 ResourceTypes: []atc.WorkerResourceType{ 184 {Type: "some-resource", Image: "some-resource-image"}, 185 }, 186 Platform: "haiku", 187 Tags: []string{"not", "a", "limerick"}, 188 Version: "1.2.3", 189 } 190 191 ttl = "30s" 192 fakeAccess.IsAuthorizedReturns(true) 193 fakeAccess.IsSystemReturns(true) 194 }) 195 196 JustBeforeEach(func() { 197 payload, err := json.Marshal(worker) 198 Expect(err).NotTo(HaveOccurred()) 199 200 req, err := http.NewRequest("POST", server.URL+"/api/v1/workers?ttl="+ttl, ioutil.NopCloser(bytes.NewBuffer(payload))) 201 Expect(err).NotTo(HaveOccurred()) 202 203 response, err = client.Do(req) 204 Expect(err).NotTo(HaveOccurred()) 205 }) 206 207 Context("when authenticated", func() { 208 BeforeEach(func() { 209 fakeAccess.IsAuthenticatedReturns(true) 210 }) 211 212 It("tries to save the worker", func() { 213 Expect(dbWorkerFactory.SaveWorkerCallCount()).To(Equal(1)) 214 savedWorker, savedTTL := dbWorkerFactory.SaveWorkerArgsForCall(0) 215 Expect(savedWorker).To(Equal(atc.Worker{ 216 GardenAddr: "1.2.3.4:7777", 217 Name: "worker-name", 218 BaggageclaimURL: "5.6.7.8:7788", 219 CertsPath: &certsPath, 220 HTTPProxyURL: "http://example.com", 221 HTTPSProxyURL: "https://example.com", 222 NoProxy: "example.com,127.0.0.1,localhost", 223 ActiveContainers: 2, 224 ActiveVolumes: 10, 225 ActiveTasks: 42, 226 ResourceTypes: []atc.WorkerResourceType{ 227 {Type: "some-resource", Image: "some-resource-image"}, 228 }, 229 Platform: "haiku", 230 Tags: []string{"not", "a", "limerick"}, 231 Version: "1.2.3", 232 })) 233 234 Expect(savedTTL.String()).To(Equal(ttl)) 235 }) 236 237 Context("when request is not from tsa", func() { 238 Context("when system claim is false", func() { 239 BeforeEach(func() { 240 fakeAccess.IsSystemReturns(false) 241 }) 242 243 It("return 403", func() { 244 Expect(response.StatusCode).To(Equal(http.StatusForbidden)) 245 }) 246 }) 247 }) 248 249 Context("when payload contains team name", func() { 250 BeforeEach(func() { 251 worker.Team = "some-team" 252 }) 253 254 Context("when specified team exists", func() { 255 var foundTeam *dbfakes.FakeTeam 256 257 BeforeEach(func() { 258 foundTeam = new(dbfakes.FakeTeam) 259 dbTeamFactory.FindTeamReturns(foundTeam, true, nil) 260 }) 261 262 It("saves team name in db", func() { 263 Expect(foundTeam.SaveWorkerCallCount()).To(Equal(1)) 264 }) 265 266 Context("when saving the worker succeeds", func() { 267 BeforeEach(func() { 268 foundTeam.SaveWorkerReturns(new(dbfakes.FakeWorker), nil) 269 }) 270 271 It("returns 200", func() { 272 Expect(response.StatusCode).To(Equal(http.StatusOK)) 273 }) 274 }) 275 276 Context("when saving the worker fails", func() { 277 BeforeEach(func() { 278 foundTeam.SaveWorkerReturns(nil, errors.New("oh no!")) 279 }) 280 281 It("returns 500", func() { 282 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 283 }) 284 }) 285 }) 286 287 Context("when specified team does not exist", func() { 288 BeforeEach(func() { 289 dbTeamFactory.FindTeamReturns(nil, false, nil) 290 }) 291 292 It("returns 400", func() { 293 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 294 }) 295 }) 296 }) 297 298 Context("when the worker has no name", func() { 299 BeforeEach(func() { 300 worker.Name = "" 301 }) 302 303 It("tries to save the worker with the garden address as the name", func() { 304 Expect(dbWorkerFactory.SaveWorkerCallCount()).To(Equal(1)) 305 306 savedInfo, savedTTL := dbWorkerFactory.SaveWorkerArgsForCall(0) 307 Expect(savedInfo).To(Equal(atc.Worker{ 308 GardenAddr: "1.2.3.4:7777", 309 Name: "1.2.3.4:7777", 310 BaggageclaimURL: "5.6.7.8:7788", 311 CertsPath: &certsPath, 312 HTTPProxyURL: "http://example.com", 313 HTTPSProxyURL: "https://example.com", 314 NoProxy: "example.com,127.0.0.1,localhost", 315 ActiveContainers: 2, 316 ActiveVolumes: 10, 317 ActiveTasks: 42, 318 ResourceTypes: []atc.WorkerResourceType{ 319 {Type: "some-resource", Image: "some-resource-image"}, 320 }, 321 Platform: "haiku", 322 Tags: []string{"not", "a", "limerick"}, 323 Version: "1.2.3", 324 })) 325 326 Expect(savedTTL.String()).To(Equal(ttl)) 327 }) 328 }) 329 330 Context("when the certs path is null", func() { 331 BeforeEach(func() { 332 worker.CertsPath = nil 333 }) 334 335 It("saves the worker with a null certs path", func() { 336 Expect(dbWorkerFactory.SaveWorkerCallCount()).To(Equal(1)) 337 338 savedInfo, savedTTL := dbWorkerFactory.SaveWorkerArgsForCall(0) 339 Expect(savedInfo).To(Equal(atc.Worker{ 340 GardenAddr: "1.2.3.4:7777", 341 Name: "worker-name", 342 BaggageclaimURL: "5.6.7.8:7788", 343 CertsPath: nil, 344 HTTPProxyURL: "http://example.com", 345 HTTPSProxyURL: "https://example.com", 346 NoProxy: "example.com,127.0.0.1,localhost", 347 ActiveContainers: 2, 348 ActiveVolumes: 10, 349 ActiveTasks: 42, 350 ResourceTypes: []atc.WorkerResourceType{ 351 {Type: "some-resource", Image: "some-resource-image"}, 352 }, 353 Platform: "haiku", 354 Tags: []string{"not", "a", "limerick"}, 355 Version: "1.2.3", 356 })) 357 358 Expect(savedTTL.String()).To(Equal(ttl)) 359 }) 360 }) 361 362 Context("when the certs path is an empty string", func() { 363 BeforeEach(func() { 364 emptyString := "" 365 worker.CertsPath = &emptyString 366 }) 367 368 It("saves the worker with a null certs path", func() { 369 Expect(dbWorkerFactory.SaveWorkerCallCount()).To(Equal(1)) 370 371 savedInfo, savedTTL := dbWorkerFactory.SaveWorkerArgsForCall(0) 372 Expect(savedInfo).To(Equal(atc.Worker{ 373 GardenAddr: "1.2.3.4:7777", 374 Name: "worker-name", 375 BaggageclaimURL: "5.6.7.8:7788", 376 CertsPath: nil, 377 HTTPProxyURL: "http://example.com", 378 HTTPSProxyURL: "https://example.com", 379 NoProxy: "example.com,127.0.0.1,localhost", 380 ActiveContainers: 2, 381 ActiveVolumes: 10, 382 ActiveTasks: 42, 383 ResourceTypes: []atc.WorkerResourceType{ 384 {Type: "some-resource", Image: "some-resource-image"}, 385 }, 386 Platform: "haiku", 387 Tags: []string{"not", "a", "limerick"}, 388 Version: "1.2.3", 389 })) 390 391 Expect(savedTTL.String()).To(Equal(ttl)) 392 }) 393 }) 394 395 Context("when saving the worker succeeds", func() { 396 var fakeWorker *dbfakes.FakeWorker 397 BeforeEach(func() { 398 fakeWorker = new(dbfakes.FakeWorker) 399 dbWorkerFactory.SaveWorkerReturns(fakeWorker, nil) 400 }) 401 402 It("returns 200", func() { 403 Expect(response.StatusCode).To(Equal(http.StatusOK)) 404 }) 405 406 }) 407 408 Context("when saving the worker fails", func() { 409 BeforeEach(func() { 410 dbWorkerFactory.SaveWorkerReturns(nil, errors.New("oh no!")) 411 }) 412 413 It("returns 500", func() { 414 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 415 }) 416 }) 417 418 Context("when the TTL is invalid", func() { 419 BeforeEach(func() { 420 ttl = "invalid-duration" 421 }) 422 423 It("returns 400", func() { 424 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 425 }) 426 427 It("returns the validation error in the response body", func() { 428 Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("malformed ttl"))) 429 }) 430 431 It("does not save it", func() { 432 Expect(dbWorkerFactory.SaveWorkerCallCount()).To(BeZero()) 433 }) 434 }) 435 436 Context("when the worker has no address", func() { 437 BeforeEach(func() { 438 worker.GardenAddr = "" 439 }) 440 441 It("returns 400", func() { 442 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 443 }) 444 445 It("returns the validation error in the response body", func() { 446 Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("missing garden address"))) 447 }) 448 449 It("does not save it", func() { 450 Expect(dbWorkerFactory.SaveWorkerCallCount()).To(BeZero()) 451 }) 452 }) 453 454 Context("when worker version is invalid", func() { 455 BeforeEach(func() { 456 worker.Version = "invalid" 457 }) 458 459 It("returns 400", func() { 460 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 461 }) 462 463 It("returns the validation error in the response body", func() { 464 Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("invalid worker version, only numeric characters are allowed"))) 465 }) 466 467 It("does not save it", func() { 468 Expect(dbWorkerFactory.SaveWorkerCallCount()).To(BeZero()) 469 }) 470 }) 471 }) 472 473 Context("when not authenticated", func() { 474 BeforeEach(func() { 475 fakeAccess.IsAuthenticatedReturns(false) 476 }) 477 478 It("returns 401", func() { 479 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 480 }) 481 482 It("does not save the config", func() { 483 Expect(dbWorkerFactory.SaveWorkerCallCount()).To(BeZero()) 484 }) 485 }) 486 }) 487 488 Describe("PUT /api/v1/workers/:worker_name/land", func() { 489 var ( 490 response *http.Response 491 workerName string 492 fakeWorker *dbfakes.FakeWorker 493 ) 494 495 JustBeforeEach(func() { 496 req, err := http.NewRequest("PUT", server.URL+"/api/v1/workers/"+workerName+"/land", nil) 497 Expect(err).NotTo(HaveOccurred()) 498 499 response, err = client.Do(req) 500 Expect(err).NotTo(HaveOccurred()) 501 }) 502 503 BeforeEach(func() { 504 fakeWorker = new(dbfakes.FakeWorker) 505 workerName = "some-worker" 506 fakeWorker.NameReturns(workerName) 507 fakeWorker.TeamNameReturns("some-team") 508 fakeWorker.LandReturns(nil) 509 510 fakeAccess.IsAuthenticatedReturns(true) 511 dbWorkerFactory.GetWorkerReturns(fakeWorker, true, nil) 512 }) 513 514 Context("when the request is authenticated as system", func() { 515 BeforeEach(func() { 516 fakeAccess.IsSystemReturns(true) 517 }) 518 519 It("returns 200", func() { 520 Expect(response.StatusCode).To(Equal(http.StatusOK)) 521 }) 522 523 It("sees if the worker exists and attempts to land it", func() { 524 Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1)) 525 Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName)) 526 Expect(fakeWorker.LandCallCount()).To(Equal(1)) 527 }) 528 529 Context("when landing the worker fails", func() { 530 var returnedErr error 531 532 BeforeEach(func() { 533 returnedErr = errors.New("some-error") 534 fakeWorker.LandReturns(returnedErr) 535 }) 536 537 It("returns 500", func() { 538 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 539 }) 540 }) 541 542 Context("when the worker does not exist", func() { 543 BeforeEach(func() { 544 dbWorkerFactory.GetWorkerReturns(nil, false, nil) 545 }) 546 547 It("returns 404", func() { 548 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 549 }) 550 }) 551 }) 552 553 Context("when the request is authorized as the worker's owner", func() { 554 BeforeEach(func() { 555 fakeAccess.IsAuthorizedReturns(true) 556 }) 557 558 It("returns 200", func() { 559 Expect(response.StatusCode).To(Equal(http.StatusOK)) 560 }) 561 }) 562 563 Context("when the request is authorized as the wrong team", func() { 564 BeforeEach(func() { 565 fakeAccess.IsAuthorizedReturns(false) 566 }) 567 568 It("returns 403", func() { 569 Expect(response.StatusCode).To(Equal(http.StatusForbidden)) 570 }) 571 }) 572 573 Context("when not authenticated", func() { 574 BeforeEach(func() { 575 fakeAccess.IsAuthenticatedReturns(false) 576 }) 577 578 It("returns 401", func() { 579 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 580 }) 581 582 It("does not attempt to find the worker", func() { 583 Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero()) 584 }) 585 }) 586 }) 587 588 Describe("PUT /api/v1/workers/:worker_name/retire", func() { 589 var ( 590 response *http.Response 591 workerName string 592 fakeWorker *dbfakes.FakeWorker 593 ) 594 595 JustBeforeEach(func() { 596 req, err := http.NewRequest("PUT", server.URL+"/api/v1/workers/"+workerName+"/retire", nil) 597 Expect(err).NotTo(HaveOccurred()) 598 599 response, err = client.Do(req) 600 Expect(err).NotTo(HaveOccurred()) 601 }) 602 603 BeforeEach(func() { 604 fakeWorker = new(dbfakes.FakeWorker) 605 workerName = "some-worker" 606 fakeWorker.NameReturns(workerName) 607 fakeWorker.TeamNameReturns("some-team") 608 fakeAccess.IsAuthenticatedReturns(true) 609 610 dbWorkerFactory.GetWorkerReturns(fakeWorker, true, nil) 611 fakeWorker.RetireReturns(nil) 612 }) 613 614 Context("when autheticated as system", func() { 615 BeforeEach(func() { 616 fakeAccess.IsSystemReturns(true) 617 }) 618 619 It("returns 200", func() { 620 Expect(response.StatusCode).To(Equal(http.StatusOK)) 621 }) 622 623 It("sees if the worker exists and attempts to retire it", func() { 624 Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1)) 625 Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName)) 626 627 Expect(fakeWorker.RetireCallCount()).To(Equal(1)) 628 }) 629 630 Context("when retiring the worker fails", func() { 631 var returnedErr error 632 633 BeforeEach(func() { 634 returnedErr = errors.New("some-error") 635 fakeWorker.RetireReturns(returnedErr) 636 }) 637 638 It("returns 500", func() { 639 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 640 }) 641 }) 642 643 Context("when the worker does not exist", func() { 644 BeforeEach(func() { 645 dbWorkerFactory.GetWorkerReturns(nil, false, nil) 646 }) 647 648 It("returns 404", func() { 649 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 650 }) 651 }) 652 }) 653 654 Context("when authorized as as the worker's owner", func() { 655 BeforeEach(func() { 656 fakeAccess.IsAuthorizedReturns(true) 657 }) 658 659 It("returns 200", func() { 660 Expect(response.StatusCode).To(Equal(http.StatusOK)) 661 }) 662 }) 663 664 Context("when authorized as some other team", func() { 665 BeforeEach(func() { 666 fakeAccess.IsAuthorizedReturns(false) 667 }) 668 669 It("returns 403", func() { 670 Expect(response.StatusCode).To(Equal(http.StatusForbidden)) 671 }) 672 }) 673 674 Context("when not authenticated", func() { 675 BeforeEach(func() { 676 fakeAccess.IsAuthenticatedReturns(false) 677 }) 678 679 It("returns 401", func() { 680 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 681 }) 682 683 It("does not attempt to find the worker", func() { 684 Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero()) 685 }) 686 }) 687 }) 688 689 Describe("PUT /api/v1/workers/:worker_name/prune", func() { 690 var ( 691 response *http.Response 692 workerName string 693 fakeWorker *dbfakes.FakeWorker 694 ) 695 696 JustBeforeEach(func() { 697 req, err := http.NewRequest("PUT", server.URL+"/api/v1/workers/"+workerName+"/prune", nil) 698 Expect(err).NotTo(HaveOccurred()) 699 700 response, err = client.Do(req) 701 Expect(err).NotTo(HaveOccurred()) 702 }) 703 704 BeforeEach(func() { 705 fakeWorker = new(dbfakes.FakeWorker) 706 workerName = "some-worker" 707 fakeWorker.NameReturns(workerName) 708 fakeWorker.TeamNameReturns("some-team") 709 710 dbWorkerFactory.GetWorkerReturns(fakeWorker, true, nil) 711 fakeAccess.IsAuthenticatedReturns(true) 712 fakeAccess.IsAuthorizedReturns(true) 713 fakeWorker.PruneReturns(nil) 714 }) 715 716 It("returns 200", func() { 717 Expect(response.StatusCode).To(Equal(http.StatusOK)) 718 }) 719 720 It("sees if the worker exists and attempts to prune it", func() { 721 Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName)) 722 Expect(fakeWorker.PruneCallCount()).To(Equal(1)) 723 }) 724 725 Context("when pruning the worker fails", func() { 726 var returnedErr error 727 728 BeforeEach(func() { 729 returnedErr = errors.New("some-error") 730 fakeWorker.PruneReturns(returnedErr) 731 }) 732 733 It("returns 500", func() { 734 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 735 }) 736 }) 737 738 Context("when the worker does not exist", func() { 739 BeforeEach(func() { 740 dbWorkerFactory.GetWorkerReturns(nil, false, nil) 741 }) 742 743 It("returns 404", func() { 744 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 745 }) 746 }) 747 748 Context("when the worker is running", func() { 749 BeforeEach(func() { 750 fakeWorker.PruneReturns(db.ErrCannotPruneRunningWorker) 751 }) 752 753 It("returns 400", func() { 754 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 755 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`{"stderr":"cannot prune running worker"}`)) 756 }) 757 }) 758 759 Context("when not authenticated", func() { 760 BeforeEach(func() { 761 fakeAccess.IsAuthenticatedReturns(false) 762 }) 763 764 It("returns 401", func() { 765 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 766 }) 767 768 It("does not attempt to find the worker", func() { 769 Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero()) 770 }) 771 }) 772 }) 773 774 Describe("PUT /api/v1/workers/:worker_name/heartbeat", func() { 775 var ( 776 response *http.Response 777 workerName string 778 ttlStr string 779 ttl time.Duration 780 err error 781 782 worker atc.Worker 783 fakeWorker *dbfakes.FakeWorker 784 ) 785 786 BeforeEach(func() { 787 fakeWorker = new(dbfakes.FakeWorker) 788 workerName = "some-name" 789 fakeWorker.NameReturns(workerName) 790 fakeWorker.ActiveContainersReturns(2) 791 fakeWorker.ActiveVolumesReturns(10) 792 fakeWorker.ActiveTasksReturns(42, nil) 793 fakeWorker.PlatformReturns("penguin") 794 fakeWorker.TagsReturns([]string{"some-tag"}) 795 fakeWorker.StateReturns(db.WorkerStateRunning) 796 fakeWorker.TeamNameReturns("some-team") 797 fakeWorker.EphemeralReturns(true) 798 799 ttlStr = "30s" 800 ttl, err = time.ParseDuration(ttlStr) 801 Expect(err).NotTo(HaveOccurred()) 802 803 worker = atc.Worker{ 804 Name: workerName, 805 ActiveContainers: 2, 806 } 807 fakeAccess.IsAuthenticatedReturns(true) 808 dbWorkerFactory.HeartbeatWorkerReturns(fakeWorker, nil) 809 }) 810 811 JustBeforeEach(func() { 812 payload, err := json.Marshal(worker) 813 Expect(err).NotTo(HaveOccurred()) 814 815 req, err := http.NewRequest("PUT", server.URL+"/api/v1/workers/"+workerName+"/heartbeat?ttl="+ttlStr, ioutil.NopCloser(bytes.NewBuffer(payload))) 816 Expect(err).NotTo(HaveOccurred()) 817 818 response, err = client.Do(req) 819 Expect(err).NotTo(HaveOccurred()) 820 }) 821 822 It("returns 200", func() { 823 Expect(response.StatusCode).To(Equal(http.StatusOK)) 824 }) 825 826 It("returns Content-Type 'application/json'", func() { 827 expectedHeaderEntries := map[string]string{ 828 "Content-Type": "application/json", 829 } 830 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 831 }) 832 833 It("returns saved worker", func() { 834 contents, err := ioutil.ReadAll(response.Body) 835 Expect(err).NotTo(HaveOccurred()) 836 837 Expect(contents).To(MatchJSON(`{ 838 "name": "some-name", 839 "state": "running", 840 "addr": "", 841 "baggageclaim_url": "", 842 "active_containers": 2, 843 "active_volumes": 10, 844 "active_tasks": 42, 845 "resource_types": null, 846 "platform": "penguin", 847 "ephemeral": true, 848 "tags": ["some-tag"], 849 "team": "some-team", 850 "start_time": 0, 851 "version": "" 852 }`)) 853 }) 854 855 It("sees if the worker exists and attempts to heartbeat with provided ttl", func() { 856 Expect(dbWorkerFactory.HeartbeatWorkerCallCount()).To(Equal(1)) 857 858 w, t := dbWorkerFactory.HeartbeatWorkerArgsForCall(0) 859 Expect(w).To(Equal(worker)) 860 Expect(t).To(Equal(ttl)) 861 }) 862 863 Context("when the TTL is invalid", func() { 864 BeforeEach(func() { 865 ttlStr = "invalid-duration" 866 }) 867 868 It("returns 400", func() { 869 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 870 }) 871 872 It("returns the validation error in the response body", func() { 873 Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("malformed ttl"))) 874 }) 875 876 It("does not heartbeat worker", func() { 877 Expect(dbWorkerFactory.HeartbeatWorkerCallCount()).To(BeZero()) 878 }) 879 }) 880 881 Context("when heartbeating the worker fails", func() { 882 var returnedErr error 883 884 BeforeEach(func() { 885 returnedErr = errors.New("some-error") 886 dbWorkerFactory.HeartbeatWorkerReturns(nil, returnedErr) 887 }) 888 889 It("returns 500", func() { 890 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 891 }) 892 }) 893 894 Context("when the worker does not exist", func() { 895 BeforeEach(func() { 896 dbWorkerFactory.HeartbeatWorkerReturns(nil, db.ErrWorkerNotPresent) 897 }) 898 899 It("returns 404", func() { 900 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 901 }) 902 }) 903 904 Context("when not authenticated", func() { 905 BeforeEach(func() { 906 fakeAccess.IsAuthenticatedReturns(false) 907 }) 908 909 It("returns 401", func() { 910 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 911 }) 912 913 It("does not heartbeat the worker", func() { 914 Expect(dbWorkerFactory.HeartbeatWorkerCallCount()).To(BeZero()) 915 }) 916 }) 917 }) 918 919 Describe("DELETE /api/v1/workers/:worker_name", func() { 920 var ( 921 response *http.Response 922 workerName string 923 fakeWorker *dbfakes.FakeWorker 924 ) 925 926 JustBeforeEach(func() { 927 req, err := http.NewRequest("DELETE", server.URL+"/api/v1/workers/"+workerName, nil) 928 Expect(err).NotTo(HaveOccurred()) 929 930 response, err = client.Do(req) 931 Expect(err).NotTo(HaveOccurred()) 932 }) 933 934 BeforeEach(func() { 935 fakeWorker = new(dbfakes.FakeWorker) 936 workerName = "some-worker" 937 fakeWorker.NameReturns(workerName) 938 939 fakeAccess.IsAuthenticatedReturns(true) 940 fakeWorker.DeleteReturns(nil) 941 dbWorkerFactory.GetWorkerReturns(fakeWorker, true, nil) 942 }) 943 944 Context("when user is system user", func() { 945 BeforeEach(func() { 946 fakeAccess.IsSystemReturns(true) 947 }) 948 It("deletes the worker from the DB", func() { 949 Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1)) 950 Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName)) 951 952 Expect(fakeWorker.DeleteCallCount()).To(Equal(1)) 953 }) 954 It("returns 200", func() { 955 Expect(response.StatusCode).To(Equal(http.StatusOK)) 956 }) 957 958 Context("when the given worker has already been deleted", func() { 959 BeforeEach(func() { 960 dbWorkerFactory.GetWorkerReturns(nil, false, nil) 961 }) 962 It("returns 500", func() { 963 Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1)) 964 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 965 }) 966 }) 967 968 Context("when deleting the worker fails", func() { 969 var returnedErr error 970 971 BeforeEach(func() { 972 returnedErr = errors.New("some-error") 973 fakeWorker.DeleteReturns(returnedErr) 974 }) 975 976 It("returns 500", func() { 977 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 978 }) 979 }) 980 }) 981 982 Context("when user is admin user", func() { 983 BeforeEach(func() { 984 fakeAccess.IsAdminReturns(true) 985 }) 986 It("deletes the worker from the DB", func() { 987 Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1)) 988 Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName)) 989 990 Expect(fakeWorker.DeleteCallCount()).To(Equal(1)) 991 }) 992 It("returns 200", func() { 993 Expect(response.StatusCode).To(Equal(http.StatusOK)) 994 }) 995 }) 996 997 Context("when user is authorized for team", func() { 998 BeforeEach(func() { 999 fakeWorker.TeamNameReturns("some-team") 1000 fakeAccess.IsAuthorizedReturns(true) 1001 }) 1002 It("deletes the worker from the DB", func() { 1003 Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1)) 1004 Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName)) 1005 1006 Expect(fakeWorker.DeleteCallCount()).To(Equal(1)) 1007 }) 1008 It("returns 200", func() { 1009 Expect(response.StatusCode).To(Equal(http.StatusOK)) 1010 }) 1011 }) 1012 1013 Context("when not authenticated", func() { 1014 BeforeEach(func() { 1015 fakeAccess.IsAuthenticatedReturns(false) 1016 }) 1017 1018 It("returns 401", func() { 1019 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 1020 }) 1021 1022 It("does not attempt to find the worker", func() { 1023 Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero()) 1024 }) 1025 }) 1026 }) 1027}) 1028