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