├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── auditor ├── auditor.go ├── auditor_suite_test.go └── auditor_test.go ├── cf ├── cf.go ├── cf_suite_test.go └── cf_test.go ├── changer ├── changer.go ├── changer_suite_test.go ├── changer_test.go └── mocks_test.go ├── deleter ├── deleter.go ├── deleter_suite_test.go └── deleter_test.go ├── go.mod ├── go.sum ├── integration ├── integration_suite_test.go ├── integration_test.go └── testdata │ ├── does_not_run_on_fs4 │ ├── Gemfile │ ├── Gemfile.lock │ ├── Procfile │ └── app │ ├── does_not_stage_on_fs4 │ ├── Gemfile │ ├── Gemfile.lock │ ├── app.rb │ └── config.ru │ └── simple_app │ ├── Gemfile │ ├── Gemfile.lock │ ├── Procfile │ └── app.rb ├── logo.png ├── main.go ├── mocks ├── cli_connection.go └── mocks.go ├── resources ├── apps.go ├── audit_json.go ├── buildpacks.go ├── droplet.go ├── errors.go ├── orgs.go ├── packages.go ├── spaces.go └── stacks.go ├── scripts ├── all-tests.sh ├── build.sh ├── install.sh ├── integration.sh ├── reinstall.sh ├── uninstall.sh └── unit.sh ├── terminalUI ├── terminalUI.go ├── terminalUI_suite_test.go └── terminalUI_test.go ├── testdata ├── appA.json ├── appB.json ├── apps.json ├── buildpacks.json ├── errorV2.json ├── errorV3.json ├── lifecycleV3Error.json └── spaces.json └── utils └── utils.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | open-pull-requests-limit: 100 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | - package-ecosystem: "github-actions" 10 | open-pull-requests-limit: 100 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | unit-tests: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: go.mod 23 | check-latest: true 24 | 25 | - name: Run Test 26 | run: | 27 | go install github.com/onsi/ginkgo/v2/ginkgo@v2 28 | ginkgo -r -v --label-filter="!integration" 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version_increment: 7 | description: 'Version increment type' 8 | type: choice 9 | options: 10 | - major 11 | - minor 12 | - patch 13 | default: 'patch' 14 | required: true 15 | draft: 16 | description: 'Create draft release' 17 | type: boolean 18 | default: true 19 | required: true 20 | 21 | permissions: 22 | contents: write 23 | packages: read 24 | 25 | jobs: 26 | prepare-release: 27 | runs-on: ubuntu-latest 28 | outputs: 29 | release_version: ${{ steps.get_version.outputs.version }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Get or calculate version 36 | id: get_version 37 | shell: bash 38 | run: | 39 | set -euo pipefail 40 | 41 | git fetch --tags 42 | latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") 43 | current_version=${latest_tag#v} 44 | IFS='.' read -ra version_parts <<< "$current_version" 45 | 46 | case "${{ inputs.version_increment }}" in 47 | major) 48 | new_major=$((version_parts[0] + 1)) 49 | version="v${new_major}.0.0" 50 | ;; 51 | minor) 52 | new_minor=$((version_parts[1] + 1)) 53 | version="v${version_parts[0]}.$new_minor.0" 54 | ;; 55 | patch) 56 | new_patch=$((version_parts[2] + 1)) 57 | version="v${version_parts[0]}.${version_parts[1]}.$new_patch" 58 | ;; 59 | esac 60 | echo "Calculated new version: $version" 61 | 62 | echo "version=$version" >> $GITHUB_OUTPUT 63 | 64 | build: 65 | needs: prepare-release 66 | runs-on: ubuntu-latest 67 | strategy: 68 | fail-fast: true 69 | matrix: 70 | include: 71 | - os: linux 72 | arch: amd64 73 | goos: linux 74 | goarch: amd64 75 | suffix: 64 76 | - os: darwin 77 | arch: amd64 78 | goos: darwin 79 | goarch: amd64 80 | suffix: amd64 81 | - os: darwin 82 | arch: arm64 83 | goos: darwin 84 | goarch: arm64 85 | suffix: arm 86 | - os: windows 87 | arch: amd64 88 | goos: windows 89 | goarch: amd64 90 | suffix: 64 91 | 92 | steps: 93 | - uses: actions/checkout@v4 94 | 95 | - name: Set up Go 96 | uses: actions/setup-go@v5 97 | with: 98 | go-version-file: go.mod 99 | cache: true 100 | check-latest: true 101 | 102 | - name: Build Binary 103 | env: 104 | GOOS: ${{ matrix.goos }} 105 | GOARCH: ${{ matrix.goarch }} 106 | CGO_ENABLED: 0 107 | run: | 108 | output_name="stack-auditor-${{ matrix.os }}-${{ matrix.suffix }}" 109 | git_tag=${{ needs.prepare-release.outputs.release_version }} 110 | version=${git_tag:1} 111 | echo "::group::Building for ${{ matrix.os }}-${{ matrix.arch }}" 112 | go build -v -trimpath \ 113 | -ldflags="-s -w -X main.tagVersion=${version:?}" \ 114 | -o "dist/${output_name}" . 115 | echo "::endgroup::" 116 | 117 | - name: Upload artifact 118 | uses: actions/upload-artifact@v4 119 | with: 120 | name: stack-auditor-${{ matrix.os }}-${{ matrix.suffix }} 121 | path: dist/stack-auditor* 122 | compression-level: 0 123 | 124 | create-release: 125 | needs: [prepare-release, build] 126 | runs-on: ubuntu-latest 127 | env: 128 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 129 | steps: 130 | - uses: actions/checkout@v4 131 | with: 132 | fetch-depth: 0 133 | 134 | - name: Create and push git tag 135 | run: | 136 | git config --global user.name "github-actions[bot]" 137 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 138 | git tag -f "${{ needs.prepare-release.outputs.release_version }}" 139 | git push origin "${{ needs.prepare-release.outputs.release_version }}" --force 140 | 141 | - name: Download all artifacts 142 | uses: actions/download-artifact@v4 143 | with: 144 | path: dist 145 | merge-multiple: true 146 | 147 | - name: Create GitHub Release 148 | run: | 149 | echo "# Changes in this release" > release_notes.md 150 | 151 | draft_flag="${{ github.event.inputs.draft || 'true' }}" 152 | 153 | gh release create "${{ needs.prepare-release.outputs.release_version }}" \ 154 | --draft="$draft_flag" \ 155 | --title="${{ needs.prepare-release.outputs.release_version }}" \ 156 | --notes-file=release_notes.md \ 157 | dist/stack-auditor* 158 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | build/ 4 | stack-auditor 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 2 | 3 | This project is licensed to you under the Apache License, Version 2.0 (the "License"). 4 | You may not use this project except in compliance with the License. 5 | 6 | This project may include a number of subcomponents with separate copyright notices 7 | and license terms. Your use of these subcomponents is subject to the terms and 8 | conditions of the subcomponent's license, as noted in the LICENSE file. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stack Auditor 2 | 3 | ![Stack Auditor Logo](logo.png "Stack Auditor Logo") 4 | 5 | ## Installation 6 | 7 | * Download the latest stack-auditor from the [release section](https://github.com/cloudfoundry/stack-auditor/releases) of this repository for your operating system. 8 | * Install the plugin with `cf install-plugin `. 9 | 10 | ### Alternative: Compile from source 11 | 12 | Prerequisite: Have a working golang environment with correctly set 13 | `GOPATH`. 14 | 15 | ```sh 16 | go get github.com/cloudfoundry/stack-auditor 17 | cd $GOPATH/src/github.com/cloudfoundry/stack-auditor 18 | ./scripts/build.sh 19 | 20 | ``` 21 | 22 | ## Usage 23 | 24 | Install the plugin with `cf install-plugin ` or use the shell scripts `./scripts/install.sh` or `./scripts/reinstall.sh`. 25 | 26 | * Audit cf applications using `cf audit-stack [--csv | --json]`. These optional flags return csv or json format instead of plain text. 27 | * Change stack association using `cf change-stack `. This will attempt to perform a zero downtime restart. Make sure to target the space that contains the app you want to re-associate. 28 | * Delete a stack using `cf delete-stack [--force | -f]` 29 | 30 | ## Run the Tests 31 | 32 | Target a cloudfoundry with the following prerequisites: 33 | - has cflinuxfs3 and cflinuxfs4 stacks and buildpacks 34 | - If using cf-deployment, this can be enabled with the ops file `operations/experimental/add-cflinuxfs4.yml` 35 | - you are targeting an org and a space 36 | 37 | Then run: 38 | 39 | `./scripts/all-tests.sh` 40 | -------------------------------------------------------------------------------- /auditor/auditor.go: -------------------------------------------------------------------------------- 1 | package auditor 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | 8 | "github.com/cloudfoundry/stack-auditor/cf" 9 | ) 10 | 11 | const ( 12 | AuditStackMsg = "Retrieving stack information for all apps...\n\n" 13 | JSONFlag = "json" 14 | CSVFlag = "csv" 15 | ) 16 | 17 | type Auditor struct { 18 | CF cf.CF 19 | OutputType string 20 | } 21 | 22 | func (a *Auditor) Audit() (string, error) { 23 | if a.OutputType == "" { 24 | fmt.Printf(AuditStackMsg) 25 | } 26 | 27 | apps, err := a.CF.GetAppsAndStacks() 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | sort.Sort(apps) 33 | 34 | if a.OutputType == CSVFlag { 35 | return apps.CSV() 36 | } 37 | if a.OutputType == JSONFlag { 38 | json, err := json.Marshal(apps) 39 | if err != nil { 40 | return "", nil 41 | } 42 | return string(json), nil 43 | } 44 | 45 | return fmt.Sprintf("%s", apps), nil 46 | } 47 | -------------------------------------------------------------------------------- /auditor/auditor_suite_test.go: -------------------------------------------------------------------------------- 1 | package auditor_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestAuditor(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Auditor Suite") 13 | } 14 | -------------------------------------------------------------------------------- /auditor/auditor_test.go: -------------------------------------------------------------------------------- 1 | package auditor_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/cloudfoundry/stack-auditor/resources" 8 | 9 | "github.com/cloudfoundry/stack-auditor/auditor" 10 | "github.com/cloudfoundry/stack-auditor/cf" 11 | "github.com/cloudfoundry/stack-auditor/mocks" 12 | "github.com/golang/mock/gomock" 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | const ( 18 | OrgName = "commonOrg" 19 | SpaceName = "commonSpace" 20 | AppAName = "appA" 21 | AppBName = "appB" 22 | AppAPath = OrgName + "/" + SpaceName + "/" + AppAName 23 | AppBPath = OrgName + "/" + SpaceName + "/" + AppBName 24 | StackAName = "stackA" 25 | StackBName = "stackB" 26 | AppAState = "started" 27 | AppBState = "stopped" 28 | ) 29 | 30 | var _ = Describe("Auditor", func() { 31 | var ( 32 | mockCtrl *gomock.Controller 33 | mockConnection *mocks.MockCliConnection 34 | a auditor.Auditor 35 | ) 36 | 37 | BeforeEach(func() { 38 | mockCtrl = gomock.NewController(GinkgoT()) 39 | 40 | mockConnection = mocks.SetupMockCliConnection(mockCtrl) 41 | 42 | a = auditor.Auditor{ 43 | CF: cf.CF{ 44 | Conn: mockConnection, 45 | }, 46 | } 47 | }) 48 | 49 | AfterEach(func() { 50 | mockCtrl.Finish() 51 | }) 52 | 53 | When("running audit-stack", func() { 54 | It("Verify that cf returns the correct stack associations", func() { 55 | result, err := a.Audit() 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | expectedResult := AppAPath + " " + StackAName + " " + AppAState + "\n" + 59 | AppBPath + " " + StackBName + " " + AppBState + "\n" 60 | Expect(result).To(Equal(expectedResult)) 61 | }) 62 | 63 | It("Outputs json format when the used provides the --json flag", func() { 64 | a.OutputType = auditor.JSONFlag 65 | result, err := a.Audit() 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | var apps resources.Apps 69 | apps = append(apps, resources.App{ 70 | Name: AppAName, 71 | Stack: StackAName, 72 | Org: OrgName, 73 | Space: SpaceName, 74 | State: AppAState, 75 | }, 76 | resources.App{ 77 | Name: AppBName, 78 | Stack: StackBName, 79 | Org: OrgName, 80 | Space: SpaceName, 81 | State: AppBState, 82 | }) 83 | 84 | expectedResult, err := json.Marshal(&apps) 85 | Expect(err).NotTo(HaveOccurred()) 86 | 87 | Expect(result).To(Equal(string(expectedResult))) 88 | }) 89 | 90 | It("Outputs csv format when the used provides the --csv flag", func() { 91 | a.OutputType = auditor.CSVFlag 92 | result, err := a.Audit() 93 | Expect(err).NotTo(HaveOccurred()) 94 | 95 | csvFmt := "%s,%s,%s,%s,%s\n" 96 | csvResult := `org,space,name,stack,state 97 | ` + fmt.Sprintf(csvFmt, OrgName, SpaceName, AppAName, StackAName, AppAState) + 98 | fmt.Sprintf(csvFmt, OrgName, SpaceName, AppBName, StackBName, AppBState) 99 | 100 | Expect(result).To(Equal(csvResult)) 101 | }) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /cf/cf.go: -------------------------------------------------------------------------------- 1 | package cf 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "strings" 9 | 10 | plugin_models "code.cloudfoundry.org/cli/plugin/models" 11 | 12 | "code.cloudfoundry.org/cli/plugin" 13 | "github.com/cloudfoundry/stack-auditor/resources" 14 | ) 15 | 16 | type CF struct { 17 | Conn plugin.CliConnection 18 | Space plugin_models.Space 19 | } 20 | 21 | var ( 22 | V2ResultsPerPage = "100" 23 | V3ResultsPerPage = "5000" 24 | ) 25 | 26 | func (cf *CF) GetAppsAndStacks() (resources.Apps, error) { 27 | var entries []resources.App 28 | 29 | orgMap, spaceNameMap, spaceOrgMap, allApps, err := cf.getCFContext() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | for _, appsJSON := range allApps { 35 | for _, app := range appsJSON.Apps { 36 | appName := app.Name 37 | spaceName := spaceNameMap[app.Relationships.Space.Data.GUID] 38 | stackName := app.Lifecycle.Data.Stack 39 | state := strings.ToLower(app.State) 40 | 41 | orgName := orgMap[spaceOrgMap[app.Relationships.Space.Data.GUID]] 42 | entries = append(entries, resources.App{ 43 | Space: spaceName, 44 | Name: appName, 45 | Stack: stackName, 46 | Org: orgName, 47 | State: state, 48 | }) 49 | } 50 | } 51 | return entries, nil 52 | } 53 | 54 | func (cf *CF) GetStackGUID(stackName string) (string, error) { 55 | out, err := cf.Conn.CliCommandWithoutTerminalOutput("stack", "--guid", stackName) 56 | if err != nil { 57 | return "", fmt.Errorf("failed to get GUID of %s", stackName) 58 | } 59 | 60 | if len(out) == 0 { 61 | return "", fmt.Errorf("%s is not a valid stack", stackName) 62 | } 63 | 64 | stackGUID := out[0] 65 | if stackGUID == "" { 66 | return "", fmt.Errorf("%s is not a valid stack", stackName) 67 | } 68 | 69 | return stackGUID, nil 70 | } 71 | 72 | func (cf *CF) GetAllBuildpacks() ([]resources.BuildpacksJSON, error) { 73 | var allBuildpacks []resources.BuildpacksJSON 74 | nextURL := fmt.Sprintf("/v2/buildpacks?results-per-page=%s", V2ResultsPerPage) 75 | for nextURL != "" { 76 | buildpackJSON, err := cf.CFCurl(nextURL) 77 | 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | var buildpacks resources.BuildpacksJSON 83 | 84 | if err := json.Unmarshal([]byte(strings.Join(buildpackJSON, "")), &buildpacks); err != nil { 85 | return nil, fmt.Errorf("error unmarshaling apps json: %v", err) 86 | } 87 | nextURL = buildpacks.NextURL 88 | allBuildpacks = append(allBuildpacks, buildpacks) 89 | } 90 | return allBuildpacks, nil 91 | } 92 | 93 | func (cf *CF) getCFContext() (orgMap, spaceNameMap, spaceOrgMap map[string]string, allApps []resources.V3AppsJSON, err error) { 94 | orgs, err := cf.getOrgs() 95 | if err != nil { 96 | return nil, nil, nil, nil, err 97 | } 98 | 99 | allSpaces, err := cf.getAllSpaces() 100 | if err != nil { 101 | return nil, nil, nil, nil, err 102 | } 103 | 104 | allApps, err = cf.GetAllApps() 105 | if err != nil { 106 | return nil, nil, nil, nil, err 107 | } 108 | 109 | orgMap = orgs.Map() 110 | spaceNameMap, spaceOrgMap = allSpaces.MakeSpaceOrgAndNameMap() 111 | 112 | return orgMap, spaceNameMap, spaceOrgMap, allApps, nil 113 | } 114 | 115 | func (cf *CF) getOrgs() (resources.Orgs, error) { 116 | return cf.Conn.GetOrgs() 117 | } 118 | 119 | func (cf *CF) getAllSpaces() (resources.Spaces, error) { 120 | var allSpaces resources.Spaces 121 | nextSpaceURL := fmt.Sprintf("/v2/spaces?results-per-page=%s", V2ResultsPerPage) 122 | for nextSpaceURL != "" { 123 | spacesJSON, err := cf.CFCurl(nextSpaceURL) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | var spaces resources.SpacesJSON 129 | if strings.Join(spacesJSON, "") == "" { 130 | break 131 | } 132 | if err := json.Unmarshal([]byte(strings.Join(spacesJSON, "")), &spaces); err != nil { 133 | return nil, fmt.Errorf("error unmarshaling spaces json: %v", err) 134 | } 135 | nextSpaceURL = spaces.NextURL 136 | allSpaces = append(allSpaces, spaces) 137 | } 138 | 139 | return allSpaces, nil 140 | } 141 | 142 | func (cf *CF) GetAllApps() ([]resources.V3AppsJSON, error) { 143 | var allApps []resources.V3AppsJSON 144 | nextURL := fmt.Sprintf("/v3/apps?per_page=%s", V3ResultsPerPage) 145 | for nextURL != "" { 146 | appJSON, err := cf.CFCurl(nextURL) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | var apps resources.V3AppsJSON 152 | if strings.Join(appJSON, "") == "" { 153 | break 154 | } 155 | 156 | if err := json.Unmarshal([]byte(strings.Join(appJSON, "")), &apps); err != nil { 157 | return nil, fmt.Errorf("error unmarshaling apps json: %v", appJSON) 158 | } 159 | nextURL = apps.Pagination.Next.Href 160 | allApps = append(allApps, apps) 161 | } 162 | return allApps, nil 163 | } 164 | 165 | func (cf *CF) GetAppByName(appName string) (resources.V3App, error) { 166 | var apps resources.V3AppsJSON 167 | var app resources.V3App 168 | 169 | endpoint := fmt.Sprintf("/v3/apps?names=%s&space_guids=%s", url.QueryEscape(appName), cf.Space.Guid) 170 | appJSON, err := cf.CFCurl(endpoint) 171 | if err != nil { 172 | return app, err 173 | } 174 | 175 | if err := json.Unmarshal([]byte(strings.Join(appJSON, "")), &apps); err != nil { 176 | return app, fmt.Errorf("error unmarshaling apps json: %v", err) 177 | } 178 | if len(apps.Apps) == 0 { 179 | return app, fmt.Errorf("no app found with name %s", appName) 180 | } 181 | 182 | app = apps.Apps[0] 183 | return app, nil 184 | } 185 | 186 | func (cf *CF) GetAppInfo(appName string) (appGuid, appState, appStack string, err error) { 187 | app, err := cf.GetAppByName(appName) 188 | if err != nil { 189 | return "", "", "", err 190 | } 191 | 192 | return app.GUID, app.State, app.Lifecycle.Data.Stack, nil 193 | } 194 | 195 | func (cf *CF) CFCurl(path string, args ...string) ([]string, error) { 196 | u, err := url.Parse(path) 197 | if err != nil { 198 | return nil, err 199 | } 200 | u.Scheme = "" 201 | u.Host = "" 202 | 203 | curlArgs := []string{"curl", u.String()} 204 | curlArgs = append(curlArgs, args...) 205 | output, err := cf.Conn.CliCommandWithoutTerminalOutput(curlArgs...) 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | if err := checkV2Error(output); err != nil { 211 | return nil, err 212 | } 213 | 214 | if err := checkV3Error(output); err != nil { 215 | return nil, err 216 | } 217 | 218 | return output, nil 219 | } 220 | 221 | func checkV2Error(lines []string) error { 222 | output := strings.Join(lines, "\n") 223 | var errorsJSON resources.V2ErrorJSON 224 | 225 | err := json.Unmarshal([]byte(output), &errorsJSON) 226 | 227 | if err != nil || errorsJSON.Description == "" { 228 | return nil 229 | } 230 | 231 | return errors.New(errorsJSON.Description) 232 | } 233 | 234 | func checkV3Error(lines []string) error { 235 | output := strings.Join(lines, "\n") 236 | var errorsJSON resources.V3ErrorJSON 237 | 238 | err := json.Unmarshal([]byte(output), &errorsJSON) 239 | 240 | if err != nil || errorsJSON.Errors == nil { 241 | return nil 242 | 243 | } 244 | 245 | errorDetails := make([]string, 0) 246 | for _, e := range errorsJSON.Errors { 247 | errorDetails = append(errorDetails, e.Detail) 248 | } 249 | 250 | return errors.New(strings.Join(errorDetails, ", ")) 251 | } 252 | -------------------------------------------------------------------------------- /cf/cf_suite_test.go: -------------------------------------------------------------------------------- 1 | package cf_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestCf(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Cf Suite") 13 | } 14 | -------------------------------------------------------------------------------- /cf/cf_test.go: -------------------------------------------------------------------------------- 1 | package cf_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudfoundry/stack-auditor/cf" 7 | 8 | "github.com/cloudfoundry/stack-auditor/mocks" 9 | "github.com/cloudfoundry/stack-auditor/resources" 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("CF", func() { 16 | var ( 17 | mockCtrl *gomock.Controller 18 | mockConnection *mocks.MockCliConnection 19 | c cf.CF 20 | ) 21 | 22 | BeforeEach(func() { 23 | mockCtrl = gomock.NewController(GinkgoT()) 24 | mockConnection = mocks.NewMockCliConnection(mockCtrl) 25 | c = cf.CF{Conn: mockConnection} 26 | }) 27 | 28 | When("getAllApps", func() { 29 | It("performs a successful getAllApps with empty Json", func() { 30 | mockOutput := make([]string, 3) 31 | var allApps []resources.V3AppsJSON 32 | cf.V3ResultsPerPage = "1" 33 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v3/apps?per_page=1")).Return(mockOutput, nil).AnyTimes() 34 | output, err := c.GetAllApps() 35 | Expect(err).NotTo(HaveOccurred()) 36 | Expect(output).To(Equal(allApps)) 37 | }) 38 | }) 39 | 40 | When("CFCurl", func() { 41 | It("performs a successful CF curl", func() { 42 | mockOutput, err := mocks.FileToString("apps.json") 43 | Expect(err).ToNot(HaveOccurred()) 44 | 45 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v3/apps")).Return(mockOutput, nil).AnyTimes() 46 | 47 | output, err := c.CFCurl("/v3/apps") 48 | Expect(err).NotTo(HaveOccurred()) 49 | Expect(output).To(Equal(mockOutput)) 50 | }) 51 | 52 | When("given a fully qualified path", func() { 53 | It("makes it a relative URL", func() { 54 | mockOutput, err := mocks.FileToString("apps.json") 55 | Expect(err).NotTo(HaveOccurred()) 56 | 57 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v3/some-endpoint")).Return(mockOutput, nil).AnyTimes() 58 | 59 | output, err := c.CFCurl("https://api.example.com/v3/some-endpoint") 60 | Expect(err).NotTo(HaveOccurred()) 61 | Expect(output).To(Equal(mockOutput)) 62 | }) 63 | }) 64 | 65 | When("hitting a V3 endpoint and CAPI returns an error JSON", func() { 66 | It("returns the error details in an error", func() { 67 | mockOutput, err := mocks.FileToString("errorV3.json") 68 | Expect(err).NotTo(HaveOccurred()) 69 | 70 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v3/some-endpoint")).Return(mockOutput, nil).AnyTimes() 71 | 72 | _, err = c.CFCurl("/v3/some-endpoint") 73 | Expect(err).To(MatchError("Some V3 error detail, Another V3 error detail")) 74 | }) 75 | }) 76 | 77 | When("hitting a V2 endpoint and CAPI returns an error JSON", func() { 78 | It("returns the error details in an error", func() { 79 | mockOutput, err := mocks.FileToString("errorV2.json") 80 | Expect(err).NotTo(HaveOccurred()) 81 | 82 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v2/some-endpoint")).Return(mockOutput, nil).AnyTimes() 83 | 84 | _, err = c.CFCurl("/v2/some-endpoint") 85 | Expect(err).To(MatchError("Some error description")) 86 | 87 | }) 88 | }) 89 | 90 | When("GetStackGUID", func() { 91 | It("returns an error when given an invalid stack", func() { 92 | invalidStack := "NotAStack" 93 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 94 | "stack", 95 | "--guid", 96 | invalidStack, 97 | ).Return([]string{}, nil) 98 | 99 | _, err := c.GetStackGUID(invalidStack) 100 | Expect(err).To(MatchError(invalidStack + " is not a valid stack")) 101 | }) 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /changer/changer.go: -------------------------------------------------------------------------------- 1 | package changer 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/cloudfoundry/stack-auditor/cf" 8 | ) 9 | 10 | const ( 11 | AttemptingToChangeStackMsg = "Attempting to change stack to %s for %s...\n\n" 12 | ChangeStackSuccessMsg = "Application %s was successfully changed to Stack %s" 13 | AppStackAssociationError = "application is already associated with stack %s" 14 | RestoringStateMsg = "Restoring prior application state: %s" 15 | ErrorChangingStack = "problem assigning target stack to %s" 16 | ErrorRestagingApp = "problem restaging app on %s" 17 | ErrorRestoringState = "problem restoring application state to %s" 18 | ) 19 | 20 | type RequestData struct { 21 | LifeCycle struct { 22 | Data struct { 23 | Stack string `json:"stack"` 24 | } `json:"data"` 25 | } `json:"lifecycle"` 26 | } 27 | 28 | type Changer struct { 29 | CF cf.CF 30 | Runner Runner 31 | Log func(writer io.Writer, msg string) 32 | } 33 | 34 | type Runner interface { 35 | Run(bin, dir string, quiet bool, args ...string) error 36 | RunWithOutput(bin, dir string, quiet bool, args ...string) (string, error) 37 | SetEnv(variableName string, path string) error 38 | } 39 | 40 | func (c *Changer) ChangeStack(appName, newStack string) (string, error) { 41 | fmt.Printf(AttemptingToChangeStackMsg, newStack, fmt.Sprintf("%s/%s/", c.CF.Space.Name, appName)) 42 | appGuid, appState, oldStack, err := c.CF.GetAppInfo(appName) 43 | if err != nil { 44 | return "", err 45 | } 46 | 47 | if oldStack == newStack { 48 | return "", fmt.Errorf(AppStackAssociationError, newStack) 49 | } 50 | 51 | if err := c.change(appName, appGuid, oldStack, newStack, appState); err != nil { 52 | return "", err 53 | } 54 | 55 | return fmt.Sprintf(ChangeStackSuccessMsg, appName, newStack), nil 56 | } 57 | 58 | func (c *Changer) change(appName, appGUID, oldStack, newStack, appInitialState string) error { 59 | err := c.assignTargetStack(appGUID, newStack) 60 | if err != nil { 61 | return fmt.Errorf(ErrorChangingStack+": %w", newStack, err) 62 | } 63 | 64 | err = c.Runner.Run("cf", ".", true, "restage", "--strategy", "rolling", appName) 65 | 66 | if err != nil { 67 | err = fmt.Errorf(ErrorRestagingApp+": %w", newStack, err) 68 | if restartErr := c.assignTargetStack(appGUID, oldStack); restartErr != nil { 69 | err = fmt.Errorf(ErrorChangingStack+": %w", oldStack, err) 70 | } 71 | if restoreErr := c.restoreAppState(appGUID, appInitialState); restoreErr != nil { 72 | err = fmt.Errorf(ErrorRestoringState+": %w", appInitialState, err) 73 | } 74 | return err 75 | } 76 | 77 | return c.restoreAppState(appGUID, appInitialState) 78 | } 79 | 80 | func (c *Changer) assignTargetStack(appGuid, stackName string) error { 81 | _, err := c.CF.CFCurl("/v3/apps/"+appGuid, "-X", "PATCH", `-d={"lifecycle":{"type":"buildpack", "data": {"stack":"`+stackName+`"} } }`) 82 | return err 83 | } 84 | 85 | func (c *Changer) restoreAppState(appGuid, appInitialState string) error { 86 | var action string 87 | 88 | switch appInitialState { 89 | case "STARTED": 90 | action = "start" 91 | case "STOPPED": 92 | action = "stop" 93 | default: 94 | return fmt.Errorf("unhandled initial application state (%s)", appInitialState) 95 | } 96 | 97 | fmt.Println(fmt.Sprintf(RestoringStateMsg, appInitialState)) 98 | _, err := c.CF.CFCurl("/v3/apps/"+appGuid+"/actions/"+action, "-X", "POST") 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /changer/changer_suite_test.go: -------------------------------------------------------------------------------- 1 | package changer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestChanger(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Changer Suite") 13 | } 14 | -------------------------------------------------------------------------------- /changer/changer_test.go: -------------------------------------------------------------------------------- 1 | package changer_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | plugin_models "code.cloudfoundry.org/cli/plugin/models" 9 | 10 | "github.com/cloudfoundry/stack-auditor/cf" 11 | 12 | "github.com/cloudfoundry/stack-auditor/changer" 13 | "github.com/cloudfoundry/stack-auditor/mocks" 14 | 15 | "github.com/golang/mock/gomock" 16 | . "github.com/onsi/ginkgo/v2" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | const ( 21 | AppAName = "appA" 22 | AppAGuid = "appAGuid" 23 | AppBName = "appB" 24 | AppBGuid = "appBGuid" 25 | StackAName = "stackA" 26 | StackBName = "stackB" 27 | ) 28 | 29 | //go:generate mockgen -source=changer.go -destination=mocks_test.go -package=changer_test 30 | 31 | var ( 32 | mockCtrl *gomock.Controller 33 | mockConnection *mocks.MockCliConnection 34 | mockRunner *MockRunner 35 | c changer.Changer 36 | logMsg string 37 | ) 38 | 39 | var _ = Describe("Changer", func() { 40 | BeforeEach(func() { 41 | mockCtrl = gomock.NewController(GinkgoT()) 42 | mockConnection = mocks.SetupMockCliConnection(mockCtrl) 43 | mockRunner = NewMockRunner(mockCtrl) 44 | 45 | c = changer.Changer{ 46 | Runner: mockRunner, 47 | CF: cf.CF{ 48 | Conn: mockConnection, 49 | Space: plugin_models.Space{ 50 | plugin_models.SpaceFields{ 51 | Guid: mocks.SpaceGuid, 52 | Name: mocks.SpaceName, 53 | }, 54 | }, 55 | }, 56 | Log: func(w io.Writer, msg string) { 57 | logMsg = msg 58 | }, 59 | } 60 | 61 | }) 62 | 63 | AfterEach(func() { 64 | mockCtrl.Finish() 65 | }) 66 | 67 | When("running change-stack", func() { 68 | It("starts the app after changing stacks", func() { 69 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 70 | "curl", 71 | "/v3/apps/"+AppAGuid, 72 | "-X", 73 | "PATCH", 74 | `-d={"lifecycle":{"type":"buildpack", "data": {"stack":"`+StackBName+`"} } }`, 75 | ).Return([]string{}, nil) 76 | 77 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 78 | "curl", 79 | "/v3/apps/"+AppAGuid+"/actions/start", 80 | "-X", 81 | "POST", 82 | ) 83 | 84 | mockRunner.EXPECT().Run("cf", ".", true, "restage", "--strategy", "rolling", AppAName) 85 | 86 | result, err := c.ChangeStack(AppAName, StackBName) 87 | Expect(err).NotTo(HaveOccurred()) 88 | Expect(result).To(Equal(fmt.Sprintf(changer.ChangeStackSuccessMsg, AppAName, StackBName))) 89 | }) 90 | 91 | When("there is an error changing stack metadata", func() { 92 | It("returns a useful error message", func() { 93 | errorMsg, err := mocks.FileToString("lifecycleV3Error.json") 94 | Expect(err).ToNot(HaveOccurred()) 95 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 96 | "curl", 97 | "/v3/apps/"+AppAGuid, 98 | "-X", 99 | "PATCH", 100 | `-d={"lifecycle":{"type":"buildpack", "data": {"stack":"`+StackBName+`"} } }`, 101 | ).Return(errorMsg, nil) 102 | 103 | _, err = c.ChangeStack(AppAName, StackBName) 104 | Expect(err).To(HaveOccurred()) 105 | Expect(err.Error()).To(ContainSubstring(changer.ErrorChangingStack, StackBName)) 106 | }) 107 | }) 108 | 109 | When("there is an error changing staging on the new stack", func() { 110 | It("returns a useful error message", func() { 111 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 112 | "curl", 113 | "/v3/apps/"+AppAGuid, 114 | "-X", 115 | "PATCH", 116 | `-d={"lifecycle":{"type":"buildpack", "data": {"stack":"`+StackBName+`"} } }`, 117 | ).Return([]string{}, nil) 118 | 119 | restageError := errors.New("restage failed") 120 | mockRunner.EXPECT().Run("cf", ".", true, "restage", "--strategy", "rolling", AppAName).Return(restageError) 121 | 122 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 123 | "curl", 124 | "/v3/apps/"+AppAGuid, 125 | "-X", 126 | "PATCH", 127 | `-d={"lifecycle":{"type":"buildpack", "data": {"stack":"`+StackAName+`"} } }`, 128 | ).Return([]string{}, nil) 129 | 130 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 131 | "curl", 132 | "/v3/apps/"+AppAGuid+"/actions/start", 133 | "-X", 134 | "POST", 135 | ).Return([]string{}, nil) 136 | 137 | _, err := c.ChangeStack(AppAName, StackBName) 138 | Expect(err).To(HaveOccurred()) 139 | Expect(err.Error()).To(ContainSubstring(changer.ErrorRestagingApp, StackBName)) 140 | }) 141 | 142 | When("the app is stopped", func() { 143 | It("restores the app state", func() { 144 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 145 | "curl", 146 | "/v3/apps/"+AppBGuid, 147 | "-X", 148 | "PATCH", 149 | `-d={"lifecycle":{"type":"buildpack", "data": {"stack":"`+StackAName+`"} } }`, 150 | ).Return([]string{}, nil) 151 | 152 | restageError := errors.New("restage failed") 153 | mockRunner.EXPECT().Run("cf", ".", true, "restage", "--strategy", "rolling", AppBName).Return(restageError) 154 | 155 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 156 | "curl", 157 | "/v3/apps/"+AppBGuid, 158 | "-X", 159 | "PATCH", 160 | `-d={"lifecycle":{"type":"buildpack", "data": {"stack":"`+StackBName+`"} } }`, 161 | ).Return([]string{}, nil) 162 | 163 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput( 164 | "curl", 165 | "/v3/apps/"+AppBGuid+"/actions/stop", 166 | "-X", 167 | "POST", 168 | ).Return([]string{}, nil) 169 | 170 | _, err := c.ChangeStack(AppBName, StackAName) 171 | Expect(err).To(HaveOccurred()) 172 | Expect(err.Error()).To(ContainSubstring(changer.ErrorRestagingApp, StackAName)) 173 | }) 174 | }) 175 | }) 176 | 177 | It("returns an error when given the stack that the app is on", func() { 178 | _, err := c.ChangeStack(AppAName, StackAName) 179 | Expect(err).To(MatchError("application is already associated with stack " + StackAName)) 180 | }) 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /changer/mocks_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: changer.go 3 | 4 | // Package changer_test is a generated GoMock package. 5 | package changer_test 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | ) 11 | 12 | // MockRunner is a mock of Runner interface 13 | type MockRunner struct { 14 | ctrl *gomock.Controller 15 | recorder *MockRunnerMockRecorder 16 | } 17 | 18 | // MockRunnerMockRecorder is the mock recorder for MockRunner 19 | type MockRunnerMockRecorder struct { 20 | mock *MockRunner 21 | } 22 | 23 | // NewMockRunner creates a new mock instance 24 | func NewMockRunner(ctrl *gomock.Controller) *MockRunner { 25 | mock := &MockRunner{ctrl: ctrl} 26 | mock.recorder = &MockRunnerMockRecorder{mock} 27 | return mock 28 | } 29 | 30 | // EXPECT returns an object that allows the caller to indicate expected use 31 | func (m *MockRunner) EXPECT() *MockRunnerMockRecorder { 32 | return m.recorder 33 | } 34 | 35 | // Run mocks base method 36 | func (m *MockRunner) Run(bin, dir string, quiet bool, args ...string) error { 37 | m.ctrl.T.Helper() 38 | varargs := []interface{}{bin, dir, quiet} 39 | for _, a := range args { 40 | varargs = append(varargs, a) 41 | } 42 | ret := m.ctrl.Call(m, "Run", varargs...) 43 | ret0, _ := ret[0].(error) 44 | return ret0 45 | } 46 | 47 | // Run indicates an expected call of Run 48 | func (mr *MockRunnerMockRecorder) Run(bin, dir, quiet interface{}, args ...interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | varargs := append([]interface{}{bin, dir, quiet}, args...) 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockRunner)(nil).Run), varargs...) 52 | } 53 | 54 | // RunWithOutput mocks base method 55 | func (m *MockRunner) RunWithOutput(bin, dir string, quiet bool, args ...string) (string, error) { 56 | m.ctrl.T.Helper() 57 | varargs := []interface{}{bin, dir, quiet} 58 | for _, a := range args { 59 | varargs = append(varargs, a) 60 | } 61 | ret := m.ctrl.Call(m, "RunWithOutput", varargs...) 62 | ret0, _ := ret[0].(string) 63 | ret1, _ := ret[1].(error) 64 | return ret0, ret1 65 | } 66 | 67 | // RunWithOutput indicates an expected call of RunWithOutput 68 | func (mr *MockRunnerMockRecorder) RunWithOutput(bin, dir, quiet interface{}, args ...interface{}) *gomock.Call { 69 | mr.mock.ctrl.T.Helper() 70 | varargs := append([]interface{}{bin, dir, quiet}, args...) 71 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunWithOutput", reflect.TypeOf((*MockRunner)(nil).RunWithOutput), varargs...) 72 | } 73 | 74 | // SetEnv mocks base method 75 | func (m *MockRunner) SetEnv(variableName, path string) error { 76 | m.ctrl.T.Helper() 77 | ret := m.ctrl.Call(m, "SetEnv", variableName, path) 78 | ret0, _ := ret[0].(error) 79 | return ret0 80 | } 81 | 82 | // SetEnv indicates an expected call of SetEnv 83 | func (mr *MockRunnerMockRecorder) SetEnv(variableName, path interface{}) *gomock.Call { 84 | mr.mock.ctrl.T.Helper() 85 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEnv", reflect.TypeOf((*MockRunner)(nil).SetEnv), variableName, path) 86 | } 87 | -------------------------------------------------------------------------------- /deleter/deleter.go: -------------------------------------------------------------------------------- 1 | package deleter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/cloudfoundry/stack-auditor/cf" 9 | ) 10 | 11 | const ( 12 | DeleteStackSuccessMsg = "Stack %s has been deleted" 13 | DeleteStackBuildpackErr = "you still have buildpacks associated to %s. Please use the `cf delete-buildpack` command to remove associated buildpacks and try again" 14 | DeleteStackAppErr = "failed to delete stack %s. You still have apps associated to this stack. Migrate those first." 15 | ) 16 | 17 | type Deleter struct { 18 | CF cf.CF 19 | } 20 | 21 | func (d *Deleter) DeleteStack(stackName string) (string, error) { 22 | if err := d.hasAppAssociation(stackName); err != nil { 23 | return "", err 24 | } 25 | 26 | if err := d.hasBuildpackAssociation(stackName); err != nil { 27 | return "", err 28 | } 29 | 30 | stackGuid, err := d.CF.GetStackGUID(stackName) 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | lines, err := d.CF.CFCurl("/v2/stacks/"+stackGuid, "-X", "DELETE") 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | out := strings.Join(lines, "\n") 41 | if err := checkCurlDelete(out, stackName); err != nil { 42 | return "", err 43 | } 44 | 45 | result := fmt.Sprintf(DeleteStackSuccessMsg, stackName) 46 | return result, nil 47 | } 48 | 49 | func (d *Deleter) hasBuildpackAssociation(stackName string) error { 50 | buildpackMetas, err := d.CF.GetAllBuildpacks() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for _, buildpackMeta := range buildpackMetas { 56 | for _, buildpack := range buildpackMeta.BuildPacks { 57 | if buildpack.Entity.Stack == stackName { 58 | return fmt.Errorf(DeleteStackBuildpackErr, stackName) 59 | } 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (d *Deleter) hasAppAssociation(stackName string) error { 67 | appMetas, err := d.CF.GetAllApps() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | for _, appMeta := range appMetas { 73 | for _, app := range appMeta.Apps { 74 | if app.Lifecycle.Data.Stack == stackName { 75 | return fmt.Errorf(DeleteStackAppErr, stackName) 76 | } 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func checkCurlDelete(out, stackName string) error { 84 | out = strings.Trim(out, " \n") 85 | var curlErr struct { 86 | Description string 87 | ErrorCode string 88 | Code int 89 | } 90 | 91 | isJSON := strings.HasPrefix(out, "{") && strings.HasSuffix(out, "}") 92 | if !isJSON { 93 | return nil 94 | } 95 | 96 | if err := json.Unmarshal([]byte(out), &curlErr); err != nil { 97 | return err 98 | } 99 | 100 | return fmt.Errorf("Failed to delete stack %s with error: %s", stackName, curlErr.Description) 101 | } 102 | -------------------------------------------------------------------------------- /deleter/deleter_suite_test.go: -------------------------------------------------------------------------------- 1 | package deleter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestDeleter(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Deleter Suite") 13 | } 14 | -------------------------------------------------------------------------------- /deleter/deleter_test.go: -------------------------------------------------------------------------------- 1 | package deleter_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudfoundry/stack-auditor/cf" 7 | 8 | "github.com/cloudfoundry/stack-auditor/deleter" 9 | "github.com/cloudfoundry/stack-auditor/mocks" 10 | 11 | "github.com/golang/mock/gomock" 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | const ( 17 | StackEName = "stackE" 18 | StackEGuid = "stackEGuid" 19 | StackCName = "stackC" 20 | InvalidStack = "notarealstack" 21 | ) 22 | 23 | var _ = Describe("Deleter", func() { 24 | var ( 25 | mockCtrl *gomock.Controller 26 | mockConnection *mocks.MockCliConnection 27 | d deleter.Deleter 28 | ) 29 | 30 | BeforeEach(func() { 31 | mockCtrl = gomock.NewController(GinkgoT()) 32 | mockConnection = mocks.SetupMockCliConnection(mockCtrl) 33 | 34 | d = deleter.Deleter{ 35 | CF: cf.CF{ 36 | Conn: mockConnection, 37 | }, 38 | } 39 | }) 40 | 41 | AfterEach(func() { 42 | mockCtrl.Finish() 43 | }) 44 | 45 | When("deleting a stack that no apps are using", func() { 46 | It("deletes the stack", func() { 47 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", "/v2/stacks/"+StackEGuid, "-X", "DELETE").Return([]string{}, nil) 48 | result, err := d.DeleteStack(StackEName) 49 | Expect(err).ToNot(HaveOccurred()) 50 | Expect(result).To(ContainSubstring(fmt.Sprintf("Stack %s has been deleted", StackEName))) 51 | }) 52 | }) 53 | 54 | When("deleting a stack that does not exist", func() { 55 | It("should tell the user the stack is invalid", func() { 56 | _, err := d.DeleteStack(InvalidStack) 57 | Expect(err).To(MatchError(fmt.Sprintf("%s is not a valid stack", InvalidStack))) 58 | }) 59 | }) 60 | 61 | When("deleting a stack that has buildpacks associated with it", func() { 62 | It("should tell the user to the delete the buildpack first", func() { 63 | _, err := d.DeleteStack(StackCName) 64 | Expect(err).To(MatchError(fmt.Sprintf(deleter.DeleteStackBuildpackErr, StackCName))) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudfoundry/stack-auditor 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | code.cloudfoundry.org/cli v7.1.0+incompatible 9 | github.com/cloudfoundry/libbuildpack v0.0.0-20230209225346-0e58f7be61d4 10 | github.com/golang/mock v1.6.0 11 | github.com/onsi/ginkgo/v2 v2.23.4 12 | github.com/onsi/gomega v1.37.0 13 | ) 14 | 15 | require ( 16 | code.cloudfoundry.org/lager v2.0.0+incompatible // indirect 17 | github.com/Masterminds/semver v1.5.0 // indirect 18 | github.com/blang/semver v3.5.1+incompatible // indirect 19 | github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 // indirect 20 | github.com/go-logr/logr v1.4.2 // indirect 21 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 22 | github.com/google/go-cmp v0.7.0 // indirect 23 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 24 | github.com/paketo-buildpacks/packit v1.3.1 // indirect 25 | github.com/pkg/errors v0.9.1 // indirect 26 | github.com/tidwall/gjson v1.12.0 // indirect 27 | github.com/tidwall/match v1.1.1 // indirect 28 | github.com/tidwall/pretty v1.2.0 // indirect 29 | go.uber.org/automaxprocs v1.6.0 // indirect 30 | golang.org/x/net v0.37.0 // indirect 31 | golang.org/x/sys v0.32.0 // indirect 32 | golang.org/x/text v0.23.0 // indirect 33 | golang.org/x/tools v0.31.0 // indirect 34 | gopkg.in/yaml.v2 v2.4.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | code.cloudfoundry.org/cli v7.1.0+incompatible h1:1Zn3I+epQBaBvnZAaTudCQQ0WdqcWtjtjEV9MBZP08Y= 2 | code.cloudfoundry.org/cli v7.1.0+incompatible/go.mod h1:e4d+EpbwevNhyTZKybrLlyTvpH+W22vMsmdmcTxs/Fo= 3 | code.cloudfoundry.org/lager v2.0.0+incompatible h1:WZwDKDB2PLd/oL+USK4b4aEjUymIej9My2nUQ9oWEwQ= 4 | code.cloudfoundry.org/lager v2.0.0+incompatible/go.mod h1:O2sS7gKP3HM2iemG+EnwvyNQK7pTSC6Foi4QiMp9sSk= 5 | github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 6 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 7 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 8 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 9 | github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= 10 | github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= 11 | github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 12 | github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= 13 | github.com/cloudfoundry/libbuildpack v0.0.0-20230209225346-0e58f7be61d4 h1:Z1BUvEdjl5mFb3fQgZcWpbwOp/9JZyAx5RpRHfzE3Ms= 14 | github.com/cloudfoundry/libbuildpack v0.0.0-20230209225346-0e58f7be61d4/go.mod h1:N2yHDHieH8S+EHM2jbhdcKRVASZSL5jEP7hMVHNrQAw= 15 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= 20 | github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= 21 | github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 h1:yY9rWGoXv1U5pl4gxqlULARMQD7x0QG85lqEXTWysik= 22 | github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 23 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= 24 | github.com/elazarl/goproxy/ext v0.0.0-20190911111923-ecfe977594f1 h1:8B7WF1rIoM8H1smfpXFvOawSAzlRDMVzoGu9zE3+OCk= 25 | github.com/elazarl/goproxy/ext v0.0.0-20190911111923-ecfe977594f1/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= 26 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 27 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 28 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 29 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 30 | github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= 31 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 32 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 33 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 34 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 35 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 36 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 37 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 38 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 40 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 41 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 42 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 43 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 44 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 45 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 46 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 47 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 48 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 49 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 52 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 53 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 54 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 55 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 56 | github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= 57 | github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= 58 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 59 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 60 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 61 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 62 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 63 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 64 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 65 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 66 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 67 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 68 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 69 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 70 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 71 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 72 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 73 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 74 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 75 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 76 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 77 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 78 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 79 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 80 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 81 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 82 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 83 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 84 | github.com/paketo-buildpacks/packit v1.3.1 h1:DJAfqsDadRllr/OPYDONxJEOHbYUMWE1NIPPArq4b7w= 85 | github.com/paketo-buildpacks/packit v1.3.1/go.mod h1:v0jVFr3GNcM9JDwwuIAzYNV4Le1L728uMSNhjUybXVA= 86 | github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 87 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 88 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 89 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 90 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 91 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 92 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 93 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 94 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 95 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 96 | github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= 97 | github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= 98 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 99 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 100 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 101 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 102 | github.com/tidwall/gjson v1.12.0 h1:61wEp/qfvFnqKH/WCI3M8HuRut+mHT6Mr82QrFmM2SY= 103 | github.com/tidwall/gjson v1.12.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 104 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 105 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 106 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 107 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 108 | github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 109 | github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 110 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 111 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 112 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 113 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 114 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 115 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 116 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 117 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 118 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 119 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 120 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 121 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 122 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 123 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 124 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 125 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 126 | golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 127 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 128 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 129 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 130 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 131 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 135 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 152 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 153 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 154 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 155 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 156 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 157 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 158 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 159 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 160 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 161 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 162 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 163 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 164 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 165 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 169 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 170 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 171 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 172 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 173 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 174 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 175 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 176 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 177 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 178 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 179 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 180 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= 181 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 182 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 183 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 184 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 185 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 186 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 187 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 188 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 189 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 190 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 191 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 192 | -------------------------------------------------------------------------------- /integration/integration_suite_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestIntegration(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Integration Suite") 13 | } 14 | -------------------------------------------------------------------------------- /integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "sync" 10 | "time" 11 | 12 | "github.com/cloudfoundry/libbuildpack/cutlass" 13 | "github.com/cloudfoundry/stack-auditor/changer" 14 | . "github.com/onsi/ginkgo/v2" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | const ( 19 | oldStack = "cflinuxfs3" 20 | newStack = "cflinuxfs4" 21 | fakeStack = "fakeStack" 22 | fakeBuildpack = "fakeBuildpack" 23 | appBody = "Hello World!" 24 | interval = 100 * time.Millisecond 25 | ) 26 | 27 | var _ = Describe("Integration", Label("integration"), func() { 28 | When("Change Stack", func() { 29 | When("the app was initially started", func() { 30 | It("should change the stack and remain started", func() { 31 | app := cutlass.New(filepath.Join("testdata", "simple_app")) 32 | app.Buildpacks = []string{"https://github.com/cloudfoundry/ruby-buildpack#v1.9.4"} 33 | app.Stack = oldStack 34 | 35 | PushAppAndConfirm(app, true) 36 | defer app.Destroy() 37 | 38 | breaker := make(chan bool) 39 | go confirmZeroDowntime(app, breaker) 40 | 41 | cmd := exec.Command("cf", "change-stack", app.Name, newStack) 42 | output, err := cmd.CombinedOutput() 43 | Expect(err).ToNot(HaveOccurred()) 44 | Expect(string(output)).To(ContainSubstring(changer.RestoringStateMsg, "STARTED")) 45 | close(breaker) 46 | }) 47 | }) 48 | 49 | When("the app was initially stopped", func() { 50 | It("it should change the stack and remain stopped", func() { 51 | app := cutlass.New(filepath.Join("testdata", "simple_app")) 52 | app.Buildpacks = []string{"https://github.com/cloudfoundry/ruby-buildpack#v1.9.4"} 53 | app.Stack = oldStack 54 | 55 | PushAppAndConfirm(app, false) 56 | defer app.Destroy() 57 | 58 | cmd := exec.Command("cf", "change-stack", app.Name, newStack) 59 | out, err := cmd.CombinedOutput() 60 | 61 | Expect(err).ToNot(HaveOccurred(), string(out)) 62 | Expect(string(out)).To(ContainSubstring(changer.RestoringStateMsg, "STOPPED")) 63 | 64 | cmd = exec.Command("cf", "app", app.Name) 65 | contents, err := cmd.CombinedOutput() 66 | Expect(err).NotTo(HaveOccurred()) 67 | Expect(string(contents)).To(ContainSubstring(newStack)) 68 | Expect(string(contents)).To(MatchRegexp(`requested state:\s*stopped`)) 69 | }) 70 | }) 71 | 72 | When("the app cannot stage on the target stack", func() { 73 | It("restarts itself on the old stack", func() { 74 | app := cutlass.New(filepath.Join("testdata", "does_not_stage_on_fs4")) 75 | app.Buildpacks = []string{"https://github.com/cloudfoundry/ruby-buildpack#v1.9.4"} 76 | app.Stack = oldStack 77 | 78 | PushAppAndConfirm(app, true) 79 | defer app.Destroy() 80 | 81 | breaker := make(chan bool) 82 | go confirmZeroDowntime(app, breaker) 83 | 84 | cmd := exec.Command("cf", "change-stack", app.Name, newStack) 85 | out, err := cmd.CombinedOutput() 86 | Expect(err).To(HaveOccurred(), string(out)) 87 | Expect(string(out)).To(ContainSubstring(changer.ErrorRestagingApp, newStack)) 88 | 89 | // need to do this because change-stack execution completes while the app is still starting up, otherwise there's a 404 90 | Eventually(func() (string, error) { return app.GetBody("/") }, 3*time.Minute).Should(ContainSubstring(appBody)) 91 | close(breaker) 92 | }) 93 | }) 94 | 95 | When("the app cannot run on the target stack", func() { 96 | It("restarts itself on the old stack", func() { 97 | app := cutlass.New(filepath.Join("testdata", "does_not_run_on_fs4")) 98 | app.Buildpacks = []string{"https://github.com/cloudfoundry/ruby-buildpack#v1.9.4"} 99 | app.Stack = oldStack 100 | 101 | PushAppAndConfirm(app, true) 102 | defer app.Destroy() 103 | 104 | breaker := make(chan bool) 105 | go confirmZeroDowntime(app, breaker) 106 | 107 | cmd := exec.Command("cf", "change-stack", app.Name, newStack) 108 | out, err := cmd.CombinedOutput() 109 | Expect(err).To(HaveOccurred(), string(out)) 110 | Expect(string(out)).To(ContainSubstring(changer.ErrorRestagingApp, newStack)) 111 | 112 | // need to do this because change-stack execution completes while the app is still starting up, otherwise there's a 404 113 | Eventually(func() (string, error) { 114 | return app.GetBody("/") 115 | }, 3*time.Minute).Should(ContainSubstring(appBody)) 116 | close(breaker) 117 | }) 118 | }) 119 | }) 120 | 121 | When("Audit Stack", func() { 122 | // 2 apps forces pagination if integration test binary is built which forces per page to 1. See ./script/build.sh 123 | const appCount = 2 124 | var ( 125 | apps [appCount]*cutlass.App 126 | spaceName, orgName string 127 | err error 128 | stacks = []string{oldStack, newStack} 129 | ) 130 | 131 | BeforeEach(func() { 132 | orgName, spaceName, err = GetOrgAndSpace() 133 | Expect(err).ToNot(HaveOccurred()) 134 | 135 | wg := sync.WaitGroup{} 136 | wg.Add(appCount) 137 | for i := 0; i < appCount; i++ { 138 | apps[i] = cutlass.New(filepath.Join("testdata", "simple_app")) 139 | apps[i].Stack = stacks[i%len(stacks)] 140 | apps[i].Buildpacks = []string{"https://github.com/cloudfoundry/ruby-buildpack#v1.9.4"} 141 | 142 | go func(i int) { // Maybe use a worker pool to not bombard our api 143 | defer wg.Done() 144 | PushAppAndConfirm(apps[i], true) 145 | }(i) 146 | } 147 | wg.Wait() 148 | }) 149 | 150 | AfterEach(func() { 151 | for _, app := range apps { 152 | Expect(app.Destroy()).To(Succeed()) 153 | } 154 | cmd := exec.Command("cf", "delete-orphaned-routes", "-f") 155 | Expect(cmd.Run()).To(Succeed()) 156 | }) 157 | 158 | It("prints all apps with their orgs spaces and stacks", func() { 159 | cmd := exec.Command("cf", "audit-stack") 160 | output, err := cmd.Output() 161 | Expect(err).NotTo(HaveOccurred()) 162 | 163 | for i, app := range apps { 164 | Expect(string(output)).To(ContainSubstring(fmt.Sprintf("%s/%s/%s %s", orgName, spaceName, app.Name, stacks[i%len(stacks)]))) 165 | } 166 | }) 167 | }) 168 | 169 | When("Delete Stack", func() { 170 | BeforeEach(func() { 171 | Expect(CreateStack(fakeStack, "fake stack")).To(Succeed()) 172 | }) 173 | 174 | It("should delete the stack", func() { 175 | cmd := exec.Command("cf", "delete-stack", fakeStack, "-f") 176 | output, err := cmd.CombinedOutput() 177 | Expect(err).NotTo(HaveOccurred(), string(output)) 178 | Expect(string(output)).To(ContainSubstring(fmt.Sprintf("%s has been deleted", fakeStack))) 179 | }) 180 | 181 | When("an app is using the stack", func() { 182 | var ( 183 | app *cutlass.App 184 | ) 185 | 186 | BeforeEach(func() { 187 | app = cutlass.New(filepath.Join("testdata", "simple_app")) 188 | app.Buildpacks = []string{"https://github.com/cloudfoundry/ruby-buildpack#v1.9.4"} 189 | app.Stack = oldStack 190 | }) 191 | 192 | AfterEach(func() { 193 | if app != nil { 194 | Expect(app.Destroy()).To(Succeed()) 195 | } 196 | app = nil 197 | }) 198 | 199 | It("fails to delete the stack", func() { 200 | PushAppAndConfirm(app, true) 201 | cmd := exec.Command("cf", "delete-stack", oldStack, "-f") 202 | out, err := cmd.CombinedOutput() 203 | Expect(err).To(HaveOccurred()) 204 | Expect(string(out)).To(ContainSubstring("failed to delete stack " + oldStack)) 205 | }) 206 | }) 207 | 208 | When("a buildpack is using the stack", func() { 209 | BeforeEach(func() { 210 | Expect(CreateBuildpack(fakeBuildpack, fakeStack)).To(Succeed()) 211 | }) 212 | 213 | AfterEach(func() { 214 | cmd := exec.Command("cf", "delete-buildpack", fakeBuildpack, "-f") 215 | Expect(cmd.Run()).To(Succeed()) 216 | cmd = exec.Command("cf", "delete-stack", fakeStack, "-f") 217 | Expect(cmd.Run()).To(Succeed()) 218 | }) 219 | 220 | It("fails to delete the stack", func() { 221 | cmd := exec.Command("cf", "delete-stack", fakeStack, "-f") 222 | out, err := cmd.CombinedOutput() 223 | Expect(err).To(HaveOccurred()) 224 | Expect(string(out)).To(ContainSubstring("you still have buildpacks associated to " + fakeStack)) 225 | }) 226 | }) 227 | }) 228 | }) 229 | 230 | func PushAppAndConfirm(app *cutlass.App, start bool) { 231 | Expect(app.Push()).To(Succeed(), fmt.Sprintf("Name: %v", app)) 232 | Eventually(func() ([]string, error) { return app.InstanceStates() }, 20*time.Second).Should(Equal([]string{"RUNNING"})) 233 | 234 | if !start { 235 | cmd := exec.Command("cf", "stop", app.Name) 236 | Expect(cmd.Run()).To(Succeed()) 237 | } 238 | } 239 | 240 | func GetOrgAndSpace() (string, string, error) { 241 | cfHome := os.Getenv("CF_HOME") 242 | if cfHome == "" { 243 | cfHome = os.Getenv("HOME") 244 | } 245 | bytes, err := os.ReadFile(filepath.Join(cfHome, ".cf", "config.json")) 246 | if err != nil { 247 | return "", "", err 248 | } 249 | 250 | var configData struct { 251 | SpaceFields struct { 252 | Name string 253 | } 254 | OrganizationFields struct { 255 | Name string 256 | } 257 | } 258 | 259 | if err := json.Unmarshal(bytes, &configData); err != nil { 260 | return "", "", err 261 | } 262 | return configData.OrganizationFields.Name, configData.SpaceFields.Name, nil 263 | } 264 | 265 | func CreateStack(stackName, description string) error { 266 | data := fmt.Sprintf(`{"name":"%s", "description":"%s"}`, stackName, description) 267 | cmd := exec.Command("cf", "curl", "/v2/stacks", "-X", "POST", "-d", data) 268 | 269 | return cmd.Run() 270 | } 271 | 272 | func CreateBuildpack(buildpackName, stackName string) error { 273 | data := fmt.Sprintf(`{"name":"%s", "stack":"%s"}`, buildpackName, stackName) 274 | cmd := exec.Command("cf", "curl", "/v2/buildpacks", "-X", "POST", "-d", data) 275 | 276 | return cmd.Run() 277 | } 278 | 279 | func confirmZeroDowntime(app *cutlass.App, breaker chan bool) { 280 | defer GinkgoRecover() 281 | for { 282 | select { 283 | case <-breaker: 284 | return 285 | default: 286 | body, err := app.GetBody("/") 287 | Expect(err).NotTo(HaveOccurred()) 288 | Expect(body).To(Equal(appBody)) 289 | time.Sleep(interval) 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /integration/testdata/does_not_run_on_fs4/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem 'webrick' 6 | # gem "rails" 7 | -------------------------------------------------------------------------------- /integration/testdata/does_not_run_on_fs4/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | webrick (1.8.1) 5 | 6 | PLATFORMS 7 | x86_64-linux 8 | 9 | DEPENDENCIES 10 | webrick 11 | 12 | BUNDLED WITH 13 | 2.4.10 14 | -------------------------------------------------------------------------------- /integration/testdata/does_not_run_on_fs4/Procfile: -------------------------------------------------------------------------------- 1 | web: ruby ./app 2 | -------------------------------------------------------------------------------- /integration/testdata/does_not_run_on_fs4/app: -------------------------------------------------------------------------------- 1 | require 'webrick' 2 | 3 | raise unless File.readlines('/etc/os-release').grep(/Bionic/).any? 4 | 5 | server = WEBrick::HTTPServer.new :Port => ENV['PORT'] 6 | 7 | server.mount_proc '/' do |request, response| 8 | response.body = 'Hello World!' 9 | end 10 | 11 | trap 'INT' do server.shutdown end 12 | 13 | server.start 14 | -------------------------------------------------------------------------------- /integration/testdata/does_not_stage_on_fs4/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "sinatra" 3 | 4 | ruby '2.7.6' 5 | -------------------------------------------------------------------------------- /integration/testdata/does_not_stage_on_fs4/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | mustermann (3.0.0) 5 | ruby2_keywords (~> 0.0.1) 6 | rack (2.2.6.2) 7 | rack-protection (3.0.5) 8 | rack 9 | ruby2_keywords (0.0.5) 10 | sinatra (3.0.5) 11 | mustermann (~> 3.0) 12 | rack (~> 2.2, >= 2.2.4) 13 | rack-protection (= 3.0.5) 14 | tilt (~> 2.0) 15 | tilt (2.0.11) 16 | 17 | PLATFORMS 18 | ruby 19 | 20 | DEPENDENCIES 21 | sinatra 22 | 23 | RUBY VERSION 24 | ruby 2.7.6p219 25 | 26 | BUNDLED WITH 27 | 2.1.4 28 | -------------------------------------------------------------------------------- /integration/testdata/does_not_stage_on_fs4/app.rb: -------------------------------------------------------------------------------- 1 | require "sinatra" 2 | 3 | get "/" do 4 | return "Hello World!" 5 | end 6 | -------------------------------------------------------------------------------- /integration/testdata/does_not_stage_on_fs4/config.ru: -------------------------------------------------------------------------------- 1 | require './app' 2 | run Sinatra::Application 3 | -------------------------------------------------------------------------------- /integration/testdata/simple_app/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "webrick" 6 | # gem "rails" 7 | -------------------------------------------------------------------------------- /integration/testdata/simple_app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | webrick (1.8.1) 5 | 6 | PLATFORMS 7 | x86_64-linux 8 | 9 | DEPENDENCIES 10 | webrick 11 | 12 | BUNDLED WITH 13 | 2.4.10 14 | -------------------------------------------------------------------------------- /integration/testdata/simple_app/Procfile: -------------------------------------------------------------------------------- 1 | web: ruby ./app.rb 2 | -------------------------------------------------------------------------------- /integration/testdata/simple_app/app.rb: -------------------------------------------------------------------------------- 1 | require 'webrick' 2 | 3 | server = WEBrick::HTTPServer.new :Port => ENV['PORT'] 4 | 5 | server.mount_proc '/' do |request, response| 6 | response.body = 'Hello World!' 7 | end 8 | 9 | trap 'INT' do server.shutdown end 10 | 11 | server.start 12 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/stack-auditor/d426c7fe1015bc3c4d7857b3b3e5de2c5248466f/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/cloudfoundry/stack-auditor/auditor" 11 | "github.com/cloudfoundry/stack-auditor/cf" 12 | "github.com/cloudfoundry/stack-auditor/changer" 13 | "github.com/cloudfoundry/stack-auditor/deleter" 14 | "github.com/cloudfoundry/stack-auditor/terminalUI" 15 | "github.com/cloudfoundry/stack-auditor/utils" 16 | 17 | "code.cloudfoundry.org/cli/plugin" 18 | ) 19 | 20 | var tagVersion = "0.0.5" 21 | var pluginVersion plugin.VersionType 22 | 23 | type StackAuditor struct { 24 | UI terminalUI.UIController 25 | } 26 | 27 | func init() { 28 | var major, minor, patch int 29 | 30 | _, err := fmt.Sscanf(tagVersion, "%d.%d.%d", &major, &minor, &patch) 31 | if err != nil { 32 | err := errors.New("problem setting version") 33 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 34 | os.Exit(1) 35 | } 36 | 37 | pluginVersion = plugin.VersionType{ 38 | Major: major, 39 | Minor: minor, 40 | Build: patch, 41 | } 42 | } 43 | 44 | const ( 45 | AuditStackCmd = "audit-stack" 46 | ChangeStackCmd = "change-stack" 47 | DeleteStackCmd = "delete-stack" 48 | ChangeStackUsage = "Usage: cf change-stack " 49 | AuditStackUsage = "Usage: cf audit-stack [--json | --csv]" 50 | ErrorMsg = "a problem occurred: %v\n" 51 | IncorrectArguments = "Incorrect arguments provided - %s\n" 52 | ) 53 | 54 | func main() { 55 | stackAuditor := StackAuditor{ 56 | UI: terminalUI.NewUi(), 57 | } 58 | plugin.Start(&stackAuditor) 59 | } 60 | 61 | func (s *StackAuditor) Run(cliConnection plugin.CliConnection, args []string) { 62 | if len(args) == 0 { 63 | err := errors.New("no command line arguments provided") 64 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 65 | os.Exit(1) 66 | } 67 | 68 | switch args[0] { 69 | case AuditStackCmd: 70 | a := auditor.Auditor{ 71 | CF: cf.CF{ 72 | Conn: cliConnection, 73 | }, 74 | } 75 | 76 | if len(args) > 2 { 77 | log.Fatalf(IncorrectArguments, AuditStackUsage) 78 | } 79 | 80 | if len(args) > 1 && args[1] == "--json" { 81 | a.OutputType = auditor.JSONFlag 82 | } else if len(args) > 1 && args[1] == "--csv" { 83 | a.OutputType = auditor.CSVFlag 84 | } else if len(args) > 1 { 85 | log.Fatalf(IncorrectArguments, AuditStackUsage) 86 | } 87 | 88 | info, err := a.Audit() 89 | if err != nil { 90 | log.Fatalf(ErrorMsg, err) 91 | } 92 | fmt.Println(info) 93 | 94 | case DeleteStackCmd: 95 | if len(args) < 2 { 96 | err := errors.New("Incorrect number of arguments provided - Usage: cf delete-stack ") 97 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 98 | os.Exit(1) 99 | } 100 | 101 | forceFlag := len(args) > 2 && (args[2] == "--force" || args[2] == "-f") 102 | 103 | if !forceFlag && !s.UI.ConfirmDelete(args[1]) { 104 | os.Exit(1) 105 | } 106 | 107 | a := deleter.Deleter{ 108 | CF: cf.CF{ 109 | Conn: cliConnection, 110 | }, 111 | } 112 | info, err := a.DeleteStack(args[1]) 113 | if err != nil { 114 | log.Fatalf(ErrorMsg, err) 115 | } 116 | fmt.Println(info) 117 | 118 | case ChangeStackCmd: 119 | if len(args) != 3 && len(args) != 4 { 120 | log.Fatalf("Incorrect arguments provided - %s\n", ChangeStackUsage) 121 | } 122 | 123 | c := changer.Changer{ 124 | Log: func(w io.Writer, msg string) { 125 | w.Write([]byte(msg)) 126 | }, 127 | } 128 | 129 | c.Runner = utils.Command{} 130 | 131 | c.CF = cf.CF{ 132 | Conn: cliConnection, 133 | } 134 | space, err := c.CF.Conn.GetCurrentSpace() 135 | if err != nil { 136 | log.Fatalf(ErrorMsg, err) 137 | } 138 | c.CF.Space = space 139 | 140 | info, err := c.ChangeStack(args[1], args[2]) 141 | if err != nil { 142 | log.Fatalf(ErrorMsg, err) 143 | } 144 | fmt.Println(info) 145 | 146 | case "CLI-MESSAGE-UNINSTALL": 147 | os.Exit(0) 148 | default: 149 | fmt.Fprintln(os.Stderr, "Unknown argument provided") 150 | os.Exit(17) 151 | } 152 | } 153 | 154 | func (s *StackAuditor) GetMetadata() plugin.PluginMetadata { 155 | return plugin.PluginMetadata{ 156 | Name: "StackAuditor", 157 | Version: pluginVersion, 158 | MinCliVersion: plugin.VersionType{ 159 | Major: 7, 160 | Minor: 0, 161 | Build: 0, 162 | }, 163 | Commands: []plugin.Command{ 164 | { 165 | Name: AuditStackCmd, 166 | HelpText: "List all apps with their stacks, orgs, and spaces", 167 | 168 | UsageDetails: plugin.Usage{ 169 | Options: map[string]string{ 170 | "-csv": fmt.Sprintf("output results in csv format"), 171 | "-json": fmt.Sprintf("output results in json format"), 172 | }, 173 | Usage: AuditStackUsage, 174 | }, 175 | }, 176 | { 177 | Name: DeleteStackCmd, 178 | HelpText: "Delete a stack from the foundation", 179 | 180 | UsageDetails: plugin.Usage{ 181 | Usage: fmt.Sprintf("cf %s STACK_NAME", DeleteStackCmd), 182 | }, 183 | }, 184 | { 185 | Name: ChangeStackCmd, 186 | HelpText: "Change an app's stack in the current space and restart the app", 187 | 188 | UsageDetails: plugin.Usage{ 189 | Usage: ChangeStackUsage, 190 | }, 191 | }, 192 | }, 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /mocks/cli_connection.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: code.cloudfoundry.org/cli/plugin (interfaces: CliConnection) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | models "code.cloudfoundry.org/cli/plugin/models" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockCliConnection is a mock of CliConnection interface 14 | type MockCliConnection struct { 15 | ctrl *gomock.Controller 16 | recorder *MockCliConnectionMockRecorder 17 | } 18 | 19 | // MockCliConnectionMockRecorder is the mock recorder for MockCliConnection 20 | type MockCliConnectionMockRecorder struct { 21 | mock *MockCliConnection 22 | } 23 | 24 | // NewMockCliConnection creates a new mock instance 25 | func NewMockCliConnection(ctrl *gomock.Controller) *MockCliConnection { 26 | mock := &MockCliConnection{ctrl: ctrl} 27 | mock.recorder = &MockCliConnectionMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockCliConnection) EXPECT() *MockCliConnectionMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // AccessToken mocks base method 37 | func (m *MockCliConnection) AccessToken() (string, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "AccessToken") 40 | ret0, _ := ret[0].(string) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // AccessToken indicates an expected call of AccessToken 46 | func (mr *MockCliConnectionMockRecorder) AccessToken() *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessToken", reflect.TypeOf((*MockCliConnection)(nil).AccessToken)) 49 | } 50 | 51 | // ApiEndpoint mocks base method 52 | func (m *MockCliConnection) ApiEndpoint() (string, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "ApiEndpoint") 55 | ret0, _ := ret[0].(string) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // ApiEndpoint indicates an expected call of ApiEndpoint 61 | func (mr *MockCliConnectionMockRecorder) ApiEndpoint() *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApiEndpoint", reflect.TypeOf((*MockCliConnection)(nil).ApiEndpoint)) 64 | } 65 | 66 | // ApiVersion mocks base method 67 | func (m *MockCliConnection) ApiVersion() (string, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "ApiVersion") 70 | ret0, _ := ret[0].(string) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // ApiVersion indicates an expected call of ApiVersion 76 | func (mr *MockCliConnectionMockRecorder) ApiVersion() *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApiVersion", reflect.TypeOf((*MockCliConnection)(nil).ApiVersion)) 79 | } 80 | 81 | // CliCommand mocks base method 82 | func (m *MockCliConnection) CliCommand(arg0 ...string) ([]string, error) { 83 | m.ctrl.T.Helper() 84 | varargs := []interface{}{} 85 | for _, a := range arg0 { 86 | varargs = append(varargs, a) 87 | } 88 | ret := m.ctrl.Call(m, "CliCommand", varargs...) 89 | ret0, _ := ret[0].([]string) 90 | ret1, _ := ret[1].(error) 91 | return ret0, ret1 92 | } 93 | 94 | // CliCommand indicates an expected call of CliCommand 95 | func (mr *MockCliConnectionMockRecorder) CliCommand(arg0 ...interface{}) *gomock.Call { 96 | mr.mock.ctrl.T.Helper() 97 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CliCommand", reflect.TypeOf((*MockCliConnection)(nil).CliCommand), arg0...) 98 | } 99 | 100 | // CliCommandWithoutTerminalOutput mocks base method 101 | func (m *MockCliConnection) CliCommandWithoutTerminalOutput(arg0 ...string) ([]string, error) { 102 | m.ctrl.T.Helper() 103 | varargs := []interface{}{} 104 | for _, a := range arg0 { 105 | varargs = append(varargs, a) 106 | } 107 | ret := m.ctrl.Call(m, "CliCommandWithoutTerminalOutput", varargs...) 108 | ret0, _ := ret[0].([]string) 109 | ret1, _ := ret[1].(error) 110 | return ret0, ret1 111 | } 112 | 113 | // CliCommandWithoutTerminalOutput indicates an expected call of CliCommandWithoutTerminalOutput 114 | func (mr *MockCliConnectionMockRecorder) CliCommandWithoutTerminalOutput(arg0 ...interface{}) *gomock.Call { 115 | mr.mock.ctrl.T.Helper() 116 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CliCommandWithoutTerminalOutput", reflect.TypeOf((*MockCliConnection)(nil).CliCommandWithoutTerminalOutput), arg0...) 117 | } 118 | 119 | // DopplerEndpoint mocks base method 120 | func (m *MockCliConnection) DopplerEndpoint() (string, error) { 121 | m.ctrl.T.Helper() 122 | ret := m.ctrl.Call(m, "DopplerEndpoint") 123 | ret0, _ := ret[0].(string) 124 | ret1, _ := ret[1].(error) 125 | return ret0, ret1 126 | } 127 | 128 | // DopplerEndpoint indicates an expected call of DopplerEndpoint 129 | func (mr *MockCliConnectionMockRecorder) DopplerEndpoint() *gomock.Call { 130 | mr.mock.ctrl.T.Helper() 131 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DopplerEndpoint", reflect.TypeOf((*MockCliConnection)(nil).DopplerEndpoint)) 132 | } 133 | 134 | // GetApp mocks base method 135 | func (m *MockCliConnection) GetApp(arg0 string) (models.GetAppModel, error) { 136 | m.ctrl.T.Helper() 137 | ret := m.ctrl.Call(m, "GetApp", arg0) 138 | ret0, _ := ret[0].(models.GetAppModel) 139 | ret1, _ := ret[1].(error) 140 | return ret0, ret1 141 | } 142 | 143 | // GetApp indicates an expected call of GetApp 144 | func (mr *MockCliConnectionMockRecorder) GetApp(arg0 interface{}) *gomock.Call { 145 | mr.mock.ctrl.T.Helper() 146 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApp", reflect.TypeOf((*MockCliConnection)(nil).GetApp), arg0) 147 | } 148 | 149 | // GetApps mocks base method 150 | func (m *MockCliConnection) GetApps() ([]models.GetAppsModel, error) { 151 | m.ctrl.T.Helper() 152 | ret := m.ctrl.Call(m, "GetApps") 153 | ret0, _ := ret[0].([]models.GetAppsModel) 154 | ret1, _ := ret[1].(error) 155 | return ret0, ret1 156 | } 157 | 158 | // GetApps indicates an expected call of GetApps 159 | func (mr *MockCliConnectionMockRecorder) GetApps() *gomock.Call { 160 | mr.mock.ctrl.T.Helper() 161 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApps", reflect.TypeOf((*MockCliConnection)(nil).GetApps)) 162 | } 163 | 164 | // GetCurrentOrg mocks base method 165 | func (m *MockCliConnection) GetCurrentOrg() (models.Organization, error) { 166 | m.ctrl.T.Helper() 167 | ret := m.ctrl.Call(m, "GetCurrentOrg") 168 | ret0, _ := ret[0].(models.Organization) 169 | ret1, _ := ret[1].(error) 170 | return ret0, ret1 171 | } 172 | 173 | // GetCurrentOrg indicates an expected call of GetCurrentOrg 174 | func (mr *MockCliConnectionMockRecorder) GetCurrentOrg() *gomock.Call { 175 | mr.mock.ctrl.T.Helper() 176 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentOrg", reflect.TypeOf((*MockCliConnection)(nil).GetCurrentOrg)) 177 | } 178 | 179 | // GetCurrentSpace mocks base method 180 | func (m *MockCliConnection) GetCurrentSpace() (models.Space, error) { 181 | m.ctrl.T.Helper() 182 | ret := m.ctrl.Call(m, "GetCurrentSpace") 183 | ret0, _ := ret[0].(models.Space) 184 | ret1, _ := ret[1].(error) 185 | return ret0, ret1 186 | } 187 | 188 | // GetCurrentSpace indicates an expected call of GetCurrentSpace 189 | func (mr *MockCliConnectionMockRecorder) GetCurrentSpace() *gomock.Call { 190 | mr.mock.ctrl.T.Helper() 191 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentSpace", reflect.TypeOf((*MockCliConnection)(nil).GetCurrentSpace)) 192 | } 193 | 194 | // GetOrg mocks base method 195 | func (m *MockCliConnection) GetOrg(arg0 string) (models.GetOrg_Model, error) { 196 | m.ctrl.T.Helper() 197 | ret := m.ctrl.Call(m, "GetOrg", arg0) 198 | ret0, _ := ret[0].(models.GetOrg_Model) 199 | ret1, _ := ret[1].(error) 200 | return ret0, ret1 201 | } 202 | 203 | // GetOrg indicates an expected call of GetOrg 204 | func (mr *MockCliConnectionMockRecorder) GetOrg(arg0 interface{}) *gomock.Call { 205 | mr.mock.ctrl.T.Helper() 206 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrg", reflect.TypeOf((*MockCliConnection)(nil).GetOrg), arg0) 207 | } 208 | 209 | // GetOrgUsers mocks base method 210 | func (m *MockCliConnection) GetOrgUsers(arg0 string, arg1 ...string) ([]models.GetOrgUsers_Model, error) { 211 | m.ctrl.T.Helper() 212 | varargs := []interface{}{arg0} 213 | for _, a := range arg1 { 214 | varargs = append(varargs, a) 215 | } 216 | ret := m.ctrl.Call(m, "GetOrgUsers", varargs...) 217 | ret0, _ := ret[0].([]models.GetOrgUsers_Model) 218 | ret1, _ := ret[1].(error) 219 | return ret0, ret1 220 | } 221 | 222 | // GetOrgUsers indicates an expected call of GetOrgUsers 223 | func (mr *MockCliConnectionMockRecorder) GetOrgUsers(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 224 | mr.mock.ctrl.T.Helper() 225 | varargs := append([]interface{}{arg0}, arg1...) 226 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrgUsers", reflect.TypeOf((*MockCliConnection)(nil).GetOrgUsers), varargs...) 227 | } 228 | 229 | // GetOrgs mocks base method 230 | func (m *MockCliConnection) GetOrgs() ([]models.GetOrgs_Model, error) { 231 | m.ctrl.T.Helper() 232 | ret := m.ctrl.Call(m, "GetOrgs") 233 | ret0, _ := ret[0].([]models.GetOrgs_Model) 234 | ret1, _ := ret[1].(error) 235 | return ret0, ret1 236 | } 237 | 238 | // GetOrgs indicates an expected call of GetOrgs 239 | func (mr *MockCliConnectionMockRecorder) GetOrgs() *gomock.Call { 240 | mr.mock.ctrl.T.Helper() 241 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrgs", reflect.TypeOf((*MockCliConnection)(nil).GetOrgs)) 242 | } 243 | 244 | // GetService mocks base method 245 | func (m *MockCliConnection) GetService(arg0 string) (models.GetService_Model, error) { 246 | m.ctrl.T.Helper() 247 | ret := m.ctrl.Call(m, "GetService", arg0) 248 | ret0, _ := ret[0].(models.GetService_Model) 249 | ret1, _ := ret[1].(error) 250 | return ret0, ret1 251 | } 252 | 253 | // GetService indicates an expected call of GetService 254 | func (mr *MockCliConnectionMockRecorder) GetService(arg0 interface{}) *gomock.Call { 255 | mr.mock.ctrl.T.Helper() 256 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetService", reflect.TypeOf((*MockCliConnection)(nil).GetService), arg0) 257 | } 258 | 259 | // GetServices mocks base method 260 | func (m *MockCliConnection) GetServices() ([]models.GetServices_Model, error) { 261 | m.ctrl.T.Helper() 262 | ret := m.ctrl.Call(m, "GetServices") 263 | ret0, _ := ret[0].([]models.GetServices_Model) 264 | ret1, _ := ret[1].(error) 265 | return ret0, ret1 266 | } 267 | 268 | // GetServices indicates an expected call of GetServices 269 | func (mr *MockCliConnectionMockRecorder) GetServices() *gomock.Call { 270 | mr.mock.ctrl.T.Helper() 271 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServices", reflect.TypeOf((*MockCliConnection)(nil).GetServices)) 272 | } 273 | 274 | // GetSpace mocks base method 275 | func (m *MockCliConnection) GetSpace(arg0 string) (models.GetSpace_Model, error) { 276 | m.ctrl.T.Helper() 277 | ret := m.ctrl.Call(m, "GetSpace", arg0) 278 | ret0, _ := ret[0].(models.GetSpace_Model) 279 | ret1, _ := ret[1].(error) 280 | return ret0, ret1 281 | } 282 | 283 | // GetSpace indicates an expected call of GetSpace 284 | func (mr *MockCliConnectionMockRecorder) GetSpace(arg0 interface{}) *gomock.Call { 285 | mr.mock.ctrl.T.Helper() 286 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSpace", reflect.TypeOf((*MockCliConnection)(nil).GetSpace), arg0) 287 | } 288 | 289 | // GetSpaceUsers mocks base method 290 | func (m *MockCliConnection) GetSpaceUsers(arg0, arg1 string) ([]models.GetSpaceUsers_Model, error) { 291 | m.ctrl.T.Helper() 292 | ret := m.ctrl.Call(m, "GetSpaceUsers", arg0, arg1) 293 | ret0, _ := ret[0].([]models.GetSpaceUsers_Model) 294 | ret1, _ := ret[1].(error) 295 | return ret0, ret1 296 | } 297 | 298 | // GetSpaceUsers indicates an expected call of GetSpaceUsers 299 | func (mr *MockCliConnectionMockRecorder) GetSpaceUsers(arg0, arg1 interface{}) *gomock.Call { 300 | mr.mock.ctrl.T.Helper() 301 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSpaceUsers", reflect.TypeOf((*MockCliConnection)(nil).GetSpaceUsers), arg0, arg1) 302 | } 303 | 304 | // GetSpaces mocks base method 305 | func (m *MockCliConnection) GetSpaces() ([]models.GetSpaces_Model, error) { 306 | m.ctrl.T.Helper() 307 | ret := m.ctrl.Call(m, "GetSpaces") 308 | ret0, _ := ret[0].([]models.GetSpaces_Model) 309 | ret1, _ := ret[1].(error) 310 | return ret0, ret1 311 | } 312 | 313 | // GetSpaces indicates an expected call of GetSpaces 314 | func (mr *MockCliConnectionMockRecorder) GetSpaces() *gomock.Call { 315 | mr.mock.ctrl.T.Helper() 316 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSpaces", reflect.TypeOf((*MockCliConnection)(nil).GetSpaces)) 317 | } 318 | 319 | // HasAPIEndpoint mocks base method 320 | func (m *MockCliConnection) HasAPIEndpoint() (bool, error) { 321 | m.ctrl.T.Helper() 322 | ret := m.ctrl.Call(m, "HasAPIEndpoint") 323 | ret0, _ := ret[0].(bool) 324 | ret1, _ := ret[1].(error) 325 | return ret0, ret1 326 | } 327 | 328 | // HasAPIEndpoint indicates an expected call of HasAPIEndpoint 329 | func (mr *MockCliConnectionMockRecorder) HasAPIEndpoint() *gomock.Call { 330 | mr.mock.ctrl.T.Helper() 331 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasAPIEndpoint", reflect.TypeOf((*MockCliConnection)(nil).HasAPIEndpoint)) 332 | } 333 | 334 | // HasOrganization mocks base method 335 | func (m *MockCliConnection) HasOrganization() (bool, error) { 336 | m.ctrl.T.Helper() 337 | ret := m.ctrl.Call(m, "HasOrganization") 338 | ret0, _ := ret[0].(bool) 339 | ret1, _ := ret[1].(error) 340 | return ret0, ret1 341 | } 342 | 343 | // HasOrganization indicates an expected call of HasOrganization 344 | func (mr *MockCliConnectionMockRecorder) HasOrganization() *gomock.Call { 345 | mr.mock.ctrl.T.Helper() 346 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasOrganization", reflect.TypeOf((*MockCliConnection)(nil).HasOrganization)) 347 | } 348 | 349 | // HasSpace mocks base method 350 | func (m *MockCliConnection) HasSpace() (bool, error) { 351 | m.ctrl.T.Helper() 352 | ret := m.ctrl.Call(m, "HasSpace") 353 | ret0, _ := ret[0].(bool) 354 | ret1, _ := ret[1].(error) 355 | return ret0, ret1 356 | } 357 | 358 | // HasSpace indicates an expected call of HasSpace 359 | func (mr *MockCliConnectionMockRecorder) HasSpace() *gomock.Call { 360 | mr.mock.ctrl.T.Helper() 361 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasSpace", reflect.TypeOf((*MockCliConnection)(nil).HasSpace)) 362 | } 363 | 364 | // IsLoggedIn mocks base method 365 | func (m *MockCliConnection) IsLoggedIn() (bool, error) { 366 | m.ctrl.T.Helper() 367 | ret := m.ctrl.Call(m, "IsLoggedIn") 368 | ret0, _ := ret[0].(bool) 369 | ret1, _ := ret[1].(error) 370 | return ret0, ret1 371 | } 372 | 373 | // IsLoggedIn indicates an expected call of IsLoggedIn 374 | func (mr *MockCliConnectionMockRecorder) IsLoggedIn() *gomock.Call { 375 | mr.mock.ctrl.T.Helper() 376 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLoggedIn", reflect.TypeOf((*MockCliConnection)(nil).IsLoggedIn)) 377 | } 378 | 379 | // IsSSLDisabled mocks base method 380 | func (m *MockCliConnection) IsSSLDisabled() (bool, error) { 381 | m.ctrl.T.Helper() 382 | ret := m.ctrl.Call(m, "IsSSLDisabled") 383 | ret0, _ := ret[0].(bool) 384 | ret1, _ := ret[1].(error) 385 | return ret0, ret1 386 | } 387 | 388 | // IsSSLDisabled indicates an expected call of IsSSLDisabled 389 | func (mr *MockCliConnectionMockRecorder) IsSSLDisabled() *gomock.Call { 390 | mr.mock.ctrl.T.Helper() 391 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSSLDisabled", reflect.TypeOf((*MockCliConnection)(nil).IsSSLDisabled)) 392 | } 393 | 394 | // LoggregatorEndpoint mocks base method 395 | func (m *MockCliConnection) LoggregatorEndpoint() (string, error) { 396 | m.ctrl.T.Helper() 397 | ret := m.ctrl.Call(m, "LoggregatorEndpoint") 398 | ret0, _ := ret[0].(string) 399 | ret1, _ := ret[1].(error) 400 | return ret0, ret1 401 | } 402 | 403 | // LoggregatorEndpoint indicates an expected call of LoggregatorEndpoint 404 | func (mr *MockCliConnectionMockRecorder) LoggregatorEndpoint() *gomock.Call { 405 | mr.mock.ctrl.T.Helper() 406 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoggregatorEndpoint", reflect.TypeOf((*MockCliConnection)(nil).LoggregatorEndpoint)) 407 | } 408 | 409 | // UserEmail mocks base method 410 | func (m *MockCliConnection) UserEmail() (string, error) { 411 | m.ctrl.T.Helper() 412 | ret := m.ctrl.Call(m, "UserEmail") 413 | ret0, _ := ret[0].(string) 414 | ret1, _ := ret[1].(error) 415 | return ret0, ret1 416 | } 417 | 418 | // UserEmail indicates an expected call of UserEmail 419 | func (mr *MockCliConnectionMockRecorder) UserEmail() *gomock.Call { 420 | mr.mock.ctrl.T.Helper() 421 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserEmail", reflect.TypeOf((*MockCliConnection)(nil).UserEmail)) 422 | } 423 | 424 | // UserGuid mocks base method 425 | func (m *MockCliConnection) UserGuid() (string, error) { 426 | m.ctrl.T.Helper() 427 | ret := m.ctrl.Call(m, "UserGuid") 428 | ret0, _ := ret[0].(string) 429 | ret1, _ := ret[1].(error) 430 | return ret0, ret1 431 | } 432 | 433 | // UserGuid indicates an expected call of UserGuid 434 | func (mr *MockCliConnectionMockRecorder) UserGuid() *gomock.Call { 435 | mr.mock.ctrl.T.Helper() 436 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserGuid", reflect.TypeOf((*MockCliConnection)(nil).UserGuid)) 437 | } 438 | 439 | // Username mocks base method 440 | func (m *MockCliConnection) Username() (string, error) { 441 | m.ctrl.T.Helper() 442 | ret := m.ctrl.Call(m, "Username") 443 | ret0, _ := ret[0].(string) 444 | ret1, _ := ret[1].(error) 445 | return ret0, ret1 446 | } 447 | 448 | // Username indicates an expected call of Username 449 | func (mr *MockCliConnectionMockRecorder) Username() *gomock.Call { 450 | mr.mock.ctrl.T.Helper() 451 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Username", reflect.TypeOf((*MockCliConnection)(nil).Username)) 452 | } 453 | -------------------------------------------------------------------------------- /mocks/mocks.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/cloudfoundry/stack-auditor/cf" 10 | 11 | plugin_models "code.cloudfoundry.org/cli/plugin/models" 12 | "github.com/golang/mock/gomock" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | //go:generate mockgen -package mocks -destination cli_connection.go code.cloudfoundry.org/cli/plugin CliConnection 17 | var ( 18 | StackAName = "stackA" 19 | StackBName = "stackB" 20 | StackAGuid = "stackAGuid" 21 | StackBGuid = "stackBGuid" 22 | StackEName = "stackE" 23 | StackEGuid = "stackEGuid" 24 | AppAName = "appA" 25 | AppBName = "appB" 26 | SpaceGuid = "commonSpaceGuid" 27 | SpaceName = "commonSpace" 28 | ) 29 | 30 | func SetupMockCliConnection(mockCtrl *gomock.Controller) *MockCliConnection { 31 | apps, err := FileToString("apps.json") 32 | Expect(err).ToNot(HaveOccurred()) 33 | 34 | appA, err := FileToString("appA.json") 35 | Expect(err).ToNot(HaveOccurred()) 36 | 37 | appB, err := FileToString("appB.json") 38 | Expect(err).ToNot(HaveOccurred()) 39 | 40 | spaces, err := FileToString("spaces.json") 41 | Expect(err).ToNot(HaveOccurred()) 42 | 43 | buildpacks, err := FileToString("buildpacks.json") 44 | Expect(err).ToNot(HaveOccurred()) 45 | 46 | mockConnection := NewMockCliConnection(mockCtrl) 47 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v3/apps?per_page=%s", cf.V3ResultsPerPage)).Return( 48 | apps, nil).AnyTimes() 49 | 50 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v3/apps?names=%s&space_guids=%s", AppAName, SpaceGuid)).Return( 51 | appA, 52 | nil).AnyTimes() 53 | 54 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v3/apps?names=%s&space_guids=%s", AppBName, SpaceGuid)).Return( 55 | appB, 56 | nil).AnyTimes() 57 | 58 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v2/spaces?results-per-page=%s", cf.V2ResultsPerPage)).Return( 59 | spaces, 60 | nil).AnyTimes() 61 | 62 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("stack", "--guid", StackAName).Return( 63 | []string{ 64 | StackAGuid, 65 | }, nil).AnyTimes() 66 | 67 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("stack", "--guid", StackBName).Return( 68 | []string{ 69 | StackBGuid, 70 | }, nil).AnyTimes() 71 | 72 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("stack", "--guid", StackEName).Return( 73 | []string{ 74 | StackEGuid, 75 | }, nil).AnyTimes() 76 | 77 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("stack", "--guid", gomock.Any()).Return( 78 | []string{}, nil).AnyTimes() 79 | 80 | mockConnection.EXPECT().CliCommandWithoutTerminalOutput("curl", fmt.Sprintf("/v2/buildpacks?results-per-page=%s", cf.V2ResultsPerPage)).Return( 81 | buildpacks, 82 | nil).AnyTimes() 83 | 84 | mockConnection.EXPECT().GetOrgs().Return( 85 | []plugin_models.GetOrgs_Model{ 86 | { 87 | Guid: "commonOrgGuid", 88 | Name: "commonOrg", 89 | }, 90 | 91 | { 92 | Guid: "orgBGuid", 93 | Name: "orgB", 94 | }, 95 | }, nil).AnyTimes() 96 | 97 | SetCurrentOrgAndSpace(mockConnection, "commonOrg", SpaceName, SpaceGuid) 98 | 99 | return mockConnection 100 | } 101 | 102 | func SetCurrentOrgAndSpace(mockConnection *MockCliConnection, org string, space string, spaceGuid string) { 103 | mockConnection.EXPECT().GetCurrentOrg().Return(plugin_models.Organization{ 104 | OrganizationFields: plugin_models.OrganizationFields{ 105 | Name: org}, 106 | }, nil).AnyTimes() 107 | mockConnection.EXPECT().GetCurrentSpace().Return(plugin_models.Space{ 108 | SpaceFields: plugin_models.SpaceFields{ 109 | Name: space, Guid: spaceGuid}, 110 | }, nil).AnyTimes() 111 | } 112 | 113 | // TODO move this somewhere more appropriate 114 | func FileToString(fileName string) ([]string, error) { 115 | path, err := filepath.Abs(filepath.Join("..", "testdata", fileName)) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | buf, err := os.ReadFile(path) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | return strings.Split(string(buf), "\n"), nil 126 | } 127 | -------------------------------------------------------------------------------- /resources/apps.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | // Partial structure of JSON when hitting the /v2/apps endpoint 4 | type V2AppsJSON struct { 5 | NextURL string `json:"next_url"` 6 | Apps []V2App `json:"resources"` 7 | } 8 | 9 | type V2App struct { 10 | Metadata struct { 11 | GUID string `json:"guid"` 12 | } `json:"metadata"` 13 | Entity struct { 14 | Name string `json:"name"` 15 | SpaceGUID string `json:"space_guid"` 16 | StackGUID string `json:"stack_guid"` 17 | State string `json:"state"` 18 | } `json:"entity"` 19 | } 20 | 21 | // Partial structure of JSON when hitting the /v3/apps endpoint 22 | type V3AppsJSON struct { 23 | Pagination struct { 24 | Next struct { 25 | Href string `json:"href"` 26 | } `json:"next"` 27 | } `json:"pagination"` 28 | Apps []V3App `json:"resources"` 29 | } 30 | 31 | type V3App struct { 32 | GUID string `json:"guid"` 33 | Name string `json:"name"` 34 | State string `json:"state"` 35 | Lifecycle struct { 36 | Data struct { 37 | Stack string `json:"stack"` 38 | } `json:"data"` 39 | } `json:"lifecycle"` 40 | Relationships struct { 41 | Space struct { 42 | Data struct { 43 | GUID string `json:"guid"` 44 | } `json:"data"` 45 | } `json:"space"` 46 | } `json:"relationships"` 47 | Links struct { 48 | Self struct { 49 | Href string `json:"href"` 50 | } `json:"self"` 51 | Packages struct { 52 | Href string `json:"href"` 53 | } `json:"packages"` 54 | CurrentDroplet struct { 55 | Href string `json:"href"` 56 | } `json:"current_droplet"` 57 | Droplets struct { 58 | Href string `json:"href"` 59 | } `json:"droplets"` 60 | Tasks struct { 61 | Href string `json:"href"` 62 | } `json:"tasks"` 63 | Start struct { 64 | Href string `json:"href"` 65 | Method string `json:"method"` 66 | } `json:"start"` 67 | Stop struct { 68 | Href string `json:"href"` 69 | Method string `json:"method"` 70 | } `json:"stop"` 71 | Revisions struct { 72 | Href string `json:"href"` 73 | } `json:"revisions"` 74 | DeployedRevisions struct { 75 | Href string `json:"href"` 76 | } `json:"deployed_revisions"` 77 | } `json:"links"` 78 | } 79 | -------------------------------------------------------------------------------- /resources/audit_json.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type App struct { 11 | Org string `json:"org"` 12 | Space string `json:"space"` 13 | Name string `json:"name"` 14 | Stack string `json:"stack"` 15 | State string `json:"state"` 16 | } 17 | 18 | type Apps []App 19 | 20 | func (a Apps) String() string { 21 | var list []string 22 | 23 | for _, app := range a { 24 | list = append(list, fmt.Sprintf("%s", app)) 25 | } 26 | 27 | return strings.Join(list, "\n") + "\n" 28 | 29 | } 30 | 31 | func (a Apps) Len() int { 32 | return len(a) 33 | } 34 | 35 | func (a Apps) Less(i, j int) bool { 36 | return a[i].Name < a[j].Name 37 | } 38 | 39 | func (a Apps) Swap(i, j int) { 40 | a[i], a[j] = a[j], a[i] 41 | } 42 | 43 | func (a Apps) CSV() (string, error) { 44 | records := a.records() 45 | 46 | var buff bytes.Buffer 47 | 48 | w := csv.NewWriter(&buff) 49 | if err := w.WriteAll(records); err != nil { 50 | return "", err 51 | } 52 | 53 | return buff.String(), nil 54 | } 55 | 56 | func (a App) String() string { 57 | return fmt.Sprintf("%s/%s/%s %s %s", a.Org, a.Space, a.Name, a.Stack, a.State) 58 | } 59 | 60 | func (a Apps) headers() []string { 61 | return []string{"org", "space", "name", "stack", "state"} 62 | } 63 | 64 | func (a Apps) values() [][]string { 65 | var result [][]string 66 | for _, app := range a { 67 | result = append(result, []string{app.Org, app.Space, 68 | app.Name, app.Stack, app.State}) 69 | } 70 | 71 | return result 72 | } 73 | 74 | func (a Apps) records() [][]string { 75 | var result [][]string 76 | 77 | headers := a.headers() 78 | values := a.values() 79 | 80 | result = append(result, headers) 81 | result = append(result, values...) 82 | 83 | return result 84 | } 85 | -------------------------------------------------------------------------------- /resources/buildpacks.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | type BuildpacksJSON struct { 4 | TotalResults int `json:"total_results"` 5 | TotalPages int `json:"total_pages"` 6 | PrevURL string `json:"prev_url"` 7 | NextURL string `json:"next_url"` 8 | BuildPacks []BuildPack `json:"resources"` 9 | } 10 | 11 | type BuildPack struct { 12 | Metadata struct { 13 | GUID string `json:"guid"` 14 | URL string `json:"url"` 15 | } `json:"metadata"` 16 | Entity struct { 17 | Name string `json:"name"` 18 | Stack string `json:"stack"` 19 | Position int `json:"position"` 20 | Enabled bool `json:"enabled"` 21 | Locked bool `json:"locked"` 22 | Filename string `json:"filename"` 23 | } `json:"entity"` 24 | } 25 | -------------------------------------------------------------------------------- /resources/droplet.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import "time" 4 | 5 | type DropletListJSON struct { 6 | Pagination struct { 7 | TotalResults int `json:"total_results"` 8 | TotalPages int `json:"total_pages"` 9 | First struct { 10 | Href string `json:"href"` 11 | } `json:"first"` 12 | Last struct { 13 | Href string `json:"href"` 14 | } `json:"last"` 15 | Next interface{} `json:"next"` 16 | Previous interface{} `json:"previous"` 17 | } `json:"pagination"` 18 | Resources []DropletJSON `json:"resources"` 19 | } 20 | 21 | type DropletJSON struct { 22 | GUID string `json:"guid"` 23 | State string `json:"state"` 24 | Stack string `json:"stack"` 25 | CreatedAt time.Time `json:"created_at"` 26 | } 27 | -------------------------------------------------------------------------------- /resources/errors.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | type V3ErrorJSON struct { 4 | Errors []struct { 5 | Detail string `json:"detail"` 6 | Title string `json:"title"` 7 | Code int `json:"code"` 8 | } `json:"errors"` 9 | } 10 | 11 | type V2ErrorJSON struct { 12 | Description string `json:"description"` 13 | ErrorCode string `json:"error_code"` 14 | Code int `json:"code"` 15 | } 16 | -------------------------------------------------------------------------------- /resources/orgs.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import "code.cloudfoundry.org/cli/plugin/models" 4 | 5 | type Orgs []plugin_models.GetOrgs_Model 6 | 7 | func (o Orgs) Map() map[string]string { 8 | m := make(map[string]string) 9 | 10 | for _, org := range o { 11 | m[org.Guid] = org.Name 12 | } 13 | return m 14 | } 15 | -------------------------------------------------------------------------------- /resources/packages.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | type PackagerJSON struct { 4 | // TOOD: do we really need paginated results? 5 | Pagination struct { 6 | TotalResults int `json:"total_results"` 7 | TotalPages int `json:"total_pages"` 8 | First struct { 9 | Href string `json:"href"` 10 | } `json:"first"` 11 | Last struct { 12 | Href string `json:"href"` 13 | } `json:"last"` 14 | Next interface{} `json:"next"` 15 | Previous interface{} `json:"previous"` 16 | } `json:"pagination"` 17 | Resources []Package `json:"resources"` 18 | } 19 | 20 | type Package struct { 21 | GUID string `json:"guid"` 22 | Type string `json:"type"` 23 | Data struct { 24 | Error interface{} `json:"error"` 25 | Checksum struct { 26 | Type string `json:"type"` 27 | Value string `json:"value"` 28 | } `json:"checksum"` 29 | } `json:"data"` 30 | State string `json:"state"` 31 | } 32 | -------------------------------------------------------------------------------- /resources/spaces.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | type SpacesJSON struct { 4 | TotalResults int `json:"total_results"` 5 | TotalPages int `json:"total_pages"` 6 | PrevURL string `json:"prev_url"` 7 | NextURL string `json:"next_url"` 8 | Resources []struct { 9 | Metadata struct { 10 | GUID string `json:"guid"` 11 | URL string `json:"url"` 12 | } `json:"metadata"` 13 | Entity struct { 14 | Name string `json:"name"` 15 | OrganizationGUID string `json:"organization_guid"` 16 | } `json:"entity"` 17 | } `json:"resources"` 18 | } 19 | 20 | type Spaces []SpacesJSON 21 | 22 | func (s Spaces) MakeSpaceOrgAndNameMap() (map[string]string, map[string]string) { 23 | spaceOrgMap := make(map[string]string) 24 | spaceNameMap := make(map[string]string) 25 | for _, spaces := range s { 26 | for _, space := range spaces.Resources { 27 | spaceNameMap[space.Metadata.GUID] = space.Entity.Name 28 | spaceOrgMap[space.Metadata.GUID] = space.Entity.OrganizationGUID 29 | } 30 | } 31 | return spaceNameMap, spaceOrgMap 32 | } 33 | -------------------------------------------------------------------------------- /resources/stacks.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | type StacksJSON struct { 4 | TotalResults int `json:"total_results"` 5 | TotalPages int `json:"total_pages"` 6 | PrevURL string `json:"prev_url"` 7 | NextURL string `json:"next_url"` 8 | Resources []struct { 9 | Metadata struct { 10 | GUID string `json:"guid"` 11 | URL string `json:"url"` 12 | } `json:"metadata"` 13 | Entity struct { 14 | Name string `json:"name"` 15 | } `json:"entity"` 16 | } `json:"resources"` 17 | } 18 | 19 | type Stacks []StacksJSON 20 | 21 | func (s Stacks) MakeStackMap() map[string]string { 22 | stackMap := make(map[string]string) 23 | for _, stacks := range s { 24 | for _, stack := range stacks.Resources { 25 | stackMap[stack.Metadata.GUID] = stack.Entity.Name 26 | } 27 | } 28 | return stackMap 29 | } 30 | -------------------------------------------------------------------------------- /scripts/all-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 5 | ./scripts/unit.sh && ./scripts/integration.sh 6 | 7 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 6 | 7 | mkdir -p build 8 | 9 | if [[ -z "$version" ]]; then #version not provided, use latest git tag 10 | git_tag=$(git describe --abbrev=0 --tags) 11 | version=${git_tag:1} 12 | fi 13 | 14 | export CGO_ENABLED=0 15 | if [[ -n "$buildall" ]]; then 16 | echo "building all binaries" 17 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.tagVersion=$version" -o build/stack-auditor-linux-64 github.com/cloudfoundry/stack-auditor 18 | GOOS=linux GOARCH=386 go build -ldflags="-s -w -X main.tagVersion=$version" -o build/stack-auditor-linux-32 github.com/cloudfoundry/stack-auditor 19 | GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.tagVersion=$version" -o build/stack-auditor-darwin-arm github.com/cloudfoundry/stack-auditor 20 | GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.tagVersion=$version" -o build/stack-auditor-darwin-amd64 github.com/cloudfoundry/stack-auditor 21 | GOOS=windows GOARCH=386 go build -ldflags="-s -w -X main.tagVersion=$version" -o build/stack-auditor-windows-32 github.com/cloudfoundry/stack-auditor 22 | GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.tagVersion=$version" -o build/stack-auditor-windows-64 github.com/cloudfoundry/stack-auditor 23 | elif [[ -n "$buildintegration" ]]; then 24 | echo "building integration binary" 25 | # integration binary overrides the default V3ResultsPerPage to allow testing of pagination 26 | go build -ldflags="-s -w -X main.tagVersion=$version -X github.com/cloudfoundry/stack-auditor/cf.V3ResultsPerPage=1" -o build/stack-auditor github.com/cloudfoundry/stack-auditor 27 | else 28 | echo "building default binary" 29 | go build -ldflags="-s -w -X main.tagVersion=$version" -o build/stack-auditor github.com/cloudfoundry/stack-auditor 30 | fi 31 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 6 | 7 | scripts/build.sh 8 | cf install-plugin build/stack-auditor -f 9 | -------------------------------------------------------------------------------- /scripts/integration.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 6 | 7 | export buildintegration=true 8 | scripts/install.sh 9 | 10 | echo "Run Integration Tests" 11 | ginkgo -timeout 0 ./integration/... -v -count=1 12 | exit_code=$? 13 | 14 | if [ "$exit_code" != "0" ]; then 15 | echo -e "\n\033[0;31m** GO Test Failed **\033[0m" 16 | else 17 | echo -e "\n\033[0;32m** GO Test Succeeded **\033[0m" 18 | fi 19 | 20 | exit $exit_code 21 | -------------------------------------------------------------------------------- /scripts/reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 5 | 6 | scripts/uninstall.sh 7 | scripts/install.sh 8 | -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cf uninstall-plugin StackAuditor 6 | -------------------------------------------------------------------------------- /scripts/unit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -uo pipefail 3 | 4 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 5 | 6 | echo "Run Unit Tests" 7 | ginkgo -r -v --label-filter="!integration" 8 | exit_code=$? 9 | 10 | if [ "$exit_code" != "0" ]; then 11 | echo -e "\n\033[0;31m** GO Test Failed **\033[0m" 12 | else 13 | echo -e "\n\033[0;32m** GO Test Succeeded **\033[0m" 14 | fi 15 | 16 | exit $exit_code 17 | -------------------------------------------------------------------------------- /terminalUI/terminalUI.go: -------------------------------------------------------------------------------- 1 | package terminalUI 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type UIController struct { 11 | Scanner *bufio.Scanner 12 | OutputWriter *bufio.Writer 13 | } 14 | 15 | func NewUi() UIController { 16 | return UIController{ 17 | Scanner: bufio.NewScanner(bufio.NewReader(os.Stdin)), 18 | OutputWriter: bufio.NewWriter(os.Stdout), 19 | } 20 | } 21 | 22 | // Probably don't have to handle below errors, if we have trouble writing to stdout, then your up a creek without a paddle 23 | func (ui *UIController) ConfirmDelete(stackName string) bool { 24 | defer ui.OutputWriter.Flush() 25 | fmt.Fprintf(ui.OutputWriter, "Are you sure you want to remove the %s stack? If so, type the name of the stack [%s]\n>", stackName, stackName) 26 | ui.OutputWriter.Flush() 27 | if ui.Scanner.Scan() { 28 | w := ui.Scanner.Text() 29 | w_trim := strings.ToLower(strings.TrimSpace(w)) 30 | if w_trim == stackName { 31 | fmt.Fprintf(ui.OutputWriter, "Deleting stack %s...\n", stackName) 32 | return true 33 | } 34 | fmt.Fprintf(ui.OutputWriter, "aborted deleting stack %s\n", stackName) 35 | return false 36 | } 37 | fmt.Fprintf(ui.OutputWriter, "failed to scan user input aborting\n") 38 | return false 39 | } 40 | -------------------------------------------------------------------------------- /terminalUI/terminalUI_suite_test.go: -------------------------------------------------------------------------------- 1 | package terminalUI_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestTerminalUI(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "TerminalUI Suite") 13 | } 14 | -------------------------------------------------------------------------------- /terminalUI/terminalUI_test.go: -------------------------------------------------------------------------------- 1 | package terminalUI_test 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | 7 | "github.com/cloudfoundry/stack-auditor/terminalUI" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("TerminalUI", func() { 13 | var ( 14 | uiController terminalUI.UIController 15 | outputBuffer bytes.Buffer 16 | inputBuffer bytes.Buffer 17 | ) 18 | 19 | BeforeEach(func() { 20 | outputBuffer = bytes.Buffer{} 21 | outputWriter := bufio.NewWriter(&outputBuffer) 22 | 23 | inputBuffer = bytes.Buffer{} 24 | testReader := bufio.NewReader(&inputBuffer) 25 | testScanner := bufio.NewScanner(testReader) 26 | 27 | uiController = terminalUI.UIController{Scanner: testScanner, OutputWriter: outputWriter} 28 | 29 | }) 30 | 31 | When("deleting a stack", func() { 32 | BeforeEach(func() { 33 | // clean up all of our streams 34 | outputBuffer.Reset() 35 | inputBuffer.Reset() 36 | }) 37 | It("return true when user types yes", func() { 38 | inputBuffer.WriteString("some-stack-name") 39 | Expect(uiController.ConfirmDelete("some-stack-name")).To(BeTrue()) 40 | Expect(outputBuffer.String()).To(ContainSubstring("Deleting stack")) 41 | }) 42 | 43 | It("returns fals when no user input", func() { 44 | inputBuffer.WriteString("") 45 | Expect(uiController.ConfirmDelete("some-stack-name")).To(BeFalse()) 46 | Expect(outputBuffer.String()).To(ContainSubstring("failed to scan user input aborting")) 47 | }) 48 | 49 | It("returns fals when user types something other than yes", func() { 50 | inputBuffer.WriteString("some-stack-name-that-is-totes-mcgoats-wrong") 51 | Expect(uiController.ConfirmDelete("some-stack-name")).To(BeFalse()) 52 | Expect(outputBuffer.String()).To(ContainSubstring("aborted deleting stack")) 53 | }) 54 | 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /testdata/appA.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": { 3 | "total_results": 2, 4 | "total_pages": 1, 5 | "first": { 6 | "href": "some-link" 7 | }, 8 | "last": { 9 | "href": "some-link" 10 | }, 11 | "next": null, 12 | "previous": null 13 | }, 14 | "resources": [ 15 | { 16 | "guid": "appAGuid", 17 | "name": "appA", 18 | "state": "STARTED", 19 | "created_at": "some-creation-time", 20 | "updated_at": "some-update-time", 21 | "lifecycle": { 22 | "type": "buildpack", 23 | "data": { 24 | "buildpacks": [ 25 | "some-buildpack" 26 | ], 27 | "stack": "stackA" 28 | } 29 | }, 30 | "relationships": { 31 | "space": { 32 | "data": { 33 | "guid": "commonSpaceGuid" 34 | } 35 | } 36 | }, 37 | "links": { 38 | "self": { 39 | "href": "some-link" 40 | }, 41 | "environment_variables": { 42 | "href": "some-link" 43 | }, 44 | "space": { 45 | "href": "some-link" 46 | }, 47 | "processes": { 48 | "href": "some-link" 49 | }, 50 | "route_mappings": { 51 | "href": "some-link" 52 | }, 53 | "packages": { 54 | "href": "some-link" 55 | }, 56 | "current_droplet": { 57 | "href": "some-link" 58 | }, 59 | "droplets": { 60 | "href": "some-link" 61 | }, 62 | "tasks": { 63 | "href": "some-link" 64 | }, 65 | "start": { 66 | "href": "some-start-link", 67 | "method": "POST" 68 | }, 69 | "stop": { 70 | "href": "some-stop-link", 71 | "method": "POST" 72 | } 73 | } 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /testdata/appB.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": { 3 | "total_results": 2, 4 | "total_pages": 1, 5 | "first": { 6 | "href": "some-link" 7 | }, 8 | "last": { 9 | "href": "some-link" 10 | }, 11 | "next": null, 12 | "previous": null 13 | }, 14 | "resources": [ 15 | { 16 | "guid": "appBGuid", 17 | "name": "appB", 18 | "state": "STOPPED", 19 | "created_at": "some-creation-time", 20 | "updated_at": "some-update-time", 21 | "lifecycle": { 22 | "type": "buildpack", 23 | "data": { 24 | "buildpacks": [ 25 | "some-buildpack" 26 | ], 27 | "stack": "stackB" 28 | } 29 | }, 30 | "relationships": { 31 | "space": { 32 | "data": { 33 | "guid": "commonSpaceGuid" 34 | } 35 | } 36 | }, 37 | "links": { 38 | "self": { 39 | "href": "some-link" 40 | }, 41 | "environment_variables": { 42 | "href": "some-link" 43 | }, 44 | "space": { 45 | "href": "some-link" 46 | }, 47 | "processes": { 48 | "href": "some-link" 49 | }, 50 | "route_mappings": { 51 | "href": "some-link" 52 | }, 53 | "packages": { 54 | "href": "some-link" 55 | }, 56 | "current_droplet": { 57 | "href": "some-link" 58 | }, 59 | "droplets": { 60 | "href": "some-link" 61 | }, 62 | "tasks": { 63 | "href": "some-link" 64 | }, 65 | "start": { 66 | "href": "some-start-link", 67 | "method": "POST" 68 | }, 69 | "stop": { 70 | "href": "some-stop-link", 71 | "method": "POST" 72 | } 73 | } 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /testdata/apps.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": { 3 | "total_results": 2, 4 | "total_pages": 1, 5 | "first": { 6 | "href": "some-link" 7 | }, 8 | "last": { 9 | "href": "some-link" 10 | }, 11 | "next": null, 12 | "previous": null 13 | }, 14 | "resources": [ 15 | { 16 | "guid": "appAGuid", 17 | "name": "appA", 18 | "state": "STARTED", 19 | "created_at": "some-creation-time", 20 | "updated_at": "some-update-time", 21 | "lifecycle": { 22 | "type": "buildpack", 23 | "data": { 24 | "buildpacks": [ 25 | "some-buildpack" 26 | ], 27 | "stack": "stackA" 28 | } 29 | }, 30 | "relationships": { 31 | "space": { 32 | "data": { 33 | "guid": "commonSpaceGuid" 34 | } 35 | } 36 | }, 37 | "links": { 38 | "self": { 39 | "href": "some-link" 40 | }, 41 | "environment_variables": { 42 | "href": "some-link" 43 | }, 44 | "space": { 45 | "href": "some-link" 46 | }, 47 | "processes": { 48 | "href": "some-link" 49 | }, 50 | "route_mappings": { 51 | "href": "some-link" 52 | }, 53 | "packages": { 54 | "href": "some-link" 55 | }, 56 | "current_droplet": { 57 | "href": "some-link" 58 | }, 59 | "droplets": { 60 | "href": "some-link" 61 | }, 62 | "tasks": { 63 | "href": "some-link" 64 | }, 65 | "start": { 66 | "href": "some-start-link", 67 | "method": "POST" 68 | }, 69 | "stop": { 70 | "href": "some-stop-link", 71 | "method": "POST" 72 | } 73 | } 74 | }, 75 | { 76 | "guid": "appBGuid", 77 | "name": "appB", 78 | "state": "STOPPED", 79 | "created_at": "some-creation-time", 80 | "updated_at": "some-update-time", 81 | "lifecycle": { 82 | "type": "buildpack", 83 | "data": { 84 | "buildpacks": [ 85 | "some-buildpack" 86 | ], 87 | "stack": "stackB" 88 | } 89 | }, 90 | "relationships": { 91 | "space": { 92 | "data": { 93 | "guid": "commonSpaceGuid" 94 | } 95 | } 96 | }, 97 | "links": { 98 | "self": { 99 | "href": "some-link" 100 | }, 101 | "environment_variables": { 102 | "href": "some-link" 103 | }, 104 | "space": { 105 | "href": "some-link" 106 | }, 107 | "processes": { 108 | "href": "some-link" 109 | }, 110 | "route_mappings": { 111 | "href": "some-link" 112 | }, 113 | "packages": { 114 | "href": "some-link" 115 | }, 116 | "current_droplet": { 117 | "href": "some-link" 118 | }, 119 | "droplets": { 120 | "href": "some-link" 121 | }, 122 | "tasks": { 123 | "href": "some-link" 124 | }, 125 | "start": { 126 | "href": "some-start-link", 127 | "method": "POST" 128 | }, 129 | "stop": { 130 | "href": "some-stop-link", 131 | "method": "POST" 132 | } 133 | } 134 | } 135 | ] 136 | } 137 | -------------------------------------------------------------------------------- /testdata/buildpacks.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_results": 27, 3 | "total_pages": 1, 4 | "prev_url": null, 5 | "next_url": null, 6 | "resources": [ 7 | { 8 | "metadata": { 9 | "guid": "b305ce3f-880f-44bb-b513-1212bcea69c9", 10 | "url": "/v2/buildpacks/b305ce3f-880f-44bb-b513-1212bcea69c9", 11 | "created_at": "2019-03-27T08:27:44Z", 12 | "updated_at": "2019-03-27T08:27:44Z" 13 | }, 14 | "entity": { 15 | "name": "staticfile_buildpack", 16 | "stack": "stackC", 17 | "position": 1, 18 | "enabled": true, 19 | "locked": false, 20 | "filename": "staticfile_buildpack-cflinuxfs2-v1.4.39.zip" 21 | } 22 | }, 23 | { 24 | "metadata": { 25 | "guid": "2f667923-36ad-4540-b99c-ca834c7633d4", 26 | "url": "/v2/buildpacks/2f667923-36ad-4540-b99c-ca834c7633d4", 27 | "created_at": "2019-03-27T08:27:47Z", 28 | "updated_at": "2019-03-27T08:27:47Z" 29 | }, 30 | "entity": { 31 | "name": "staticfile_buildpack", 32 | "stack": "stackD", 33 | "position": 2, 34 | "enabled": true, 35 | "locked": false, 36 | "filename": "staticfile_buildpack-cflinuxfs3-v1.4.39.zip" 37 | } 38 | }, 39 | { 40 | "metadata": { 41 | "guid": "43748a17-c21b-4b84-b944-4916bb5d081d", 42 | "url": "/v2/buildpacks/43748a17-c21b-4b84-b944-4916bb5d081d", 43 | "created_at": "2019-03-27T08:27:47Z", 44 | "updated_at": "2019-03-27T08:27:47Z" 45 | }, 46 | "entity": { 47 | "name": "java_buildpack", 48 | "stack": "stackC", 49 | "position": 3, 50 | "enabled": true, 51 | "locked": false, 52 | "filename": "java-buildpack-cflinuxfs2-v4.17.2.zip" 53 | } 54 | }, 55 | { 56 | "metadata": { 57 | "guid": "7610b94b-a8c7-4427-85ff-ce249c51e0a0", 58 | "url": "/v2/buildpacks/7610b94b-a8c7-4427-85ff-ce249c51e0a0", 59 | "created_at": "2019-03-27T08:27:47Z", 60 | "updated_at": "2019-03-27T08:27:47Z" 61 | }, 62 | "entity": { 63 | "name": "java_buildpack", 64 | "stack": "stackD", 65 | "position": 4, 66 | "enabled": true, 67 | "locked": false, 68 | "filename": "java-buildpack-cflinuxfs3-v4.17.2.zip" 69 | } 70 | }, 71 | { 72 | "metadata": { 73 | "guid": "fdb69523-d076-4e93-926d-8458ffcbea85", 74 | "url": "/v2/buildpacks/fdb69523-d076-4e93-926d-8458ffcbea85", 75 | "created_at": "2019-03-27T08:27:47Z", 76 | "updated_at": "2019-03-27T08:27:48Z" 77 | }, 78 | "entity": { 79 | "name": "ruby_buildpack", 80 | "stack": "stackC", 81 | "position": 5, 82 | "enabled": true, 83 | "locked": false, 84 | "filename": "ruby_buildpack-cflinuxfs2-v1.7.31.zip" 85 | } 86 | }, 87 | { 88 | "metadata": { 89 | "guid": "3e832b5e-54f1-4baa-8e2c-3e1e287153fe", 90 | "url": "/v2/buildpacks/3e832b5e-54f1-4baa-8e2c-3e1e287153fe", 91 | "created_at": "2019-03-27T08:27:48Z", 92 | "updated_at": "2019-03-27T08:27:48Z" 93 | }, 94 | "entity": { 95 | "name": "ruby_buildpack", 96 | "stack": "stackD", 97 | "position": 6, 98 | "enabled": true, 99 | "locked": false, 100 | "filename": "ruby_buildpack-cflinuxfs3-v1.7.31.zip" 101 | } 102 | }, 103 | { 104 | "metadata": { 105 | "guid": "9f61623e-6343-4aaf-9df5-c4924d607b47", 106 | "url": "/v2/buildpacks/9f61623e-6343-4aaf-9df5-c4924d607b47", 107 | "created_at": "2019-03-27T08:27:48Z", 108 | "updated_at": "2019-03-27T08:27:48Z" 109 | }, 110 | "entity": { 111 | "name": "dotnet_core_buildpack", 112 | "stack": "stackC", 113 | "position": 7, 114 | "enabled": true, 115 | "locked": false, 116 | "filename": "dotnet-core_buildpack-cflinuxfs2-v2.2.5.zip" 117 | } 118 | }, 119 | { 120 | "metadata": { 121 | "guid": "8c5f1d37-3073-4f1d-940d-5d756ede8cf8", 122 | "url": "/v2/buildpacks/8c5f1d37-3073-4f1d-940d-5d756ede8cf8", 123 | "created_at": "2019-03-27T08:27:48Z", 124 | "updated_at": "2019-03-27T08:27:48Z" 125 | }, 126 | "entity": { 127 | "name": "dotnet_core_buildpack", 128 | "stack": "stackD", 129 | "position": 8, 130 | "enabled": true, 131 | "locked": false, 132 | "filename": "dotnet-core_buildpack-cflinuxfs3-v2.2.5.zip" 133 | } 134 | }, 135 | { 136 | "metadata": { 137 | "guid": "97ef99cf-d337-467a-ac05-d1c93751dc79", 138 | "url": "/v2/buildpacks/97ef99cf-d337-467a-ac05-d1c93751dc79", 139 | "created_at": "2019-03-27T08:27:48Z", 140 | "updated_at": "2019-03-27T08:27:48Z" 141 | }, 142 | "entity": { 143 | "name": "nodejs_buildpack", 144 | "stack": "stackC", 145 | "position": 9, 146 | "enabled": true, 147 | "locked": false, 148 | "filename": "nodejs_buildpack-cflinuxfs2-v1.6.43.zip" 149 | } 150 | }, 151 | { 152 | "metadata": { 153 | "guid": "61bfb934-0054-4e39-b1e3-0176249645b1", 154 | "url": "/v2/buildpacks/61bfb934-0054-4e39-b1e3-0176249645b1", 155 | "created_at": "2019-03-27T08:27:48Z", 156 | "updated_at": "2019-03-29T17:51:40Z" 157 | }, 158 | "entity": { 159 | "name": "nodejs_buildpack", 160 | "stack": "stackD", 161 | "position": 10, 162 | "enabled": true, 163 | "locked": false, 164 | "filename": "nodejs_buildpack-cached-cflinuxfs3-v1.6.46.20190329135115.zip" 165 | } 166 | }, 167 | { 168 | "metadata": { 169 | "guid": "133575ff-0fdc-40d1-a80e-d9fd4c2d94b7", 170 | "url": "/v2/buildpacks/133575ff-0fdc-40d1-a80e-d9fd4c2d94b7", 171 | "created_at": "2019-03-27T08:27:48Z", 172 | "updated_at": "2019-03-29T15:52:03Z" 173 | }, 174 | "entity": { 175 | "name": "go_buildpack", 176 | "stack": "stackC", 177 | "position": 11, 178 | "enabled": true, 179 | "locked": false, 180 | "filename": "go_buildpack-cflinuxfs2-v1.8.36.zip" 181 | } 182 | }, 183 | { 184 | "metadata": { 185 | "guid": "7afd0203-dd57-48a3-8f10-4b85b6bb4fef", 186 | "url": "/v2/buildpacks/7afd0203-dd57-48a3-8f10-4b85b6bb4fef", 187 | "created_at": "2019-03-27T08:27:48Z", 188 | "updated_at": "2019-03-27T08:27:48Z" 189 | }, 190 | "entity": { 191 | "name": "go_buildpack", 192 | "stack": "stackD", 193 | "position": 12, 194 | "enabled": true, 195 | "locked": false, 196 | "filename": "go_buildpack-cflinuxfs3-v1.8.33.zip" 197 | } 198 | }, 199 | { 200 | "metadata": { 201 | "guid": "89b4b71d-5024-41a5-8f56-a022307e9bfb", 202 | "url": "/v2/buildpacks/89b4b71d-5024-41a5-8f56-a022307e9bfb", 203 | "created_at": "2019-03-27T08:27:48Z", 204 | "updated_at": "2019-03-27T08:27:48Z" 205 | }, 206 | "entity": { 207 | "name": "python_buildpack", 208 | "stack": "stackC", 209 | "position": 13, 210 | "enabled": true, 211 | "locked": false, 212 | "filename": "python_buildpack-cflinuxfs2-v1.6.28.zip" 213 | } 214 | }, 215 | { 216 | "metadata": { 217 | "guid": "9e13264f-a712-4e39-9876-28b714a8d63a", 218 | "url": "/v2/buildpacks/9e13264f-a712-4e39-9876-28b714a8d63a", 219 | "created_at": "2019-03-27T08:27:48Z", 220 | "updated_at": "2019-03-27T08:27:48Z" 221 | }, 222 | "entity": { 223 | "name": "python_buildpack", 224 | "stack": "stackD", 225 | "position": 14, 226 | "enabled": true, 227 | "locked": false, 228 | "filename": "python_buildpack-cflinuxfs3-v1.6.28.zip" 229 | } 230 | }, 231 | { 232 | "metadata": { 233 | "guid": "058768ab-a246-43d2-845d-a7973a64e920", 234 | "url": "/v2/buildpacks/058768ab-a246-43d2-845d-a7973a64e920", 235 | "created_at": "2019-03-27T08:27:48Z", 236 | "updated_at": "2019-03-27T18:57:55Z" 237 | }, 238 | "entity": { 239 | "name": "php_buildpack", 240 | "stack": "stackC", 241 | "position": 15, 242 | "enabled": true, 243 | "locked": false, 244 | "filename": "php_buildpack-cached-cflinuxfs2-v4.3.72.20190327185701.zip" 245 | } 246 | }, 247 | { 248 | "metadata": { 249 | "guid": "158d749f-f760-40ab-a997-2ae030f3b355", 250 | "url": "/v2/buildpacks/158d749f-f760-40ab-a997-2ae030f3b355", 251 | "created_at": "2019-03-27T08:27:48Z", 252 | "updated_at": "2019-03-27T18:56:12Z" 253 | }, 254 | "entity": { 255 | "name": "php_buildpack", 256 | "stack": "stackD", 257 | "position": 16, 258 | "enabled": true, 259 | "locked": false, 260 | "filename": "php_buildpack-cached-cflinuxfs3-v4.3.72.20190327185510.zip" 261 | } 262 | }, 263 | { 264 | "metadata": { 265 | "guid": "c2abd88a-1423-4673-b0f8-1e17c45ec01c", 266 | "url": "/v2/buildpacks/c2abd88a-1423-4673-b0f8-1e17c45ec01c", 267 | "created_at": "2019-03-27T08:27:48Z", 268 | "updated_at": "2019-03-27T08:27:48Z" 269 | }, 270 | "entity": { 271 | "name": "binary_buildpack", 272 | "stack": "stackC", 273 | "position": 17, 274 | "enabled": true, 275 | "locked": false, 276 | "filename": "binary_buildpack-cflinuxfs2-v1.0.30.zip" 277 | } 278 | }, 279 | { 280 | "metadata": { 281 | "guid": "53d81505-53b2-459a-814d-c2ba66b5ee7c", 282 | "url": "/v2/buildpacks/53d81505-53b2-459a-814d-c2ba66b5ee7c", 283 | "created_at": "2019-03-27T08:27:48Z", 284 | "updated_at": "2019-03-27T08:27:49Z" 285 | }, 286 | "entity": { 287 | "name": "binary_buildpack", 288 | "stack": "stackD", 289 | "position": 18, 290 | "enabled": true, 291 | "locked": false, 292 | "filename": "binary_buildpack-cflinuxfs3-v1.0.30.zip" 293 | } 294 | }, 295 | { 296 | "metadata": { 297 | "guid": "94f3ab84-a67e-4a97-b356-134cdbee7c7e", 298 | "url": "/v2/buildpacks/94f3ab84-a67e-4a97-b356-134cdbee7c7e", 299 | "created_at": "2019-03-27T08:27:49Z", 300 | "updated_at": "2019-03-29T07:06:52Z" 301 | }, 302 | "entity": { 303 | "name": "binary_buildpack", 304 | "stack": "windows2012R2", 305 | "position": 19, 306 | "enabled": true, 307 | "locked": false, 308 | "filename": "binary_buildpack-windows2012R2-v1.0.31.zip" 309 | } 310 | }, 311 | { 312 | "metadata": { 313 | "guid": "4195a99f-f9f1-4861-a5c7-e11461097457", 314 | "url": "/v2/buildpacks/4195a99f-f9f1-4861-a5c7-e11461097457", 315 | "created_at": "2019-03-27T08:27:49Z", 316 | "updated_at": "2019-03-29T07:07:07Z" 317 | }, 318 | "entity": { 319 | "name": "binary_buildpack", 320 | "stack": "stackC", 321 | "position": 20, 322 | "enabled": true, 323 | "locked": false, 324 | "filename": "binary_buildpack-windows2016-v1.0.31.zip" 325 | } 326 | }, 327 | { 328 | "metadata": { 329 | "guid": "5cfca053-c1b1-4970-84f5-f40465cfe5da", 330 | "url": "/v2/buildpacks/5cfca053-c1b1-4970-84f5-f40465cfe5da", 331 | "created_at": "2019-03-27T08:27:49Z", 332 | "updated_at": "2019-03-29T07:07:22Z" 333 | }, 334 | "entity": { 335 | "name": "binary_buildpack", 336 | "stack": "windows", 337 | "position": 21, 338 | "enabled": true, 339 | "locked": false, 340 | "filename": "binary_buildpack-windows-v1.0.31.zip" 341 | } 342 | }, 343 | { 344 | "metadata": { 345 | "guid": "4efd76c0-1d60-49e2-99e7-5c3a8e871aca", 346 | "url": "/v2/buildpacks/4efd76c0-1d60-49e2-99e7-5c3a8e871aca", 347 | "created_at": "2019-03-27T08:27:49Z", 348 | "updated_at": "2019-03-27T08:27:49Z" 349 | }, 350 | "entity": { 351 | "name": "nginx_buildpack", 352 | "stack": "stackD", 353 | "position": 22, 354 | "enabled": true, 355 | "locked": false, 356 | "filename": "nginx_buildpack-cflinuxfs3-v1.0.8.zip" 357 | } 358 | }, 359 | { 360 | "metadata": { 361 | "guid": "3ed63a51-0b97-4c11-b753-6c03dbd7ec7d", 362 | "url": "/v2/buildpacks/3ed63a51-0b97-4c11-b753-6c03dbd7ec7d", 363 | "created_at": "2019-03-27T08:27:49Z", 364 | "updated_at": "2019-03-27T08:27:49Z" 365 | }, 366 | "entity": { 367 | "name": "r_buildpack", 368 | "stack": "stackD", 369 | "position": 23, 370 | "enabled": true, 371 | "locked": false, 372 | "filename": "r_buildpack-cflinuxfs3-v1.0.4.zip" 373 | } 374 | }, 375 | { 376 | "metadata": { 377 | "guid": "6faf3f18-f2ac-448d-a990-d3efd3cdc52f", 378 | "url": "/v2/buildpacks/6faf3f18-f2ac-448d-a990-d3efd3cdc52f", 379 | "created_at": "2019-03-29T07:06:43Z", 380 | "updated_at": "2019-03-29T07:06:43Z" 381 | }, 382 | "entity": { 383 | "name": "hwc_buildpack", 384 | "stack": "windows2012R2", 385 | "position": 24, 386 | "enabled": true, 387 | "locked": false, 388 | "filename": "hwc_buildpack-windows2012R2-v3.1.6.zip" 389 | } 390 | }, 391 | { 392 | "metadata": { 393 | "guid": "7d71a9f9-0a1b-4953-9092-4bb719ae682f", 394 | "url": "/v2/buildpacks/7d71a9f9-0a1b-4953-9092-4bb719ae682f", 395 | "created_at": "2019-03-29T07:06:59Z", 396 | "updated_at": "2019-03-29T07:06:59Z" 397 | }, 398 | "entity": { 399 | "name": "hwc_buildpack", 400 | "stack": "stackC", 401 | "position": 25, 402 | "enabled": true, 403 | "locked": false, 404 | "filename": "hwc_buildpack-windows2016-v3.1.6.zip" 405 | } 406 | }, 407 | { 408 | "metadata": { 409 | "guid": "e7c45130-3e97-4120-9edd-87ecde2d2df2", 410 | "url": "/v2/buildpacks/e7c45130-3e97-4120-9edd-87ecde2d2df2", 411 | "created_at": "2019-03-29T07:07:13Z", 412 | "updated_at": "2019-03-29T07:07:14Z" 413 | }, 414 | "entity": { 415 | "name": "hwc_buildpack", 416 | "stack": "windows", 417 | "position": 26, 418 | "enabled": true, 419 | "locked": false, 420 | "filename": "hwc_buildpack-windows-v3.1.6.zip" 421 | } 422 | }, 423 | { 424 | "metadata": { 425 | "guid": "b7889451-865b-418b-806c-1249feca8cac", 426 | "url": "/v2/buildpacks/b7889451-865b-418b-806c-1249feca8cac", 427 | "created_at": "2019-03-29T17:40:40Z", 428 | "updated_at": "2019-03-29T17:40:40Z" 429 | }, 430 | "entity": { 431 | "name": "nodejs_buildpack", 432 | "stack": null, 433 | "position": 27, 434 | "enabled": true, 435 | "locked": false, 436 | "filename": null 437 | } 438 | } 439 | ] 440 | } -------------------------------------------------------------------------------- /testdata/errorV2.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Some error description", 3 | "error_code": "SomeErrorCode", 4 | "code": 1 5 | } 6 | -------------------------------------------------------------------------------- /testdata/errorV3.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "detail": "Some V3 error detail", 5 | "title": "SomeV3ErrorTitle", 6 | "code": 1 7 | }, 8 | { 9 | "detail": "Another V3 error detail", 10 | "title": "AnotherV3ErrorTitle", 11 | "code": 1 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /testdata/lifecycleV3Error.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "detail": "Lifecycle type is not included in the list: buildpack, docker", 5 | "title": "CF-UnprocessableEntity", 6 | "code": 10008 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /testdata/spaces.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_results": 12, 3 | "total_pages": 1, 4 | "prev_url": null, 5 | "next_url": null, 6 | "resources": [ 7 | { 8 | "metadata": { 9 | "guid": "commonSpaceGuid", 10 | "url": "/v2/spaces/commonSpaceGuid" 11 | }, 12 | "entity": { 13 | "name": "commonSpace", 14 | "organization_guid": "commonOrgGuid" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | type Command struct { 12 | } 13 | 14 | func (c Command) Run(bin, dir string, quiet bool, args ...string) error { 15 | cmd := exec.Command(bin, args...) 16 | cmd.Dir = dir 17 | if quiet { 18 | cmd.Stdout = io.Discard 19 | cmd.Stderr = io.Discard 20 | } else { 21 | cmd.Stdout = os.Stdout 22 | cmd.Stderr = os.Stderr 23 | } 24 | return cmd.Run() 25 | } 26 | 27 | func (c Command) RunWithOutput(bin, dir string, quiet bool, args ...string) (string, error) { 28 | logs := &bytes.Buffer{} 29 | 30 | cmd := exec.Command(bin, args...) 31 | cmd.Dir = dir 32 | if quiet { 33 | cmd.Stdout = io.MultiWriter(io.Discard, logs) 34 | cmd.Stderr = io.MultiWriter(io.Discard, logs) 35 | } else { 36 | cmd.Stdout = io.MultiWriter(os.Stdout, logs) 37 | cmd.Stderr = io.MultiWriter(os.Stderr, logs) 38 | } 39 | err := cmd.Run() 40 | 41 | return strings.TrimSpace(logs.String()), err 42 | } 43 | 44 | func (c Command) SetEnv(variableName string, path string) error { 45 | return os.Setenv(variableName, path) 46 | } 47 | --------------------------------------------------------------------------------