github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/api/config_test.go (about) 1 package api_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "io/ioutil" 8 "net/http" 9 "time" 10 11 "github.com/pf-qiu/concourse/v6/atc" 12 "github.com/pf-qiu/concourse/v6/atc/creds/noop" 13 "github.com/pf-qiu/concourse/v6/atc/db" 14 "github.com/pf-qiu/concourse/v6/atc/db/dbfakes" 15 . "github.com/pf-qiu/concourse/v6/atc/testhelpers" 16 "github.com/onsi/gomega/gbytes" 17 "github.com/tedsuo/rata" 18 "sigs.k8s.io/yaml" 19 20 // load dummy credential manager 21 _ "github.com/pf-qiu/concourse/v6/atc/creds/dummy" 22 23 . "github.com/onsi/ginkgo" 24 . "github.com/onsi/gomega" 25 ) 26 27 var _ = Describe("Config API", func() { 28 var ( 29 pipelineConfig atc.Config 30 requestGenerator *rata.RequestGenerator 31 ) 32 33 BeforeEach(func() { 34 requestGenerator = rata.NewRequestGenerator(server.URL, atc.Routes) 35 36 pipelineConfig = atc.Config{ 37 Groups: atc.GroupConfigs{ 38 { 39 Name: "some-group", 40 Jobs: []string{"some-job"}, 41 Resources: []string{"some-resource"}, 42 }, 43 }, 44 45 VarSources: atc.VarSourceConfigs{ 46 { 47 Name: "some", 48 Type: "dummy", 49 Config: map[string]interface{}{ 50 "vars": map[string]interface{}{}, 51 }, 52 }, 53 }, 54 55 Resources: atc.ResourceConfigs{ 56 { 57 Name: "some-resource", 58 Type: "some-type", 59 Source: atc.Source{ 60 "source-config": "some-value", 61 }, 62 }, 63 }, 64 65 ResourceTypes: atc.ResourceTypes{ 66 { 67 Name: "custom-resource", 68 Type: "custom-type", 69 Source: atc.Source{"custom": "source"}, 70 Tags: atc.Tags{"some-tag"}, 71 }, 72 }, 73 74 Jobs: atc.JobConfigs{ 75 { 76 Name: "some-job", 77 Public: true, 78 Serial: true, 79 PlanSequence: []atc.Step{ 80 { 81 Config: &atc.GetStep{ 82 Name: "some-input", 83 Resource: "some-resource", 84 Params: atc.Params{"some-param": "some-value"}, 85 }, 86 }, 87 { 88 Config: &atc.TaskStep{ 89 Name: "some-task", 90 Privileged: true, 91 Config: &atc.TaskConfig{ 92 Platform: "linux", 93 RootfsURI: "some-image", 94 Run: atc.TaskRunConfig{ 95 Path: "/path/to/run", 96 }, 97 }, 98 }, 99 }, 100 { 101 Config: &atc.PutStep{ 102 Name: "some-output", 103 Resource: "some-resource", 104 Params: atc.Params{"some-param": "some-value"}, 105 }, 106 }, 107 }, 108 }, 109 }, 110 } 111 }) 112 113 Describe("GET /api/v1/teams/:team_name/pipelines/:name/config", func() { 114 var ( 115 request *http.Request 116 response *http.Response 117 ) 118 119 BeforeEach(func() { 120 var err error 121 request, err = requestGenerator.CreateRequest(atc.GetConfig, rata.Params{ 122 "team_name": "a-team", 123 "pipeline_name": "something-else", 124 }, nil) 125 Expect(err).NotTo(HaveOccurred()) 126 }) 127 128 JustBeforeEach(func() { 129 var err error 130 response, err = client.Do(request) 131 Expect(err).NotTo(HaveOccurred()) 132 }) 133 134 Context("when authorized", func() { 135 BeforeEach(func() { 136 fakeAccess.IsAuthenticatedReturns(true) 137 fakeAccess.IsAuthorizedReturns(true) 138 }) 139 140 Context("when the team is found", func() { 141 var fakeTeam *dbfakes.FakeTeam 142 BeforeEach(func() { 143 fakeTeam = new(dbfakes.FakeTeam) 144 fakeTeam.NameReturns("a-team") 145 dbTeamFactory.FindTeamReturns(fakeTeam, true, nil) 146 }) 147 148 Context("when the pipeline is found", func() { 149 var fakePipeline *dbfakes.FakePipeline 150 BeforeEach(func() { 151 fakePipeline = new(dbfakes.FakePipeline) 152 fakePipeline.NameReturns("something-else") 153 fakePipeline.ConfigVersionReturns(1) 154 fakePipeline.GroupsReturns(atc.GroupConfigs{ 155 { 156 Name: "some-group", 157 Jobs: []string{"some-job"}, 158 Resources: []string{"some-resource"}, 159 }, 160 }) 161 fakePipeline.VarSourcesReturns(atc.VarSourceConfigs{ 162 { 163 Name: "some", 164 Type: "dummy", 165 Config: map[string]interface{}{ 166 "vars": map[string]interface{}{}, 167 }, 168 }, 169 }) 170 fakeTeam.PipelineReturns(fakePipeline, true, nil) 171 }) 172 173 Context("when instance vars ar specified", func() { 174 Context("when instance vars are malformed", func() { 175 BeforeEach(func() { 176 query := request.URL.Query() 177 query.Add("instance_vars", "{") 178 request.URL.RawQuery = query.Encode() 179 }) 180 181 It("returns 400", func() { 182 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 183 }) 184 185 It("returns Content-Type 'application/json'", func() { 186 expectedHeaderEntries := map[string]string{ 187 "Content-Type": "application/json", 188 } 189 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 190 }) 191 192 It("returns an error in the response body", func() { 193 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(` 194 { 195 "errors": [ 196 "instance_vars is malformed: unexpected end of JSON input" 197 ] 198 }`)) 199 }) 200 201 It("doesn't find the pipeline", func() { 202 Expect(dbTeam.PipelineCallCount()).To(Equal(0)) 203 }) 204 }) 205 206 Context("when instance vars is valid", func() { 207 BeforeEach(func() { 208 query := request.URL.Query() 209 query.Add("instance_vars", "{\"branch\":\"feature\"}") 210 request.URL.RawQuery = query.Encode() 211 212 fakePipeline.InstanceVarsReturns(atc.InstanceVars{"branch": "feature"}) 213 }) 214 215 It("finds the pipeline", func() { 216 Expect(fakeTeam.PipelineCallCount()).To(Equal(1)) 217 218 ref := fakeTeam.PipelineArgsForCall(0) 219 Expect(ref).To(Equal(atc.PipelineRef{ 220 Name: "something-else", 221 InstanceVars: atc.InstanceVars{"branch": "feature"}, 222 })) 223 }) 224 }) 225 }) 226 227 Context("when the pipeline config is found", func() { 228 BeforeEach(func() { 229 fakePipeline.ConfigReturns(pipelineConfig, nil) 230 }) 231 232 It("returns 200", func() { 233 Expect(response.StatusCode).To(Equal(http.StatusOK)) 234 }) 235 236 It("returns Content-Type 'application/json' and config version as X-Concourse-Config-Version", func() { 237 expectedHeaderEntries := map[string]string{ 238 "Content-Type": "application/json", 239 atc.ConfigVersionHeader: "1", 240 } 241 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 242 }) 243 244 It("returns the config", func() { 245 var actualConfigResponse atc.ConfigResponse 246 err := json.NewDecoder(response.Body).Decode(&actualConfigResponse) 247 Expect(err).NotTo(HaveOccurred()) 248 249 Expect(actualConfigResponse).To(Equal(atc.ConfigResponse{ 250 Config: pipelineConfig, 251 })) 252 }) 253 254 Context("when finding the config fails", func() { 255 BeforeEach(func() { 256 fakePipeline.ConfigReturns(atc.Config{}, errors.New("fail")) 257 }) 258 259 It("returns 500", func() { 260 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 261 }) 262 }) 263 }) 264 265 Context("when the pipeline is archived", func() { 266 BeforeEach(func() { 267 fakePipeline.ArchivedReturns(true) 268 }) 269 It("returns 404", func() { 270 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 271 }) 272 }) 273 }) 274 275 Context("when the pipeline is not found", func() { 276 BeforeEach(func() { 277 fakeTeam.PipelineReturns(nil, false, nil) 278 }) 279 280 It("returns 404", func() { 281 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 282 }) 283 }) 284 285 Context("when finding the pipeline fails", func() { 286 BeforeEach(func() { 287 fakeTeam.PipelineReturns(nil, false, errors.New("failed")) 288 }) 289 290 It("returns 500", func() { 291 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 292 }) 293 }) 294 }) 295 296 Context("when the team is not found", func() { 297 BeforeEach(func() { 298 dbTeamFactory.FindTeamReturns(nil, false, nil) 299 }) 300 301 It("returns 404", func() { 302 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 303 }) 304 }) 305 306 Context("when finding the team fails", func() { 307 BeforeEach(func() { 308 dbTeamFactory.FindTeamReturns(nil, false, errors.New("failed")) 309 }) 310 311 It("returns 500", func() { 312 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 313 }) 314 }) 315 }) 316 317 Context("when not authenticated", func() { 318 BeforeEach(func() { 319 fakeAccess.IsAuthenticatedReturns(false) 320 }) 321 322 It("returns 401", func() { 323 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 324 }) 325 }) 326 }) 327 328 Describe("PUT /api/v1/teams/:team_name/pipelines/:name/config", func() { 329 var ( 330 request *http.Request 331 response *http.Response 332 ) 333 334 BeforeEach(func() { 335 var err error 336 request, err = requestGenerator.CreateRequest(atc.SaveConfig, rata.Params{ 337 "team_name": "a-team", 338 "pipeline_name": "a-pipeline", 339 }, nil) 340 Expect(err).NotTo(HaveOccurred()) 341 }) 342 343 JustBeforeEach(func() { 344 var err error 345 response, err = client.Do(request) 346 Expect(err).NotTo(HaveOccurred()) 347 }) 348 349 Context("when authorized", func() { 350 BeforeEach(func() { 351 fakeAccess.IsAuthenticatedReturns(true) 352 fakeAccess.IsAuthorizedReturns(true) 353 }) 354 355 Context("when an identifier is invalid", func() { 356 357 BeforeEach(func() { 358 var err error 359 request, err = requestGenerator.CreateRequest(atc.SaveConfig, rata.Params{ 360 "team_name": "_team", 361 "pipeline_name": "_pipeline", 362 }, nil) 363 Expect(err).NotTo(HaveOccurred()) 364 365 request.Header.Set("Content-Type", "application/json") 366 367 payload, err := json.Marshal(pipelineConfig) 368 Expect(err).NotTo(HaveOccurred()) 369 370 request.Body = gbytes.BufferWithBytes(payload) 371 }) 372 373 It("returns warnings in the response body", func() { 374 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(` 375 { 376 "warnings": [ 377 { 378 "type": "invalid_identifier", 379 "message": "pipeline: '_pipeline' is not a valid identifier: must start with a lowercase letter" 380 }, 381 { 382 "type": "invalid_identifier", 383 "message": "team: '_team' is not a valid identifier: must start with a lowercase letter" 384 } 385 ] 386 }`)) 387 }) 388 }) 389 390 Context("when a config version is specified", func() { 391 BeforeEach(func() { 392 request.Header.Set(atc.ConfigVersionHeader, "42") 393 }) 394 395 Context("when the config is malformed", func() { 396 Context("JSON", func() { 397 BeforeEach(func() { 398 request.Header.Set("Content-Type", "application/json") 399 request.Body = gbytes.BufferWithBytes([]byte(`{`)) 400 }) 401 402 It("returns 400", func() { 403 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 404 }) 405 406 It("returns Content-Type 'application/json'", func() { 407 expectedHeaderEntries := map[string]string{ 408 "Content-Type": "application/json", 409 } 410 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 411 }) 412 413 It("returns error JSON", func() { 414 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(` 415 { 416 "errors": [ 417 "malformed config: error converting YAML to JSON: yaml: line 1: did not find expected node content" 418 ] 419 }`)) 420 }) 421 422 It("does not save anything", func() { 423 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 424 }) 425 }) 426 427 Context("YAML", func() { 428 BeforeEach(func() { 429 request.Header.Set("Content-Type", "application/x-yaml") 430 request.Body = gbytes.BufferWithBytes([]byte(`{`)) 431 }) 432 433 It("returns 400", func() { 434 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 435 }) 436 437 It("returns Content-Type 'application/json'", func() { 438 expectedHeaderEntries := map[string]string{ 439 "Content-Type": "application/json", 440 } 441 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 442 }) 443 444 It("returns error JSON", func() { 445 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(` 446 { 447 "errors": [ 448 "malformed config: error converting YAML to JSON: yaml: line 1: did not find expected node content" 449 ] 450 }`)) 451 }) 452 453 It("does not save anything", func() { 454 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 455 }) 456 }) 457 }) 458 459 Context("when the config is valid", func() { 460 Context("JSON", func() { 461 BeforeEach(func() { 462 request.Header.Set("Content-Type", "application/json") 463 464 payload, err := json.Marshal(pipelineConfig) 465 Expect(err).NotTo(HaveOccurred()) 466 467 request.Body = gbytes.BufferWithBytes(payload) 468 }) 469 470 It("returns 200", func() { 471 Expect(response.StatusCode).To(Equal(http.StatusOK)) 472 }) 473 474 It("notifies the scanner to run", func() { 475 Expect(dbTeamFactory.NotifyResourceScannerCallCount()).To(Equal(1)) 476 }) 477 478 It("returns Content-Type 'application/json'", func() { 479 expectedHeaderEntries := map[string]string{ 480 "Content-Type": "application/json", 481 } 482 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 483 }) 484 485 It("saves it initially paused", func() { 486 Expect(dbTeam.SavePipelineCallCount()).To(Equal(1)) 487 488 ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0) 489 Expect(ref.Name).To(Equal("a-pipeline")) 490 Expect(savedConfig).To(Equal(pipelineConfig)) 491 Expect(id).To(Equal(db.ConfigVersion(42))) 492 Expect(initiallyPaused).To(BeTrue()) 493 }) 494 495 Context("and saving it fails", func() { 496 BeforeEach(func() { 497 dbTeam.SavePipelineReturns(nil, false, errors.New("oh no!")) 498 }) 499 500 It("returns 500", func() { 501 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 502 }) 503 504 It("returns the error in the response body", func() { 505 Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("failed to save config: oh no!"))) 506 }) 507 }) 508 509 Context("when it's the first time the pipeline has been created", func() { 510 BeforeEach(func() { 511 returnedPipeline := new(dbfakes.FakePipeline) 512 dbTeam.SavePipelineReturns(returnedPipeline, true, nil) 513 }) 514 515 It("returns 201", func() { 516 Expect(response.StatusCode).To(Equal(http.StatusCreated)) 517 }) 518 519 It("does not notify the scanner to run", func() { 520 Expect(dbTeamFactory.NotifyResourceScannerCallCount()).To(Equal(0)) 521 }) 522 }) 523 524 Context("when the config is invalid", func() { 525 BeforeEach(func() { 526 pipelineConfig.Groups[0].Resources = []string{"missing-resource"} 527 payload, err := json.Marshal(pipelineConfig) 528 Expect(err).NotTo(HaveOccurred()) 529 request.Body = gbytes.BufferWithBytes(payload) 530 }) 531 532 It("returns 400", func() { 533 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 534 }) 535 536 It("returns Content-Type 'application/json'", func() { 537 expectedHeaderEntries := map[string]string{ 538 "Content-Type": "application/json", 539 } 540 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 541 }) 542 543 It("returns error JSON", func() { 544 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(` 545 { 546 "errors": [ 547 "invalid groups:\n\tgroup 'some-group' has unknown resource 'missing-resource'\n" 548 ] 549 }`)) 550 }) 551 552 It("does not save it", func() { 553 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 554 }) 555 }) 556 }) 557 558 Context("YAML", func() { 559 BeforeEach(func() { 560 request.Header.Set("Content-Type", "application/x-yaml") 561 562 payload, err := yaml.Marshal(pipelineConfig) 563 Expect(err).NotTo(HaveOccurred()) 564 565 request.Body = gbytes.BufferWithBytes(payload) 566 }) 567 568 It("returns 200", func() { 569 Expect(response.StatusCode).To(Equal(http.StatusOK)) 570 }) 571 572 It("notifies the scanner to run", func() { 573 Expect(dbTeamFactory.NotifyResourceScannerCallCount()).To(Equal(1)) 574 }) 575 576 It("returns Content-Type 'application/json'", func() { 577 expectedHeaderEntries := map[string]string{ 578 "Content-Type": "application/json", 579 } 580 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 581 }) 582 583 It("saves it initially paused", func() { 584 Expect(dbTeam.SavePipelineCallCount()).To(Equal(1)) 585 586 ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0) 587 Expect(ref.Name).To(Equal("a-pipeline")) 588 Expect(savedConfig).To(Equal(pipelineConfig)) 589 Expect(id).To(Equal(db.ConfigVersion(42))) 590 Expect(initiallyPaused).To(BeTrue()) 591 }) 592 593 Context("when the payload contains suspicious types", func() { 594 BeforeEach(func() { 595 payload := `--- 596 resources: 597 - name: some-resource 598 type: some-type 599 check_every: 10s 600 check_timeout: 1m 601 jobs: 602 - name: some-job 603 plan: 604 - get: some-resource 605 - task: some-task 606 config: 607 platform: linux 608 run: 609 path: ls 610 params: 611 FOO: true 612 BAR: 1 613 BAZ: 1.9` 614 615 request.Header.Set("Content-Type", "application/x-yaml") 616 request.Body = ioutil.NopCloser(bytes.NewBufferString(payload)) 617 }) 618 619 It("returns 200", func() { 620 Expect(response.StatusCode).To(Equal(http.StatusOK)) 621 }) 622 623 It("returns Content-Type 'application/json'", func() { 624 expectedHeaderEntries := map[string]string{ 625 "Content-Type": "application/json", 626 } 627 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 628 }) 629 630 It("saves it", func() { 631 Expect(dbTeam.SavePipelineCallCount()).To(Equal(1)) 632 633 ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0) 634 Expect(ref.Name).To(Equal("a-pipeline")) 635 Expect(savedConfig).To(Equal(atc.Config{ 636 Resources: []atc.ResourceConfig{ 637 { 638 Name: "some-resource", 639 Type: "some-type", 640 Source: nil, 641 CheckEvery: "10s", 642 CheckTimeout: "1m", 643 }, 644 }, 645 Jobs: atc.JobConfigs{ 646 { 647 Name: "some-job", 648 PlanSequence: []atc.Step{ 649 { 650 Config: &atc.GetStep{ 651 Name: "some-resource", 652 }, 653 }, 654 { 655 Config: &atc.TaskStep{ 656 Name: "some-task", 657 Config: &atc.TaskConfig{ 658 Platform: "linux", 659 660 Run: atc.TaskRunConfig{ 661 Path: "ls", 662 }, 663 664 Params: atc.TaskEnv{ 665 "FOO": "true", 666 "BAR": "1", 667 "BAZ": "1.9", 668 }, 669 }, 670 }, 671 }, 672 }, 673 }, 674 }, 675 })) 676 Expect(id).To(Equal(db.ConfigVersion(42))) 677 Expect(initiallyPaused).To(BeTrue()) 678 }) 679 }) 680 681 Describe("test validate cred params when the check_creds param is set in request", func() { 682 var ( 683 payload string 684 ) 685 686 BeforeEach(func() { 687 query := request.URL.Query() 688 query.Add(atc.SaveConfigCheckCreds, "") 689 request.URL.RawQuery = query.Encode() 690 }) 691 692 ExpectCredsValidationPass := func() { 693 Context("when the param exists in creds manager", func() { 694 BeforeEach(func() { 695 fakeSecretManager.GetReturns("this-string-value-doesn't-matter", nil, true, nil) 696 }) 697 698 It("passes validation", func() { 699 Expect(dbTeam.SavePipelineCallCount()).To(Equal(1)) 700 }) 701 702 It("returns 200 ok", func() { 703 Expect(response.StatusCode).To(Equal(http.StatusOK)) 704 }) 705 }) 706 } 707 708 ExpectCredsValidationFail := func() { 709 Context("when the param does not exist in creds manager", func() { 710 BeforeEach(func() { 711 fakeSecretManager.GetReturns(nil, nil, false, nil) 712 }) 713 714 It("fail validation", func() { 715 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 716 }) 717 718 It("returns 400", func() { 719 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 720 }) 721 722 }) 723 } 724 Context("when there is param in resource type config", func() { 725 BeforeEach(func() { 726 payload = `--- 727 resource_types: 728 - name: some-type 729 type: some-base-resource-type 730 source: 731 FOO: ((BAR)) 732 733 jobs: 734 - name: some-job 735 plan: 736 - task: some-task 737 file: some/task/config.yaml` 738 739 request.Header.Set("Content-Type", "application/x-yaml") 740 request.Body = ioutil.NopCloser(bytes.NewBufferString(payload)) 741 }) 742 743 ExpectCredsValidationPass() 744 ExpectCredsValidationFail() 745 }) 746 747 Context("when there is param in resource source config", func() { 748 BeforeEach(func() { 749 payload = `--- 750 resources: 751 - name: some-resource 752 type: some-type 753 source: 754 FOO: ((BAR)) 755 jobs: 756 - name: some-job 757 plan: 758 - get: some-resource` 759 760 request.Header.Set("Content-Type", "application/x-yaml") 761 request.Body = ioutil.NopCloser(bytes.NewBufferString(payload)) 762 }) 763 764 ExpectCredsValidationPass() 765 ExpectCredsValidationFail() 766 }) 767 768 Context("when there is param in resource webhook token", func() { 769 BeforeEach(func() { 770 payload = `--- 771 resources: 772 - name: some-resource 773 type: some-type 774 webhook_token: ((BAR)) 775 jobs: 776 - name: some-job 777 plan: 778 - get: some-resource` 779 780 request.Header.Set("Content-Type", "application/x-yaml") 781 request.Body = ioutil.NopCloser(bytes.NewBufferString(payload)) 782 }) 783 784 ExpectCredsValidationPass() 785 ExpectCredsValidationFail() 786 }) 787 788 Context("when it contains task that uses external config file and params in task params", func() { 789 BeforeEach(func() { 790 payload = `--- 791 resources: 792 - name: some-resource 793 type: some-type 794 check_every: 10s 795 jobs: 796 - name: some-job 797 plan: 798 - get: some-resource 799 - task: some-task 800 file: some-resource/config.yml 801 params: 802 FOO: ((BAR))` 803 804 request.Header.Set("Content-Type", "application/x-yaml") 805 request.Body = ioutil.NopCloser(bytes.NewBufferString(payload)) 806 }) 807 808 ExpectCredsValidationPass() 809 ExpectCredsValidationFail() 810 }) 811 812 Context("when it contains task that uses external config file and params in task vars", func() { 813 BeforeEach(func() { 814 payload = `--- 815 resources: 816 - name: some-resource 817 type: some-type 818 check_every: 10s 819 jobs: 820 - name: some-job 821 plan: 822 - get: some-resource 823 - task: some-task 824 file: some-resource/config.yml 825 vars: 826 FOO: ((BAR))` 827 828 request.Header.Set("Content-Type", "application/x-yaml") 829 request.Body = ioutil.NopCloser(bytes.NewBufferString(payload)) 830 }) 831 832 ExpectCredsValidationPass() 833 ExpectCredsValidationFail() 834 }) 835 836 Context("when it contains nested task that uses external config file and params in task vars", func() { 837 BeforeEach(func() { 838 payload = `--- 839 resources: 840 - name: some-resource 841 type: some-type 842 check_every: 10s 843 jobs: 844 - name: some-job 845 plan: 846 - get: some-resource 847 - do: 848 - task: some-task 849 file: some-resource/config.yml 850 vars: 851 FOO: ((BAR))` 852 853 request.Header.Set("Content-Type", "application/x-yaml") 854 request.Body = ioutil.NopCloser(bytes.NewBufferString(payload)) 855 }) 856 857 ExpectCredsValidationPass() 858 ExpectCredsValidationFail() 859 }) 860 }) 861 862 Context("when it contains credentials to be interpolated", func() { 863 var ( 864 payloadAsConfig atc.Config 865 payload string 866 ) 867 868 BeforeEach(func() { 869 payload = `--- 870 resources: 871 - name: some-resource 872 type: some-type 873 check_every: 10s 874 jobs: 875 - name: some-job 876 plan: 877 - get: some-resource 878 - task: some-task 879 config: 880 platform: linux 881 run: 882 path: ls 883 params: 884 FOO: ((BAR))` 885 payloadAsConfig = atc.Config{Resources: []atc.ResourceConfig{ 886 { 887 Name: "some-resource", 888 Type: "some-type", 889 Source: nil, 890 CheckEvery: "10s", 891 }, 892 }, 893 Jobs: atc.JobConfigs{ 894 { 895 Name: "some-job", 896 PlanSequence: []atc.Step{ 897 { 898 Config: &atc.GetStep{ 899 Name: "some-resource", 900 }, 901 }, 902 { 903 Config: &atc.TaskStep{ 904 Name: "some-task", 905 Config: &atc.TaskConfig{ 906 Platform: "linux", 907 908 Run: atc.TaskRunConfig{ 909 Path: "ls", 910 }, 911 912 Params: atc.TaskEnv{ 913 "FOO": "((BAR))", 914 }, 915 }, 916 }, 917 }, 918 }, 919 }, 920 }, 921 } 922 923 request.Header.Set("Content-Type", "application/x-yaml") 924 request.Body = ioutil.NopCloser(bytes.NewBufferString(payload)) 925 }) 926 927 Context("when the check_creds param is set", func() { 928 BeforeEach(func() { 929 query := request.URL.Query() 930 query.Add(atc.SaveConfigCheckCreds, "") 931 request.URL.RawQuery = query.Encode() 932 }) 933 934 Context("when the credential exists in the credential manager", func() { 935 BeforeEach(func() { 936 fakeSecretManager.GetReturns("this-string-value-doesn't-matter", nil, true, nil) 937 }) 938 939 It("passes validation and saves it un-interpolated", func() { 940 Expect(dbTeam.SavePipelineCallCount()).To(Equal(1)) 941 942 ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0) 943 Expect(ref.Name).To(Equal("a-pipeline")) 944 Expect(savedConfig).To(Equal(payloadAsConfig)) 945 Expect(id).To(Equal(db.ConfigVersion(42))) 946 Expect(initiallyPaused).To(BeTrue()) 947 }) 948 949 It("returns 200", func() { 950 Expect(response.StatusCode).To(Equal(http.StatusOK)) 951 }) 952 }) 953 954 Context("when the credential does not exist in the credential manager", func() { 955 BeforeEach(func() { 956 fakeSecretManager.GetReturns(nil, nil, false, nil) // nil value, nil expiration, not found, no error 957 }) 958 959 It("returns 400", func() { 960 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 961 }) 962 963 It("returns the credential name that was missing", func() { 964 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`{"errors":["credential validation failed\n\n1 error occurred:\n\t* failed to interpolate task config: undefined vars: BAR\n\n"]}`)) 965 }) 966 }) 967 968 Context("when a credentials manager is not used", func() { 969 BeforeEach(func() { 970 fakeSecretManager.GetStub = func(secretPath string) (interface{}, *time.Time, bool, error) { 971 return noop.Noop{}.Get(secretPath) 972 } 973 }) 974 975 It("returns 400", func() { 976 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 977 }) 978 979 It("returns the credential name that was missing", func() { 980 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`{"errors":["credential validation failed\n\n1 error occurred:\n\t* failed to interpolate task config: undefined vars: BAR\n\n"]}`)) 981 }) 982 }) 983 }) 984 985 }) 986 987 Context("when it's the first time the pipeline has been created", func() { 988 BeforeEach(func() { 989 returnedPipeline := new(dbfakes.FakePipeline) 990 dbTeam.SavePipelineReturns(returnedPipeline, true, nil) 991 }) 992 993 It("returns 201", func() { 994 Expect(response.StatusCode).To(Equal(http.StatusCreated)) 995 }) 996 997 It("does not notify the scanner to run", func() { 998 Expect(dbTeamFactory.NotifyResourceScannerCallCount()).To(Equal(0)) 999 }) 1000 }) 1001 1002 Context("and saving it fails", func() { 1003 BeforeEach(func() { 1004 dbTeam.SavePipelineReturns(nil, false, errors.New("oh no!")) 1005 }) 1006 1007 It("returns 500", func() { 1008 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 1009 }) 1010 1011 It("returns the error in the response body", func() { 1012 Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("failed to save config: oh no!"))) 1013 }) 1014 }) 1015 1016 Context("when the config is invalid", func() { 1017 BeforeEach(func() { 1018 pipelineConfig.Groups[0].Resources = []string{"missing-resource"} 1019 payload, err := json.Marshal(pipelineConfig) 1020 Expect(err).NotTo(HaveOccurred()) 1021 request.Body = gbytes.BufferWithBytes(payload) 1022 }) 1023 1024 It("returns 400", func() { 1025 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 1026 }) 1027 1028 It("returns Content-Type 'application/json'", func() { 1029 expectedHeaderEntries := map[string]string{ 1030 "Content-Type": "application/json", 1031 } 1032 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 1033 }) 1034 1035 It("returns error JSON", func() { 1036 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(` 1037 { 1038 "errors": [ 1039 "invalid groups:\n\tgroup 'some-group' has unknown resource 'missing-resource'\n" 1040 ] 1041 }`)) 1042 }) 1043 1044 It("does not save it", func() { 1045 Expect(dbTeam.SavePipelineCallCount()).To(BeZero()) 1046 }) 1047 }) 1048 1049 Context("when instance vars are specified", func() { 1050 Context("when instance vars are malformed", func() { 1051 BeforeEach(func() { 1052 query := request.URL.Query() 1053 query.Add("instance_vars", "{") 1054 request.URL.RawQuery = query.Encode() 1055 }) 1056 1057 It("returns 400", func() { 1058 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 1059 }) 1060 1061 It("returns Content-Type 'application/json'", func() { 1062 expectedHeaderEntries := map[string]string{ 1063 "Content-Type": "application/json", 1064 } 1065 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 1066 }) 1067 1068 It("returns an error in the response body", func() { 1069 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(` 1070 { 1071 "errors": [ 1072 "instance_vars is malformed: unexpected end of JSON input" 1073 ] 1074 }`)) 1075 }) 1076 1077 It("does not save anything", func() { 1078 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 1079 }) 1080 }) 1081 1082 Context("when instance vars is valid", func() { 1083 BeforeEach(func() { 1084 query := request.URL.Query() 1085 query.Add("instance_vars", "{\"branch\":\"feature\"}") 1086 request.URL.RawQuery = query.Encode() 1087 }) 1088 1089 It("saves an instanced pipeline", func() { 1090 Expect(dbTeam.SavePipelineCallCount()).To(Equal(1)) 1091 1092 ref, _, _, _ := dbTeam.SavePipelineArgsForCall(0) 1093 Expect(ref).To(Equal(atc.PipelineRef{ 1094 Name: "a-pipeline", 1095 InstanceVars: atc.InstanceVars{"branch": "feature"}, 1096 })) 1097 }) 1098 }) 1099 }) 1100 }) 1101 1102 Context("there is a problem fetching the team", func() { 1103 BeforeEach(func() { 1104 request.Header.Set("Content-Type", "application/json") 1105 1106 payload, err := json.Marshal(pipelineConfig) 1107 Expect(err).NotTo(HaveOccurred()) 1108 1109 request.Body = gbytes.BufferWithBytes(payload) 1110 }) 1111 1112 Context("when the team is not found", func() { 1113 BeforeEach(func() { 1114 dbTeamFactory.FindTeamReturns(nil, false, nil) 1115 }) 1116 1117 It("returns 404", func() { 1118 Expect(response.StatusCode).To(Equal(http.StatusNotFound)) 1119 }) 1120 }) 1121 1122 Context("when finding the team fails", func() { 1123 BeforeEach(func() { 1124 dbTeamFactory.FindTeamReturns(nil, false, errors.New("failed")) 1125 }) 1126 1127 It("returns 500", func() { 1128 Expect(response.StatusCode).To(Equal(http.StatusInternalServerError)) 1129 }) 1130 }) 1131 }) 1132 1133 }) 1134 1135 Context("when the Content-Type is unsupported", func() { 1136 BeforeEach(func() { 1137 request.Header.Set("Content-Type", "application/x-toml") 1138 1139 payload, err := yaml.Marshal(pipelineConfig) 1140 Expect(err).NotTo(HaveOccurred()) 1141 1142 request.Body = gbytes.BufferWithBytes(payload) 1143 }) 1144 1145 It("returns Unsupported Media Type", func() { 1146 Expect(response.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 1147 }) 1148 1149 It("does not save it", func() { 1150 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 1151 }) 1152 }) 1153 1154 Context("when the config contains extra keys at the toplevel", func() { 1155 BeforeEach(func() { 1156 request.Header.Set("Content-Type", "application/json") 1157 1158 remoraPayload, err := json.Marshal(map[string]interface{}{ 1159 "extra": "noooooo", 1160 1161 "meta": map[string]interface{}{ 1162 "whoa": "lol", 1163 }, 1164 1165 "jobs": []map[string]interface{}{ 1166 { 1167 "name": "some-job", 1168 "public": true, 1169 "plan": []atc.Step{}, 1170 }, 1171 }, 1172 }) 1173 Expect(err).NotTo(HaveOccurred()) 1174 1175 request.Body = gbytes.BufferWithBytes(remoraPayload) 1176 }) 1177 1178 It("returns 200", func() { 1179 Expect(response.StatusCode).To(Equal(http.StatusOK)) 1180 }) 1181 1182 It("returns Content-Type 'application/json'", func() { 1183 expectedHeaderEntries := map[string]string{ 1184 "Content-Type": "application/json", 1185 } 1186 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 1187 }) 1188 1189 It("saves it", func() { 1190 Expect(dbTeam.SavePipelineCallCount()).To(Equal(1)) 1191 1192 ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0) 1193 Expect(ref.Name).To(Equal("a-pipeline")) 1194 Expect(savedConfig).To(Equal(atc.Config{ 1195 Jobs: atc.JobConfigs{ 1196 { 1197 Name: "some-job", 1198 Public: true, 1199 PlanSequence: []atc.Step{}, 1200 }, 1201 }, 1202 })) 1203 Expect(id).To(Equal(db.ConfigVersion(42))) 1204 Expect(initiallyPaused).To(BeTrue()) 1205 }) 1206 }) 1207 1208 Context("when the config contains extra keys nested under a valid key", func() { 1209 BeforeEach(func() { 1210 request.Header.Set("Content-Type", "application/json") 1211 1212 remoraPayload, err := json.Marshal(map[string]interface{}{ 1213 "extra": "noooooo", 1214 1215 "jobs": []map[string]interface{}{ 1216 { 1217 "name": "some-job", 1218 "pubic": true, 1219 "plan": []atc.Step{}, 1220 }, 1221 }, 1222 }) 1223 Expect(err).NotTo(HaveOccurred()) 1224 1225 request.Body = gbytes.BufferWithBytes(remoraPayload) 1226 }) 1227 1228 It("returns 400", func() { 1229 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 1230 }) 1231 1232 It("returns Content-Type 'application/json'", func() { 1233 expectedHeaderEntries := map[string]string{ 1234 "Content-Type": "application/json", 1235 } 1236 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 1237 }) 1238 1239 It("returns an error in the response body", func() { 1240 Expect(ioutil.ReadAll(response.Body)).To(ContainSubstring(`malformed config: error unmarshaling JSON: while decoding JSON: json: unknown field \"pubic\"`)) 1241 }) 1242 1243 It("does not save it", func() { 1244 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 1245 }) 1246 }) 1247 }) 1248 1249 Context("when a config version is malformed", func() { 1250 BeforeEach(func() { 1251 request.Header.Set(atc.ConfigVersionHeader, "forty-two") 1252 }) 1253 1254 It("returns 400", func() { 1255 Expect(response.StatusCode).To(Equal(http.StatusBadRequest)) 1256 }) 1257 1258 It("returns Content-Type 'application/json'", func() { 1259 expectedHeaderEntries := map[string]string{ 1260 "Content-Type": "application/json", 1261 } 1262 Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries)) 1263 }) 1264 1265 It("returns an error in the response body", func() { 1266 Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(` 1267 { 1268 "errors": [ 1269 "config version is malformed: expected integer" 1270 ] 1271 }`)) 1272 }) 1273 1274 It("does not save it", func() { 1275 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 1276 }) 1277 }) 1278 }) 1279 1280 Context("when not authenticated", func() { 1281 BeforeEach(func() { 1282 fakeAccess.IsAuthenticatedReturns(false) 1283 }) 1284 1285 It("returns 401", func() { 1286 Expect(response.StatusCode).To(Equal(http.StatusUnauthorized)) 1287 }) 1288 1289 It("does not save the config", func() { 1290 Expect(dbTeam.SavePipelineCallCount()).To(Equal(0)) 1291 }) 1292 }) 1293 }) 1294 })