github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/api/cloudcontroller/ccv3/buildpack_test.go (about) 1 package ccv3_test 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "mime/multipart" 9 "net/http" 10 "strings" 11 12 "code.cloudfoundry.org/cli/api/cloudcontroller" 13 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/ccv3fakes" 14 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" 15 "code.cloudfoundry.org/cli/api/cloudcontroller/wrapper" 16 "code.cloudfoundry.org/cli/resources" 17 "code.cloudfoundry.org/cli/types" 18 19 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 20 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" 21 . "code.cloudfoundry.org/cli/resources" 22 . "github.com/onsi/ginkgo" 23 . "github.com/onsi/gomega" 24 . "github.com/onsi/gomega/ghttp" 25 ) 26 27 var _ = Describe("Buildpacks", func() { 28 var client *Client 29 30 BeforeEach(func() { 31 client, _ = NewTestClient() 32 }) 33 34 Describe("GetBuildpacks", func() { 35 var ( 36 query Query 37 38 buildpacks []resources.Buildpack 39 warnings Warnings 40 executeErr error 41 ) 42 43 JustBeforeEach(func() { 44 buildpacks, warnings, executeErr = client.GetBuildpacks(query) 45 }) 46 47 When("buildpacks exist", func() { 48 BeforeEach(func() { 49 response1 := fmt.Sprintf(`{ 50 "pagination": { 51 "next": { 52 "href": "%s/v3/buildpacks?names=some-buildpack-name&page=2&per_page=2" 53 } 54 }, 55 "resources": [ 56 { 57 "guid": "guid1", 58 "name": "ruby_buildpack", 59 "state": "AWAITING_UPLOAD", 60 "stack": "windows64", 61 "position": 1, 62 "enabled": true, 63 "locked": false, 64 "metadata": { 65 "labels": {} 66 } 67 }, 68 { 69 "guid": "guid2", 70 "name": "staticfile_buildpack", 71 "state": "AWAITING_UPLOAD", 72 "stack": "cflinuxfs3", 73 "position": 2, 74 "enabled": false, 75 "locked": true, 76 "metadata": { 77 "labels": {} 78 } 79 } 80 ] 81 }`, server.URL()) 82 response2 := `{ 83 "pagination": { 84 "next": null 85 }, 86 "resources": [ 87 { 88 "guid": "guid3", 89 "name": "go_buildpack", 90 "state": "AWAITING_UPLOAD", 91 "stack": "cflinuxfs2", 92 "position": 3, 93 "enabled": true, 94 "locked": false, 95 "metadata": { 96 "labels": {} 97 } 98 } 99 ] 100 }` 101 102 server.AppendHandlers( 103 CombineHandlers( 104 VerifyRequest(http.MethodGet, "/v3/buildpacks", "names=some-buildpack-name"), 105 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 106 ), 107 ) 108 server.AppendHandlers( 109 CombineHandlers( 110 VerifyRequest(http.MethodGet, "/v3/buildpacks", "names=some-buildpack-name&page=2&per_page=2"), 111 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"this is another warning"}}), 112 ), 113 ) 114 115 query = Query{ 116 Key: NameFilter, 117 Values: []string{"some-buildpack-name"}, 118 } 119 }) 120 121 It("returns the queried buildpacks and all warnings", func() { 122 Expect(executeErr).NotTo(HaveOccurred()) 123 124 Expect(buildpacks).To(ConsistOf( 125 Buildpack{ 126 Name: "ruby_buildpack", 127 GUID: "guid1", 128 Position: types.NullInt{Value: 1, IsSet: true}, 129 Enabled: types.NullBool{Value: true, IsSet: true}, 130 Locked: types.NullBool{Value: false, IsSet: true}, 131 Stack: "windows64", 132 State: "AWAITING_UPLOAD", 133 Metadata: &Metadata{Labels: map[string]types.NullString{}}, 134 }, 135 Buildpack{ 136 Name: "staticfile_buildpack", 137 GUID: "guid2", 138 Position: types.NullInt{Value: 2, IsSet: true}, 139 Enabled: types.NullBool{Value: false, IsSet: true}, 140 Locked: types.NullBool{Value: true, IsSet: true}, 141 Stack: "cflinuxfs3", 142 State: "AWAITING_UPLOAD", 143 Metadata: &Metadata{Labels: map[string]types.NullString{}}, 144 }, 145 Buildpack{ 146 Name: "go_buildpack", 147 GUID: "guid3", 148 Position: types.NullInt{Value: 3, IsSet: true}, 149 Enabled: types.NullBool{Value: true, IsSet: true}, 150 Locked: types.NullBool{Value: false, IsSet: true}, 151 Stack: "cflinuxfs2", 152 State: "AWAITING_UPLOAD", 153 Metadata: &Metadata{Labels: map[string]types.NullString{}}, 154 }, 155 )) 156 Expect(warnings).To(ConsistOf("this is a warning", "this is another warning")) 157 }) 158 }) 159 160 When("the cloud controller returns errors and warnings", func() { 161 BeforeEach(func() { 162 response := `{ 163 "errors": [ 164 { 165 "code": 10008, 166 "detail": "The request is semantically invalid: command presence", 167 "title": "CF-UnprocessableEntity" 168 }, 169 { 170 "code": 10010, 171 "detail": "buildpack not found", 172 "title": "CF-buildpackNotFound" 173 } 174 ] 175 }` 176 server.AppendHandlers( 177 CombineHandlers( 178 VerifyRequest(http.MethodGet, "/v3/buildpacks"), 179 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 180 ), 181 ) 182 }) 183 184 It("returns the error and all warnings", func() { 185 Expect(executeErr).To(MatchError(ccerror.MultiError{ 186 ResponseCode: http.StatusTeapot, 187 Errors: []ccerror.V3Error{ 188 { 189 Code: 10008, 190 Detail: "The request is semantically invalid: command presence", 191 Title: "CF-UnprocessableEntity", 192 }, 193 { 194 Code: 10010, 195 Detail: "buildpack not found", 196 Title: "CF-buildpackNotFound", 197 }, 198 }, 199 })) 200 Expect(warnings).To(ConsistOf("this is a warning")) 201 }) 202 }) 203 }) 204 205 Describe("CreateBuildpack", func() { 206 var ( 207 inputBuildpack Buildpack 208 209 bp Buildpack 210 warnings Warnings 211 executeErr error 212 ) 213 214 JustBeforeEach(func() { 215 bp, warnings, executeErr = client.CreateBuildpack(inputBuildpack) 216 }) 217 218 When("the buildpack is successfully created", func() { 219 BeforeEach(func() { 220 inputBuildpack = Buildpack{ 221 Name: "some-buildpack", 222 Stack: "some-stack", 223 } 224 response := `{ 225 "guid": "some-bp-guid", 226 "created_at": "2016-03-18T23:26:46Z", 227 "updated_at": "2016-10-17T20:00:42Z", 228 "name": "some-buildpack", 229 "state": "AWAITING_UPLOAD", 230 "filename": null, 231 "stack": "some-stack", 232 "position": 42, 233 "enabled": true, 234 "locked": false, 235 "links": { 236 "self": { 237 "href": "/v3/buildpacks/some-bp-guid" 238 }, 239 "upload": { 240 "href": "/v3/buildpacks/some-bp-guid/upload", 241 "method": "POST" 242 } 243 } 244 }` 245 246 expectedBody := map[string]interface{}{ 247 "name": "some-buildpack", 248 "stack": "some-stack", 249 } 250 server.AppendHandlers( 251 CombineHandlers( 252 VerifyRequest(http.MethodPost, "/v3/buildpacks"), 253 VerifyJSONRepresenting(expectedBody), 254 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 255 ), 256 ) 257 }) 258 259 It("returns the created buildpack and warnings", func() { 260 Expect(executeErr).NotTo(HaveOccurred()) 261 Expect(warnings).To(ConsistOf("this is a warning")) 262 263 expectedBuildpack := Buildpack{ 264 GUID: "some-bp-guid", 265 Name: "some-buildpack", 266 Stack: "some-stack", 267 Enabled: types.NullBool{Value: true, IsSet: true}, 268 Filename: "", 269 Locked: types.NullBool{Value: false, IsSet: true}, 270 State: constant.BuildpackAwaitingUpload, 271 Position: types.NullInt{Value: 42, IsSet: true}, 272 Links: resources.APILinks{ 273 "upload": resources.APILink{ 274 Method: "POST", 275 HREF: "/v3/buildpacks/some-bp-guid/upload", 276 }, 277 "self": resources.APILink{ 278 HREF: "/v3/buildpacks/some-bp-guid", 279 }, 280 }, 281 } 282 Expect(bp).To(Equal(expectedBuildpack)) 283 }) 284 }) 285 286 When("cc returns back an error or warnings", func() { 287 BeforeEach(func() { 288 inputBuildpack = resources.Buildpack{} 289 response := ` { 290 "errors": [ 291 { 292 "code": 10008, 293 "detail": "The request is semantically invalid: command presence", 294 "title": "CF-UnprocessableEntity" 295 }, 296 { 297 "code": 10010, 298 "detail": "Buildpack not found", 299 "title": "CF-ResourceNotFound" 300 } 301 ] 302 }` 303 server.AppendHandlers( 304 CombineHandlers( 305 VerifyRequest(http.MethodPost, "/v3/buildpacks"), 306 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 307 ), 308 ) 309 }) 310 311 It("returns the error and all warnings", func() { 312 Expect(executeErr).To(MatchError(ccerror.MultiError{ 313 ResponseCode: http.StatusTeapot, 314 Errors: []ccerror.V3Error{ 315 { 316 Code: 10008, 317 Detail: "The request is semantically invalid: command presence", 318 Title: "CF-UnprocessableEntity", 319 }, 320 { 321 Code: 10010, 322 Detail: "Buildpack not found", 323 Title: "CF-ResourceNotFound", 324 }, 325 }, 326 })) 327 Expect(warnings).To(ConsistOf("this is a warning")) 328 }) 329 }) 330 }) 331 332 Describe("UploadBuildpack", func() { 333 var ( 334 jobURL JobURL 335 warnings Warnings 336 executeErr error 337 bpFile io.Reader 338 bpFilePath string 339 bpContent string 340 ) 341 342 BeforeEach(func() { 343 bpContent = "some-content" 344 bpFile = strings.NewReader(bpContent) 345 bpFilePath = "some/fake-buildpack.zip" 346 }) 347 348 JustBeforeEach(func() { 349 jobURL, warnings, executeErr = client.UploadBuildpack("some-buildpack-guid", bpFilePath, bpFile, int64(len(bpContent))) 350 }) 351 352 When("the upload is successful", func() { 353 BeforeEach(func() { 354 response := `{ 355 "metadata": { 356 "guid": "some-buildpack-guid", 357 "url": "/v3/buildpacks/buildpack-guid/upload" 358 }, 359 "entity": { 360 "guid": "some-buildpack-guid", 361 "status": "queued" 362 } 363 }` 364 365 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 366 contentType := req.Header.Get("Content-Type") 367 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 368 369 defer req.Body.Close() 370 requestReader := multipart.NewReader(req.Body, contentType[30:]) 371 372 buildpackPart, err := requestReader.NextPart() 373 Expect(err).NotTo(HaveOccurred()) 374 375 Expect(buildpackPart.FormName()).To(Equal("bits")) 376 Expect(buildpackPart.FileName()).To(Equal("fake-buildpack.zip")) 377 378 defer buildpackPart.Close() 379 partContents, err := ioutil.ReadAll(buildpackPart) 380 Expect(err).ToNot(HaveOccurred()) 381 Expect(string(partContents)).To(Equal(bpContent)) 382 } 383 384 server.AppendHandlers( 385 CombineHandlers( 386 VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"), 387 verifyHeaderAndBody, 388 RespondWith( 389 http.StatusAccepted, 390 response, 391 http.Header{ 392 "X-Cf-Warnings": {"this is a warning"}, 393 "Location": {"http://example.com/job-guid"}, 394 }, 395 ), 396 ), 397 ) 398 }) 399 400 It("returns the processing job URL and warnings", func() { 401 Expect(executeErr).ToNot(HaveOccurred()) 402 Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) 403 Expect(jobURL).To(Equal(JobURL("http://example.com/job-guid"))) 404 }) 405 }) 406 407 When("there is an error reading the buildpack", func() { 408 var ( 409 fakeReader *ccv3fakes.FakeReader 410 expectedErr error 411 ) 412 413 BeforeEach(func() { 414 expectedErr = errors.New("some read error") 415 fakeReader = new(ccv3fakes.FakeReader) 416 fakeReader.ReadReturns(0, expectedErr) 417 bpFile = fakeReader 418 419 server.AppendHandlers( 420 VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"), 421 ) 422 }) 423 424 It("returns the error", func() { 425 Expect(executeErr).To(MatchError(expectedErr)) 426 }) 427 }) 428 429 When("the upload returns an error", func() { 430 BeforeEach(func() { 431 response := `{ 432 "errors": [{ 433 "detail": "The buildpack could not be found: some-buildpack-guid", 434 "title": "CF-ResourceNotFound", 435 "code": 10010 436 }] 437 }` 438 439 server.AppendHandlers( 440 CombineHandlers( 441 VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"), 442 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 443 ), 444 ) 445 }) 446 447 It("returns the error and warnings", func() { 448 Expect(executeErr).To(MatchError( 449 ccerror.ResourceNotFoundError{ 450 Message: "The buildpack could not be found: some-buildpack-guid", 451 }, 452 )) 453 Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) 454 }) 455 }) 456 457 When("a retryable error occurs", func() { 458 BeforeEach(func() { 459 wrapper := &wrapper.CustomWrapper{ 460 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 461 defer GinkgoRecover() // Since this will be running in a thread 462 463 if strings.HasSuffix(request.URL.String(), "/v3/buildpacks/some-buildpack-guid/upload") { 464 _, err := ioutil.ReadAll(request.Body) 465 Expect(err).ToNot(HaveOccurred()) 466 Expect(request.Body.Close()).ToNot(HaveOccurred()) 467 return request.ResetBody() 468 } 469 return connection.Make(request, response) 470 }, 471 } 472 473 client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 474 }) 475 476 It("returns the PipeSeekError", func() { 477 Expect(executeErr).To(MatchError(ccerror.PipeSeekError{})) 478 }) 479 }) 480 481 When("an http error occurs mid-transfer", func() { 482 var expectedErr error 483 484 BeforeEach(func() { 485 expectedErr = errors.New("some read error") 486 487 wrapper := &wrapper.CustomWrapper{ 488 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 489 defer GinkgoRecover() // Since this will be running in a thread 490 491 if strings.HasSuffix(request.URL.String(), "/v3/buildpacks/some-buildpack-guid/upload") { 492 defer request.Body.Close() 493 readBytes, err := ioutil.ReadAll(request.Body) 494 Expect(err).ToNot(HaveOccurred()) 495 Expect(len(readBytes)).To(BeNumerically(">", len(bpContent))) 496 return expectedErr 497 } 498 return connection.Make(request, response) 499 }, 500 } 501 502 client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 503 }) 504 505 It("returns the http error", func() { 506 Expect(executeErr).To(MatchError(expectedErr)) 507 }) 508 }) 509 }) 510 511 Describe("UpdateBuildpack", func() { 512 var ( 513 inputBuildpack resources.Buildpack 514 515 bp resources.Buildpack 516 warnings Warnings 517 executeErr error 518 ) 519 520 JustBeforeEach(func() { 521 bp, warnings, executeErr = client.UpdateBuildpack(inputBuildpack) 522 }) 523 524 When("the buildpack is successfully created", func() { 525 BeforeEach(func() { 526 inputBuildpack = resources.Buildpack{ 527 Name: "some-buildpack", 528 GUID: "some-bp-guid", 529 Stack: "some-stack", 530 Locked: types.NullBool{IsSet: true, Value: true}, 531 } 532 response := `{ 533 "guid": "some-bp-guid", 534 "created_at": "2016-03-18T23:26:46Z", 535 "updated_at": "2016-10-17T20:00:42Z", 536 "name": "some-buildpack", 537 "state": "AWAITING_UPLOAD", 538 "filename": null, 539 "stack": "some-stack", 540 "position": 42, 541 "enabled": true, 542 "locked": true, 543 "links": { 544 "self": { 545 "href": "/v3/buildpacks/some-bp-guid" 546 }, 547 "upload": { 548 "href": "/v3/buildpacks/some-bp-guid/upload", 549 "method": "POST" 550 } 551 } 552 }` 553 554 expectedBody := map[string]interface{}{ 555 "name": "some-buildpack", 556 "stack": "some-stack", 557 "locked": true, 558 } 559 server.AppendHandlers( 560 CombineHandlers( 561 VerifyRequest(http.MethodPatch, "/v3/buildpacks/some-bp-guid"), 562 VerifyJSONRepresenting(expectedBody), 563 RespondWith(http.StatusAccepted, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 564 ), 565 ) 566 }) 567 568 It("returns the created buildpack and warnings", func() { 569 Expect(executeErr).NotTo(HaveOccurred()) 570 Expect(warnings).To(ConsistOf("this is a warning")) 571 572 expectedBuildpack := resources.Buildpack{ 573 GUID: "some-bp-guid", 574 Name: "some-buildpack", 575 Stack: "some-stack", 576 Enabled: types.NullBool{Value: true, IsSet: true}, 577 Filename: "", 578 Locked: types.NullBool{Value: true, IsSet: true}, 579 State: constant.BuildpackAwaitingUpload, 580 Position: types.NullInt{Value: 42, IsSet: true}, 581 Links: resources.APILinks{ 582 "upload": resources.APILink{ 583 Method: "POST", 584 HREF: "/v3/buildpacks/some-bp-guid/upload", 585 }, 586 "self": resources.APILink{ 587 HREF: "/v3/buildpacks/some-bp-guid", 588 }, 589 }, 590 } 591 Expect(bp).To(Equal(expectedBuildpack)) 592 }) 593 }) 594 595 When("cc returns back an error or warnings", func() { 596 BeforeEach(func() { 597 inputBuildpack = resources.Buildpack{ 598 GUID: "some-bp-guid", 599 } 600 response := ` { 601 "errors": [ 602 { 603 "code": 10008, 604 "detail": "The request is semantically invalid: command presence", 605 "title": "CF-UnprocessableEntity" 606 }, 607 { 608 "code": 10010, 609 "detail": "Buildpack not found", 610 "title": "CF-ResourceNotFound" 611 } 612 ] 613 }` 614 server.AppendHandlers( 615 CombineHandlers( 616 VerifyRequest(http.MethodPatch, "/v3/buildpacks/some-bp-guid"), 617 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 618 ), 619 ) 620 }) 621 622 It("returns the error and all warnings", func() { 623 Expect(executeErr).To(MatchError(ccerror.MultiError{ 624 ResponseCode: http.StatusTeapot, 625 Errors: []ccerror.V3Error{ 626 { 627 Code: 10008, 628 Detail: "The request is semantically invalid: command presence", 629 Title: "CF-UnprocessableEntity", 630 }, 631 { 632 Code: 10010, 633 Detail: "Buildpack not found", 634 Title: "CF-ResourceNotFound", 635 }, 636 }, 637 })) 638 Expect(warnings).To(ConsistOf("this is a warning")) 639 }) 640 }) 641 }) 642 643 Describe("DeleteBuildpacks", func() { 644 var ( 645 buildpackGUID = "some-guid" 646 647 jobURL JobURL 648 warnings Warnings 649 executeErr error 650 ) 651 652 JustBeforeEach(func() { 653 jobURL, warnings, executeErr = client.DeleteBuildpack(buildpackGUID) 654 }) 655 656 When("buildpacks exist", func() { 657 BeforeEach(func() { 658 server.AppendHandlers( 659 CombineHandlers( 660 VerifyRequest(http.MethodDelete, "/v3/buildpacks/"+buildpackGUID), 661 RespondWith(http.StatusAccepted, "{}", http.Header{"X-Cf-Warnings": {"this is a warning"}, "Location": {"some-job-url"}}), 662 ), 663 ) 664 }) 665 666 It("returns the delete job URL and all warnings", func() { 667 Expect(executeErr).NotTo(HaveOccurred()) 668 669 Expect(jobURL).To(Equal(JobURL("some-job-url"))) 670 Expect(warnings).To(ConsistOf("this is a warning")) 671 }) 672 }) 673 674 When("the cloud controller returns errors and warnings", func() { 675 BeforeEach(func() { 676 response := `{ 677 "errors": [ 678 { 679 "code": 10008, 680 "detail": "The request is semantically invalid: command presence", 681 "title": "CF-UnprocessableEntity" 682 }, 683 { 684 "code": 10010, 685 "detail": "buildpack not found", 686 "title": "CF-buildpackNotFound" 687 } 688 ] 689 }` 690 server.AppendHandlers( 691 CombineHandlers( 692 VerifyRequest(http.MethodDelete, "/v3/buildpacks/"+buildpackGUID), 693 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 694 ), 695 ) 696 }) 697 698 It("returns the error and all warnings", func() { 699 Expect(executeErr).To(MatchError(ccerror.MultiError{ 700 ResponseCode: http.StatusTeapot, 701 Errors: []ccerror.V3Error{ 702 { 703 Code: 10008, 704 Detail: "The request is semantically invalid: command presence", 705 Title: "CF-UnprocessableEntity", 706 }, 707 { 708 Code: 10010, 709 Detail: "buildpack not found", 710 Title: "CF-buildpackNotFound", 711 }, 712 }, 713 })) 714 Expect(warnings).To(ConsistOf("this is a warning")) 715 }) 716 }) 717 }) 718 })