├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── golangci-lint.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── client ├── admin.go ├── admin_test.go ├── app.go ├── app_features.go ├── app_features_test.go ├── app_test.go ├── app_usage.go ├── app_usage_test.go ├── audit_event.go ├── audit_event_test.go ├── build.go ├── build_test.go ├── buildpack.go ├── buildpack_test.go ├── client.go ├── client_test.go ├── deployment.go ├── deployment_test.go ├── domain.go ├── domain_test.go ├── droplet.go ├── droplet_test.go ├── envar_group.go ├── envar_group_test.go ├── feature_flag.go ├── feature_flag_test.go ├── filter.go ├── isolation_segment.go ├── isolation_segment_test.go ├── job.go ├── job_test.go ├── list_opt.go ├── list_opt_test.go ├── manifest.go ├── manifest_test.go ├── organization.go ├── organization_quota.go ├── organization_quota_test.go ├── organization_test.go ├── package.go ├── package_test.go ├── pager.go ├── pager_test.go ├── polling.go ├── polling_test.go ├── process.go ├── process_test.go ├── resource_match.go ├── resource_match_test.go ├── revision.go ├── revision_test.go ├── role.go ├── role_test.go ├── root.go ├── route.go ├── route_test.go ├── security_group.go ├── security_group_test.go ├── service_broker.go ├── service_broker_test.go ├── service_credential_binding.go ├── service_credential_binding_test.go ├── service_instance.go ├── service_instance_test.go ├── service_offering.go ├── service_offering_test.go ├── service_plan.go ├── service_plan_test.go ├── service_plan_visibility.go ├── service_plan_visibility_test.go ├── service_route_binding.go ├── service_route_binding_test.go ├── service_usage.go ├── service_usage_test.go ├── sidecar.go ├── sidecar_test.go ├── space.go ├── space_feature.go ├── space_feature_test.go ├── space_quota.go ├── space_quota_test.go ├── space_test.go ├── stack.go ├── stack_test.go ├── task.go ├── task_test.go ├── test_runner.go ├── user.go └── user_test.go ├── config ├── cf_cli.go ├── cf_cli_test.go ├── config.go ├── config_test.go └── options.go ├── examples ├── auth │ └── main.go ├── droplet │ └── main.go ├── metadata │ └── main.go ├── push_app │ └── main.go ├── roles │ └── main.go ├── ssh_code │ └── main.go └── svc_creds │ └── main.go ├── go.mod ├── go.sum ├── internal ├── check │ ├── check.go │ └── check_test.go ├── http │ ├── client.go │ ├── client_test.go │ ├── request.go │ ├── request_test.go │ ├── response.go │ ├── response_test.go │ ├── status.go │ └── status_test.go ├── ios │ └── close.go ├── jwt │ ├── token.go │ └── token_test.go └── path │ ├── path.go │ ├── path_test.go │ ├── querystring_reader.go │ └── querystring_reader_test.go ├── operation ├── manifest.go ├── manifest_test.go ├── push.go └── push_test.go ├── resource ├── app.go ├── app_feature.go ├── app_usage.go ├── audit_event.go ├── build.go ├── buildpack.go ├── deployment.go ├── domain.go ├── droplet.go ├── envar_group.go ├── error.go ├── error_cf.go ├── error_test.go ├── feature_flag.go ├── isolation_segment.go ├── job.go ├── manifest.go ├── metadata.go ├── metadata_test.go ├── organization.go ├── organization_quota.go ├── package.go ├── process.go ├── quota.go ├── resource_match.go ├── revision.go ├── role.go ├── root.go ├── route.go ├── security_group.go ├── service_broker.go ├── service_credential_binding.go ├── service_instance.go ├── service_offering.go ├── service_plan.go ├── service_plan_visibility.go ├── service_route_binding.go ├── service_usage.go ├── sidecar.go ├── space.go ├── space_feature.go ├── space_quota.go ├── stack.go ├── task.go ├── types.go └── user.go ├── test ├── e2e_test.go └── helloworld │ ├── go.mod │ ├── main.go │ └── manifest.yml ├── testutil ├── api_mock.go ├── name_generator.go ├── object_generator.go ├── pointer.go └── template │ ├── app.json │ ├── app_environment.json │ ├── app_environment_expected.json │ ├── app_envvar.json │ ├── app_feature.json │ ├── app_permissions.json │ ├── app_ssh.json │ ├── app_update_envvar.json │ ├── app_usage.json │ ├── audit_event.json │ ├── build.json │ ├── buildpack.json │ ├── deployment.json │ ├── domain.json │ ├── domain_shared.json │ ├── droplet.json │ ├── droplet_association.json │ ├── environment_variable_group.json │ ├── feature_flag.json │ ├── isolation_segment.json │ ├── isolation_segment_relationships.json │ ├── job.json │ ├── job_failed.json │ ├── manifest.yml │ ├── manifest_diff.json │ ├── org.json │ ├── org_quota.json │ ├── org_usage_summary.json │ ├── package.json │ ├── package_docker.json │ ├── process.json │ ├── process_stats.json │ ├── resource_match.json │ ├── revision.json │ ├── role.json │ ├── route.json │ ├── route_destination_with_links.json │ ├── route_destinations.json │ ├── route_space_relationships.json │ ├── security_group.json │ ├── service_broker.json │ ├── service_credential_binding.json │ ├── service_credential_binding_detail.json │ ├── service_instance.json │ ├── service_instance_space_relationships.json │ ├── service_instance_usage_summary.json │ ├── service_instance_user_provided.json │ ├── service_offering.json │ ├── service_plan.json │ ├── service_plan_visibility.json │ ├── service_route_binding.json │ ├── service_usage.json │ ├── sidecar.json │ ├── space.json │ ├── space_quota.json │ ├── stack.json │ ├── task.json │ └── user.json └── tools ├── gen_error.go └── tools.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | paths-ignore: 8 | - 'README.md' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v2 20 | - name: Set up Go 1.x 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ^1.23 24 | - name: Run Test 25 | run: make test 26 | shell: bash 27 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | paths-ignore: 7 | - 'README.md' 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | permissions: 13 | contents: read 14 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 15 | # pull-requests: read 16 | 17 | jobs: 18 | golangci: 19 | name: lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v2 25 | with: 26 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 27 | version: latest 28 | 29 | # Optional: working directory, useful for monorepos 30 | # working-directory: somedir 31 | 32 | # Optional: golangci-lint command line arguments. 33 | # args: --issues-exit-code=0 34 | 35 | # Optional: show only new issues if it's a pull request. The default value is `false`. 36 | # only-new-issues: true 37 | 38 | # Optional: if set to true then the action will use pre-installed Go. 39 | # skip-go-installation: true 40 | 41 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 42 | # skip-pkg-cache: true 43 | 44 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 45 | # skip-build-cache: true 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | .idea/ 20 | .vscode/ 21 | 22 | # OS files 23 | .DS_Store 24 | bin 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Long Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOLANG_CI_LINT_VERSION := $(shell golangci-lint --version 2>/dev/null) 2 | 3 | .PHONY: all 4 | all: test lint 5 | 6 | .PHONY: clean 7 | clean: ## Clean testcache and delete build output 8 | go clean -testcache 9 | 10 | .PHONY: test 11 | test: ## Run the unit tests 12 | go test ./... -v -race 13 | 14 | .PHONY: generate 15 | generate: ## Generate fakes 16 | go generate ./... 17 | 18 | .PHONY: lint-prepare 19 | lint-prepare: 20 | ifdef GOLANG_CI_LINT_VERSION 21 | @echo "Found golangci-lint $(GOLANG_CI_LINT_VERSION)" 22 | else 23 | @echo "Installing golangci-lint" 24 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s latest 25 | @echo "[OK] golangci-lint installed" 26 | endif 27 | 28 | .PHONY: lint 29 | lint: lint-prepare ## Run the golangci linter 30 | ./bin/golangci-lint run 31 | 32 | .PHONY: update 33 | update: ## Update all dependencies 34 | go get -u ./... && go mod tidy 35 | 36 | .PHONY: tidy 37 | tidy: ## Remove unused dependencies 38 | go mod tidy 39 | 40 | .PHONY: list 41 | list: ## Print the current module's dependencies. 42 | go list -m all 43 | 44 | # Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 45 | help: ## Print help for each make target 46 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -------------------------------------------------------------------------------- /client/admin.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "context" 4 | 5 | type AdminClient commonClient 6 | 7 | // ClearBuildpackCache will delete all the existing buildpack caches in the blobstore. Success returns a JobID. 8 | // 9 | // The buildpack cache is used during staging by buildpacks as a way to cache certain resources, e.g. downloaded 10 | // Ruby gems. An admin who wants to decrease the size of their blobstore could use this endpoint to delete 11 | // unnecessary blobs. 12 | func (c *AdminClient) ClearBuildpackCache(ctx context.Context) (string, error) { 13 | return c.client.post(ctx, "/v3/admin/actions/clear_buildpack_cache", nil, nil) 14 | } 15 | -------------------------------------------------------------------------------- /client/admin_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 9 | ) 10 | 11 | func TestAdmin(t *testing.T) { 12 | tests := []RouteTest{ 13 | { 14 | Description: "Clear buildpack cache", 15 | Route: testutil.MockRoute{ 16 | Method: "POST", 17 | Endpoint: "/v3/admin/actions/clear_buildpack_cache", 18 | Status: http.StatusAccepted, 19 | RedirectLocation: "https://api.example.org/api/v3/jobs/c33a5caf-77e0-4d6e-b587-5555d339bc9a", 20 | }, 21 | Expected: "c33a5caf-77e0-4d6e-b587-5555d339bc9a", 22 | Action: func(c *Client, t *testing.T) (any, error) { 23 | return c.Admin.ClearBuildpackCache(context.Background()) 24 | }, 25 | }, 26 | } 27 | ExecuteTests(tests, t) 28 | } 29 | -------------------------------------------------------------------------------- /client/app_features.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 7 | "github.com/cloudfoundry/go-cfclient/v3/resource" 8 | ) 9 | 10 | type AppFeatureClient commonClient 11 | 12 | // Get retrieves the named app feature 13 | func (c *AppFeatureClient) Get(ctx context.Context, appGUID, featureName string) (*resource.AppFeature, error) { 14 | var a resource.AppFeature 15 | err := c.client.get(ctx, path.Format("/v3/apps/%s/features/%s", appGUID, featureName), &a) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return &a, nil 20 | } 21 | 22 | // GetSSH retrieves the SSH app feature 23 | func (c *AppFeatureClient) GetSSH(ctx context.Context, appGUID string) (*resource.AppFeature, error) { 24 | return c.Get(ctx, appGUID, "ssh") 25 | } 26 | 27 | // GetRevisions retrieves the revisions app feature 28 | func (c *AppFeatureClient) GetRevisions(ctx context.Context, appGUID string) (*resource.AppFeature, error) { 29 | return c.Get(ctx, appGUID, "revisions") 30 | } 31 | 32 | // List pages all app features 33 | func (c *AppFeatureClient) List(ctx context.Context, appGUID string) ([]*resource.AppFeature, *Pager, error) { 34 | var res resource.AppFeatureList 35 | err := c.client.get(ctx, path.Format("/v3/apps/%s/features", appGUID), &res) 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | pager := NewPager(res.Pagination) 40 | return res.Resources, pager, nil 41 | } 42 | 43 | // Update the enabled attribute of the named app feature 44 | func (c *AppFeatureClient) Update(ctx context.Context, appGUID, featureName string, enabled bool) (*resource.AppFeature, error) { 45 | r := &resource.AppFeatureUpdate{ 46 | Enabled: enabled, 47 | } 48 | var a resource.AppFeature 49 | _, err := c.client.patch(ctx, path.Format("/v3/apps/%s/features/%s", appGUID, featureName), r, &a) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return &a, nil 54 | } 55 | 56 | // UpdateSSH updated the enabled attribute of the SSH app feature 57 | func (c *AppFeatureClient) UpdateSSH(ctx context.Context, appGUID string, enabled bool) (*resource.AppFeature, error) { 58 | return c.Update(ctx, appGUID, "ssh", enabled) 59 | } 60 | 61 | // UpdateRevisions updated the enabled attribute of the revisions app feature 62 | func (c *AppFeatureClient) UpdateRevisions(ctx context.Context, appGUID string, enabled bool) (*resource.AppFeature, error) { 63 | return c.Update(ctx, appGUID, "revisions", enabled) 64 | } 65 | -------------------------------------------------------------------------------- /client/app_features_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 9 | ) 10 | 11 | func TestAppFeatures(t *testing.T) { 12 | g := testutil.NewObjectJSONGenerator() 13 | appFeature := g.AppFeature().JSON 14 | 15 | tests := []RouteTest{ 16 | { 17 | Description: "Get SSH app feature", 18 | Route: testutil.MockRoute{ 19 | Method: "GET", 20 | Endpoint: "/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/features/ssh", 21 | Output: g.Single(appFeature), 22 | Status: http.StatusOK}, 23 | Expected: appFeature, 24 | Action: func(c *Client, t *testing.T) (any, error) { 25 | return c.AppFeatures.GetSSH(context.Background(), "1cb006ee-fb05-47e1-b541-c34179ddc446") 26 | }, 27 | }, 28 | { 29 | Description: "List all app features", 30 | Route: testutil.MockRoute{ 31 | Method: "GET", 32 | Endpoint: "/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/features", 33 | Output: g.SinglePaged(appFeature), 34 | Status: http.StatusOK}, 35 | Expected: g.Array(appFeature), 36 | Action: func(c *Client, t *testing.T) (any, error) { 37 | f, _, err := c.AppFeatures.List(context.Background(), "1cb006ee-fb05-47e1-b541-c34179ddc446") 38 | return f, err 39 | }, 40 | }, 41 | { 42 | Description: "Update SSH app feature", 43 | Route: testutil.MockRoute{ 44 | Method: "PATCH", 45 | Endpoint: "/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/features/ssh", 46 | Output: g.Single(appFeature), 47 | Status: http.StatusOK, 48 | PostForm: `{ "enabled": false }`, 49 | }, 50 | Expected: appFeature, 51 | Action: func(c *Client, t *testing.T) (any, error) { 52 | return c.AppFeatures.UpdateSSH(context.Background(), "1cb006ee-fb05-47e1-b541-c34179ddc446", false) 53 | }, 54 | }, 55 | } 56 | ExecuteTests(tests, t) 57 | } 58 | -------------------------------------------------------------------------------- /client/app_usage.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | 7 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 8 | "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | ) 10 | 11 | type AppUsageClient commonClient 12 | 13 | // AppUsageListOptions list filters 14 | type AppUsageListOptions struct { 15 | *ListOptions 16 | 17 | AfterGUID string `qs:"after_guid"` 18 | GUIDs Filter `qs:"guids"` 19 | } 20 | 21 | // NewAppUsageOptions creates new options to pass to list 22 | func NewAppUsageOptions() *AppUsageListOptions { 23 | return &AppUsageListOptions{ 24 | ListOptions: NewListOptions(), 25 | } 26 | } 27 | 28 | func (o AppUsageListOptions) ToQueryString() (url.Values, error) { 29 | return o.ListOptions.ToQueryString(o) 30 | } 31 | 32 | // Get retrieves the specified app event 33 | func (c *AppUsageClient) Get(ctx context.Context, guid string) (*resource.AppUsage, error) { 34 | var a resource.AppUsage 35 | err := c.client.get(ctx, path.Format("/v3/app_usage_events/%s", guid), &a) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &a, nil 40 | } 41 | 42 | // List pages all app usage events 43 | func (c *AppUsageClient) List(ctx context.Context, opts *AppUsageListOptions) ([]*resource.AppUsage, *Pager, error) { 44 | if opts == nil { 45 | opts = NewAppUsageOptions() 46 | } 47 | var res resource.AppUsageList 48 | err := c.client.list(ctx, "/v3/app_usage_events", opts.ToQueryString, &res) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | pager := NewPager(res.Pagination) 53 | return res.Resources, pager, nil 54 | } 55 | 56 | // ListAll retrieves all app usage events 57 | func (c *AppUsageClient) ListAll(ctx context.Context, opts *AppUsageListOptions) ([]*resource.AppUsage, error) { 58 | if opts == nil { 59 | opts = NewAppUsageOptions() 60 | } 61 | return AutoPage[*AppUsageListOptions, *resource.AppUsage](opts, func(opts *AppUsageListOptions) ([]*resource.AppUsage, *Pager, error) { 62 | return c.List(ctx, opts) 63 | }) 64 | } 65 | 66 | // Purge destroys all existing events. Populates new usage events, one for each started app. 67 | // All populated events will have a created_at value of current time. 68 | // 69 | // There is the potential race condition if apps are currently being started, stopped, or scaled. 70 | // The seeded usage events will have the same guid as the app. 71 | func (c *AppUsageClient) Purge(ctx context.Context) error { 72 | _, err := c.client.post(ctx, "/v3/app_usage_events/actions/destructively_purge_all_and_reseed", nil, nil) 73 | return err 74 | } 75 | -------------------------------------------------------------------------------- /client/app_usage_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 9 | ) 10 | 11 | func TestAppUsages(t *testing.T) { 12 | g := testutil.NewObjectJSONGenerator() 13 | appUsage := g.AppUsage().JSON 14 | appUsage2 := g.AppUsage().JSON 15 | appUsage3 := g.AppUsage().JSON 16 | 17 | tests := []RouteTest{ 18 | { 19 | Description: "Get app usage event", 20 | Route: testutil.MockRoute{ 21 | Method: "GET", 22 | Endpoint: "/v3/app_usage_events/af846b67-e0c4-44eb-bfa8-ff30e902d710", 23 | Output: g.Single(appUsage), 24 | Status: http.StatusOK}, 25 | Expected: appUsage, 26 | Action: func(c *Client, t *testing.T) (any, error) { 27 | return c.AppUsageEvents.Get(context.Background(), "af846b67-e0c4-44eb-bfa8-ff30e902d710") 28 | }, 29 | }, 30 | { 31 | Description: "List all app usage events", 32 | Route: testutil.MockRoute{ 33 | Method: "GET", 34 | Endpoint: "/v3/app_usage_events", 35 | Output: g.Paged([]string{appUsage, appUsage2}, []string{appUsage3}), 36 | Status: http.StatusOK}, 37 | Expected: g.Array(appUsage, appUsage2, appUsage3), 38 | Action: func(c *Client, t *testing.T) (any, error) { 39 | return c.AppUsageEvents.ListAll(context.Background(), nil) 40 | }, 41 | }, 42 | { 43 | Description: "Purge all app usage events", 44 | Route: testutil.MockRoute{ 45 | Method: "POST", 46 | Endpoint: "/v3/app_usage_events/actions/destructively_purge_all_and_reseed", 47 | Status: http.StatusOK}, 48 | Action: func(c *Client, t *testing.T) (any, error) { 49 | err := c.AppUsageEvents.Purge(context.Background()) 50 | return nil, err 51 | }, 52 | }, 53 | } 54 | ExecuteTests(tests, t) 55 | } 56 | -------------------------------------------------------------------------------- /client/audit_event.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | 7 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 8 | "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | ) 10 | 11 | type AuditEventClient commonClient 12 | 13 | // AuditEventListOptions list filters 14 | type AuditEventListOptions struct { 15 | *ListOptions 16 | 17 | Types Filter `qs:"types"` // list of event types to filter by 18 | TargetGUIDs ExclusionFilter `qs:"target_guids"` // list of target guids to filter by 19 | OrganizationGUIDs Filter `qs:"organization_guids"` 20 | SpaceGUIDs Filter `qs:"space_guids"` 21 | } 22 | 23 | // NewAuditEventListOptions creates new options to pass to list 24 | func NewAuditEventListOptions() *AuditEventListOptions { 25 | return &AuditEventListOptions{ 26 | ListOptions: NewListOptions(), 27 | } 28 | } 29 | 30 | func (o AuditEventListOptions) ToQueryString() (url.Values, error) { 31 | return o.ListOptions.ToQueryString(o) 32 | } 33 | 34 | // First returns the first audit event matching the options or an error when less than 1 match 35 | func (c *AuditEventClient) First(ctx context.Context, opts *AuditEventListOptions) (*resource.AuditEvent, error) { 36 | return First[*AuditEventListOptions, *resource.AuditEvent](opts, func(opts *AuditEventListOptions) ([]*resource.AuditEvent, *Pager, error) { 37 | return c.List(ctx, opts) 38 | }) 39 | } 40 | 41 | // Get retrieves the specified audit event 42 | func (c *AuditEventClient) Get(ctx context.Context, guid string) (*resource.AuditEvent, error) { 43 | var a resource.AuditEvent 44 | err := c.client.get(ctx, path.Format("/v3/audit_events/%s", guid), &a) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return &a, nil 49 | } 50 | 51 | // List pages all audit events the user has access to 52 | func (c *AuditEventClient) List(ctx context.Context, opts *AuditEventListOptions) ([]*resource.AuditEvent, *Pager, error) { 53 | if opts == nil { 54 | opts = NewAuditEventListOptions() 55 | } 56 | var res resource.AuditEventList 57 | err := c.client.list(ctx, "/v3/audit_events", opts.ToQueryString, &res) 58 | if err != nil { 59 | return nil, nil, err 60 | } 61 | pager := NewPager(res.Pagination) 62 | return res.Resources, pager, nil 63 | } 64 | 65 | // ListAll retrieves all audit events the user has access to 66 | func (c *AuditEventClient) ListAll(ctx context.Context, opts *AuditEventListOptions) ([]*resource.AuditEvent, error) { 67 | if opts == nil { 68 | opts = NewAuditEventListOptions() 69 | } 70 | 71 | var all []*resource.AuditEvent 72 | for { 73 | page, pager, err := c.List(ctx, opts) 74 | if err != nil { 75 | return nil, err 76 | } 77 | all = append(all, page...) 78 | if !pager.HasNextPage() { 79 | break 80 | } 81 | pager.NextPage(opts) 82 | } 83 | return all, nil 84 | } 85 | 86 | // Single returns a single audit event matching the options or an error if not exactly 1 match 87 | func (c *AuditEventClient) Single(ctx context.Context, opts *AuditEventListOptions) (*resource.AuditEvent, error) { 88 | return Single[*AuditEventListOptions, *resource.AuditEvent](opts, func(opts *AuditEventListOptions) ([]*resource.AuditEvent, *Pager, error) { 89 | return c.List(ctx, opts) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /client/audit_event_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 9 | ) 10 | 11 | func TestAuditEvents(t *testing.T) { 12 | g := testutil.NewObjectJSONGenerator() 13 | auditEvent := g.AuditEvent().JSON 14 | auditEvent2 := g.AuditEvent().JSON 15 | auditEvent3 := g.AuditEvent().JSON 16 | 17 | tests := []RouteTest{ 18 | { 19 | Description: "Get audit event", 20 | Route: testutil.MockRoute{ 21 | Method: "GET", 22 | Endpoint: "/v3/audit_events/27a9b4a5-ba8a-448c-ac51-3a6dab9aa3f8", 23 | Output: g.Single(auditEvent), 24 | Status: http.StatusOK}, 25 | Expected: auditEvent, 26 | Action: func(c *Client, t *testing.T) (any, error) { 27 | return c.AuditEvents.Get(context.Background(), "27a9b4a5-ba8a-448c-ac51-3a6dab9aa3f8") 28 | }, 29 | }, 30 | { 31 | Description: "List all audit events", 32 | Route: testutil.MockRoute{ 33 | Method: "GET", 34 | Endpoint: "/v3/audit_events", 35 | Output: g.Paged([]string{auditEvent, auditEvent2}, []string{auditEvent3}), 36 | Status: http.StatusOK}, 37 | Expected: g.Array(auditEvent, auditEvent2, auditEvent3), 38 | Action: func(c *Client, t *testing.T) (any, error) { 39 | return c.AuditEvents.ListAll(context.Background(), nil) 40 | }, 41 | }, 42 | } 43 | ExecuteTests(tests, t) 44 | } 45 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/client" 7 | "github.com/cloudfoundry/go-cfclient/v3/config" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestClientWithInvalidConfig(t *testing.T) { 13 | _, err := client.New(nil) 14 | require.Error(t, err) 15 | require.EqualError(t, err, "config is nil") 16 | 17 | cfg := &config.Config{} 18 | _, err = client.New(cfg) 19 | require.Error(t, err) 20 | require.Equal(t, config.ErrConfigInvalid, err) 21 | } 22 | -------------------------------------------------------------------------------- /client/envar_group.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 7 | "github.com/cloudfoundry/go-cfclient/v3/resource" 8 | ) 9 | 10 | type EnvVarGroupClient commonClient 11 | 12 | // Get retrieves the specified envvar group 13 | func (c *EnvVarGroupClient) Get(ctx context.Context, name string) (*resource.EnvVarGroup, error) { 14 | var e resource.EnvVarGroup 15 | err := c.client.get(ctx, path.Format("/v3/environment_variable_groups/%s", name), &e) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return &e, nil 20 | } 21 | 22 | // GetRunning retrieves the running envvar group 23 | func (c *EnvVarGroupClient) GetRunning(ctx context.Context) (*resource.EnvVarGroup, error) { 24 | return c.Get(ctx, "running") 25 | } 26 | 27 | // GetStaging retrieves the running envvar group 28 | func (c *EnvVarGroupClient) GetStaging(ctx context.Context) (*resource.EnvVarGroup, error) { 29 | return c.Get(ctx, "staging") 30 | } 31 | 32 | // Update the specified attributes of the envar group 33 | func (c *EnvVarGroupClient) Update(ctx context.Context, name string, r *resource.EnvVarGroupUpdate) (*resource.EnvVarGroup, error) { 34 | var e resource.EnvVarGroup 35 | _, err := c.client.patch(ctx, path.Format("/v3/environment_variable_groups/%s", name), r, &e) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &e, nil 40 | } 41 | 42 | // UpdateRunning updates the specified attributes of the running envar group 43 | func (c *EnvVarGroupClient) UpdateRunning(ctx context.Context, r *resource.EnvVarGroupUpdate) (*resource.EnvVarGroup, error) { 44 | return c.Update(ctx, "running", r) 45 | } 46 | 47 | // UpdateStaging updates the specified attributes of the staging envar group 48 | func (c *EnvVarGroupClient) UpdateStaging(ctx context.Context, r *resource.EnvVarGroupUpdate) (*resource.EnvVarGroup, error) { 49 | return c.Update(ctx, "staging", r) 50 | } 51 | -------------------------------------------------------------------------------- /client/envar_group_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 10 | ) 11 | 12 | func TestEnvVarGroups(t *testing.T) { 13 | g := testutil.NewObjectJSONGenerator() 14 | envVarGroup := g.EnvVarGroup().JSON 15 | 16 | tests := []RouteTest{ 17 | { 18 | Description: "Get running env var group", 19 | Route: testutil.MockRoute{ 20 | Method: "GET", 21 | Endpoint: "/v3/environment_variable_groups/running", 22 | Output: g.Single(envVarGroup), 23 | Status: http.StatusOK}, 24 | Expected: envVarGroup, 25 | Action: func(c *Client, t *testing.T) (any, error) { 26 | return c.EnvVarGroups.GetRunning(context.Background()) 27 | }, 28 | }, 29 | { 30 | Description: "Update buildpack", 31 | Route: testutil.MockRoute{ 32 | Method: "PATCH", 33 | Endpoint: "/v3/environment_variable_groups/staging", 34 | Output: g.Single(envVarGroup), 35 | Status: http.StatusOK, 36 | PostForm: `{ "var": { "DEBUG": "false" }}`, 37 | }, 38 | Expected: envVarGroup, 39 | Action: func(c *Client, t *testing.T) (any, error) { 40 | r := &resource.EnvVarGroupUpdate{ 41 | Var: map[string]string{ 42 | "DEBUG": "false", 43 | }, 44 | } 45 | return c.EnvVarGroups.UpdateStaging(context.Background(), r) 46 | }, 47 | }, 48 | } 49 | ExecuteTests(tests, t) 50 | } 51 | -------------------------------------------------------------------------------- /client/feature_flag.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | 7 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 8 | "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | ) 10 | 11 | type FeatureFlagClient commonClient 12 | 13 | // FeatureFlagListOptions list filters 14 | type FeatureFlagListOptions struct { 15 | *ListOptions 16 | } 17 | 18 | // NewFeatureFlagListOptions creates new options to pass to list 19 | func NewFeatureFlagListOptions() *FeatureFlagListOptions { 20 | return &FeatureFlagListOptions{ 21 | ListOptions: NewListOptions(), 22 | } 23 | } 24 | 25 | func (o FeatureFlagListOptions) ToQueryString() (url.Values, error) { 26 | return o.ListOptions.ToQueryString(o) 27 | } 28 | 29 | // Get the specified feature flag 30 | func (c *FeatureFlagClient) Get(ctx context.Context, featureFlag resource.FeatureFlagType) (*resource.FeatureFlag, error) { 31 | var ff resource.FeatureFlag 32 | err := c.client.get(ctx, path.Format("/v3/feature_flags/%s", featureFlag), &ff) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &ff, nil 37 | } 38 | 39 | // List pages feature flags 40 | func (c *FeatureFlagClient) List(ctx context.Context, opts *FeatureFlagListOptions) ([]*resource.FeatureFlag, *Pager, error) { 41 | if opts == nil { 42 | opts = NewFeatureFlagListOptions() 43 | } 44 | var res resource.FeatureFlagList 45 | err := c.client.list(ctx, "/v3/feature_flags", opts.ToQueryString, &res) 46 | if err != nil { 47 | return nil, nil, err 48 | } 49 | pager := NewPager(res.Pagination) 50 | return res.Resources, pager, nil 51 | } 52 | 53 | // ListAll retrieves all feature flags 54 | func (c *FeatureFlagClient) ListAll(ctx context.Context, opts *FeatureFlagListOptions) ([]*resource.FeatureFlag, error) { 55 | if opts == nil { 56 | opts = NewFeatureFlagListOptions() 57 | } 58 | return AutoPage[*FeatureFlagListOptions, *resource.FeatureFlag](opts, func(opts *FeatureFlagListOptions) ([]*resource.FeatureFlag, *Pager, error) { 59 | return c.List(ctx, opts) 60 | }) 61 | } 62 | 63 | // Update the specified attributes of the feature flag 64 | func (c *FeatureFlagClient) Update(ctx context.Context, featureFlag resource.FeatureFlagType, r *resource.FeatureFlagUpdate) (*resource.FeatureFlag, error) { 65 | var d resource.FeatureFlag 66 | _, err := c.client.patch(ctx, path.Format("/v3/feature_flags/%s", featureFlag), r, &d) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return &d, nil 71 | } 72 | -------------------------------------------------------------------------------- /client/feature_flag_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 10 | ) 11 | 12 | func TestFeatureFlags(t *testing.T) { 13 | g := testutil.NewObjectJSONGenerator() 14 | ff := g.FeatureFlag().JSON 15 | ff2 := g.FeatureFlag().JSON 16 | ff3 := g.FeatureFlag().JSON 17 | ff4 := g.FeatureFlag().JSON 18 | 19 | tests := []RouteTest{ 20 | { 21 | Description: "Get feature flag", 22 | Route: testutil.MockRoute{ 23 | Method: "GET", 24 | Endpoint: "/v3/feature_flags/resource_matching", 25 | Output: g.Single(ff), 26 | Status: http.StatusOK}, 27 | Expected: ff, 28 | Action: func(c *Client, t *testing.T) (any, error) { 29 | return c.FeatureFlags.Get(context.Background(), resource.FeatureFlagResourceMatching) 30 | }, 31 | }, 32 | { 33 | Description: "List all feature flags", 34 | Route: testutil.MockRoute{ 35 | Method: "GET", 36 | Endpoint: "/v3/feature_flags", 37 | Output: g.Paged([]string{ff, ff2}, []string{ff3, ff4}), 38 | Status: http.StatusOK}, 39 | Expected: g.Array(ff, ff2, ff3, ff4), 40 | Action: func(c *Client, t *testing.T) (any, error) { 41 | return c.FeatureFlags.ListAll(context.Background(), nil) 42 | }, 43 | }, 44 | { 45 | Description: "Update feature flag", 46 | Route: testutil.MockRoute{ 47 | Method: "PATCH", 48 | Endpoint: "/v3/feature_flags/resource_matching", 49 | Output: g.Single(ff), 50 | Status: http.StatusOK, 51 | PostForm: `{ "enabled": true, "custom_error_message": "error message the user sees" }`, 52 | }, 53 | Expected: ff, 54 | Action: func(c *Client, t *testing.T) (any, error) { 55 | r := resource.NewFeatureFlagUpdate(). 56 | WithEnabled(true). 57 | WithCustomErrorMessage("error message the user sees") 58 | return c.FeatureFlags.Update(context.Background(), resource.FeatureFlagResourceMatching, r) 59 | }, 60 | }, 61 | } 62 | ExecuteTests(tests, t) 63 | } 64 | -------------------------------------------------------------------------------- /client/job.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 6 | "github.com/cloudfoundry/go-cfclient/v3/resource" 7 | ) 8 | 9 | type JobClient commonClient 10 | 11 | // Get the specified job 12 | func (c *JobClient) Get(ctx context.Context, guid string) (*resource.Job, error) { 13 | var job resource.Job 14 | err := c.client.get(ctx, path.Format("/v3/jobs/%s", guid), &job) 15 | if err != nil { 16 | return nil, err 17 | } 18 | return &job, nil 19 | } 20 | 21 | // PollComplete waits until the job completes, fails, or times out 22 | func (c *JobClient) PollComplete(ctx context.Context, jobGUID string, opts *PollingOptions) error { 23 | err := PollForStateOrTimeout(func() (string, string, error) { 24 | job, err := c.Get(ctx, jobGUID) 25 | if job != nil { 26 | var cfErrors string 27 | for _, e := range job.Errors { 28 | cfErrors += "\n" + e.Error() 29 | } 30 | for _, e := range job.Warnings { 31 | cfErrors += "\n" + e.Detail 32 | } 33 | return string(job.State), cfErrors, err 34 | } 35 | return "", "", err 36 | }, string(resource.JobStateComplete), opts) 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /client/job_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/require" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 11 | ) 12 | 13 | func TestJobs(t *testing.T) { 14 | g := testutil.NewObjectJSONGenerator() 15 | job := g.Job("COMPLETE").JSON 16 | jobProcessing := g.Job("PROCESSING").JSON 17 | jobFailed := g.JobFailed().JSON 18 | pollingOpts := &PollingOptions{ 19 | FailedState: "FAILED", 20 | Timeout: time.Second, 21 | CheckInterval: time.Nanosecond, 22 | } 23 | 24 | tests := []RouteTest{ 25 | { 26 | Description: "Get job", 27 | Route: testutil.MockRoute{ 28 | Method: "GET", 29 | Endpoint: "/v3/jobs/c33a5caf-77e0-4d6e-b587-5555d339bc9a", 30 | Output: g.Single(job), 31 | Status: http.StatusOK, 32 | }, 33 | Expected: job, 34 | Action: func(c *Client, t *testing.T) (any, error) { 35 | return c.Jobs.Get(context.Background(), "c33a5caf-77e0-4d6e-b587-5555d339bc9a") 36 | }, 37 | }, 38 | { 39 | Description: "Poll job that succeeds", 40 | Route: testutil.MockRoute{ 41 | Method: "GET", 42 | Endpoint: "/v3/jobs/c33a5caf-77e0-4d6e-b587-5555d339bc9a", 43 | Output: []string{jobProcessing, job}, 44 | Statuses: []int{http.StatusOK, http.StatusOK}, 45 | }, 46 | Action: func(c *Client, t *testing.T) (any, error) { 47 | err := c.Jobs.PollComplete(context.Background(), "c33a5caf-77e0-4d6e-b587-5555d339bc9a", pollingOpts) 48 | return nil, err 49 | }, 50 | }, 51 | { 52 | Description: "Poll job that fails", 53 | Route: testutil.MockRoute{ 54 | Method: "GET", 55 | Endpoint: "/v3/jobs/40e49716-44a3-4ae9-9926-d1a804acf70c", 56 | Output: []string{jobProcessing, jobFailed}, 57 | Statuses: []int{http.StatusOK, http.StatusOK}, 58 | }, 59 | Action: func(c *Client, t *testing.T) (any, error) { 60 | err := c.Jobs.PollComplete(context.Background(), "40e49716-44a3-4ae9-9926-d1a804acf70c", pollingOpts) 61 | require.Error(t, err) 62 | require.ErrorContains(t, err, "received state FAILED while waiting for async process") 63 | require.ErrorContains(t, err, "cfclient error (CF-UnprocessableEntity|10008): something went wrong") 64 | require.ErrorContains(t, err, "cfclient error (UnknownError|10001): unexpected error occurred") 65 | require.ErrorContains(t, err, "some warning") 66 | return nil, nil 67 | }, 68 | }, 69 | } 70 | ExecuteTests(tests, t) 71 | } 72 | -------------------------------------------------------------------------------- /client/list_opt.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | ) 8 | 9 | const ( 10 | filterTagName = "qs" 11 | 12 | DefaultPage = 1 13 | DefaultPageSize = 50 14 | 15 | PageField = "page" 16 | PerPageField = "per_page" 17 | ) 18 | 19 | type ListOptionsSerializer interface { 20 | Serialize(values url.Values, tag string) error 21 | } 22 | 23 | var listOptionsSerializerType = reflect.TypeOf((*ListOptionsSerializer)(nil)).Elem() 24 | 25 | type ListOptioner interface { 26 | CurrentPage(page, perPage int) 27 | ToQueryString() (url.Values, error) 28 | } 29 | 30 | // ListOptions is the shared common type for all other list option types 31 | type ListOptions struct { 32 | Page int `qs:"page"` 33 | PerPage int `qs:"per_page"` 34 | OrderBy string `qs:"order_by"` 35 | LabelSel LabelSelector `qs:"label_selector"` 36 | CreatedAts TimestampFilterList `qs:"created_ats"` 37 | UpdatedAts TimestampFilterList `qs:"updated_ats"` 38 | } 39 | 40 | // NewListOptions creates a default list options with page and page size set 41 | func NewListOptions() *ListOptions { 42 | return &ListOptions{ 43 | Page: DefaultPage, 44 | PerPage: DefaultPageSize, 45 | } 46 | } 47 | 48 | func (lo *ListOptions) CurrentPage(page, perPage int) { 49 | lo.Page = page 50 | lo.PerPage = perPage 51 | } 52 | 53 | func (lo ListOptions) Serialize(values url.Values, _ string) error { 54 | return serializeField(values, reflect.ValueOf(lo)) 55 | } 56 | 57 | func (lo *ListOptions) ToQueryString(subOptionsPtr any) (url.Values, error) { 58 | if subOptionsPtr != nil { 59 | values := url.Values{} 60 | err := serializeField(values, reflect.ValueOf(subOptionsPtr)) 61 | return values, err 62 | } 63 | return nil, nil 64 | } 65 | 66 | func serializeField(values url.Values, val reflect.Value) error { 67 | if val.Kind() == reflect.Ptr { 68 | if val.IsNil() { 69 | return nil 70 | } 71 | val = val.Elem() 72 | } 73 | if val.Kind() != reflect.Struct { 74 | return nil 75 | } 76 | valTypes := val.Type() 77 | for i := 0; i < valTypes.NumField(); i++ { 78 | fieldType := valTypes.Field(i) 79 | rawTag := fieldType.Tag.Get(filterTagName) 80 | if (rawTag != "" && rawTag != "-") || fieldType.Type.Implements(listOptionsSerializerType) { 81 | sv := val.Field(i) 82 | if sv.Kind() == reflect.Ptr { 83 | if sv.IsNil() { 84 | continue 85 | } 86 | sv = sv.Elem() 87 | } 88 | if sv.IsZero() { 89 | continue 90 | } 91 | svi := sv.Interface() 92 | if filter, ok := svi.(ListOptionsSerializer); ok { 93 | if err := filter.Serialize(values, rawTag); err != nil { 94 | return err 95 | } 96 | } else { 97 | values.Add(rawTag, fmt.Sprintf("%v", svi)) 98 | } 99 | } 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /client/manifest.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | 10 | internalhttp "github.com/cloudfoundry/go-cfclient/v3/internal/http" 11 | "github.com/cloudfoundry/go-cfclient/v3/internal/ios" 12 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 13 | "github.com/cloudfoundry/go-cfclient/v3/resource" 14 | ) 15 | 16 | type ManifestClient commonClient 17 | 18 | // Generate the specified app manifest as a yaml text string 19 | func (c *ManifestClient) Generate(ctx context.Context, appGUID string) (string, error) { 20 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.client.ApiURL(path.Format("/v3/apps/%s/manifest", appGUID)), nil) 21 | if err != nil { 22 | return "", fmt.Errorf("failed to create manifest request for app %s: %w", appGUID, err) 23 | } 24 | 25 | resp, err := c.client.ExecuteAuthRequest(req) 26 | if err != nil { 27 | return "", fmt.Errorf("failed to execute manifest request for app %s: %w", appGUID, err) 28 | } 29 | defer ios.Close(resp.Body) 30 | 31 | buf := new(strings.Builder) 32 | if _, err = io.Copy(buf, resp.Body); err != nil { 33 | return "", fmt.Errorf("failed to read manifest for app %s: %w", appGUID, err) 34 | } 35 | return buf.String(), nil 36 | } 37 | 38 | // ApplyManifest applies the changes specified in a manifest to the named apps and their underlying processes 39 | // asynchronously and returns a jobGUID. 40 | // 41 | // The apps must reside in the space. These changes are additive and will not modify any unspecified 42 | // properties or remove any existing environment variables, routes, or services. 43 | func (c *ManifestClient) ApplyManifest(ctx context.Context, spaceGUID string, manifest string) (string, error) { 44 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.client.ApiURL(path.Format("/v3/spaces/%s/actions/apply_manifest", spaceGUID)), strings.NewReader(manifest)) 45 | if err != nil { 46 | return "", fmt.Errorf("failed to create manifest apply request for space %s: %w", spaceGUID, err) 47 | } 48 | req.Header.Set("Content-Type", "application/x-yaml") 49 | 50 | resp, err := c.client.ExecuteAuthRequest(req) 51 | if err != nil { 52 | return "", fmt.Errorf("failed to upload manifest for space %s: %w", spaceGUID, err) 53 | } 54 | defer ios.Close(resp.Body) 55 | return internalhttp.DecodeJobID(resp), nil 56 | } 57 | 58 | // ManifestDiff compares the provided manifest against the current state of the space. 59 | func (c *ManifestClient) ManifestDiff(ctx context.Context, spaceGUID string, manifest string) (*resource.ManifestDiff, error) { 60 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.client.ApiURL(path.Format("/v3/spaces/%s/manifest_diff", spaceGUID)), strings.NewReader(manifest)) 61 | if err != nil { 62 | return nil, fmt.Errorf("failed to create manifest diff request for space %s: %w", spaceGUID, err) 63 | } 64 | req.Header.Set("Content-Type", "application/x-yaml") 65 | 66 | resp, err := c.client.ExecuteAuthRequest(req) 67 | if err != nil { 68 | return nil, fmt.Errorf("failed to execute manifest diff request for space %s: %w", spaceGUID, err) 69 | } 70 | defer ios.Close(resp.Body) 71 | var diff resource.ManifestDiff 72 | if err = internalhttp.DecodeBody(resp, &diff); err != nil { 73 | return nil, err 74 | } 75 | return &diff, nil 76 | } 77 | -------------------------------------------------------------------------------- /client/manifest_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestManifests(t *testing.T) { 14 | g := testutil.NewObjectJSONGenerator() 15 | manifest := g.Manifest().JSON 16 | 17 | tests := []RouteTest{ 18 | { 19 | Description: "Generate app manifest", 20 | Route: testutil.MockRoute{ 21 | Method: "GET", 22 | Endpoint: "/v3/apps/389f0d73-04ee-455b-b63c-513c7c78d5ff/manifest", 23 | Output: g.Single(manifest), 24 | Status: http.StatusOK}, 25 | Action: func(c *Client, t *testing.T) (any, error) { 26 | actual, err := c.Manifests.Generate(context.Background(), "389f0d73-04ee-455b-b63c-513c7c78d5ff") 27 | require.NoError(t, err) 28 | require.Equal(t, manifest, actual) 29 | return nil, nil 30 | }, 31 | }, 32 | } 33 | ExecuteTests(tests, t) 34 | } 35 | -------------------------------------------------------------------------------- /client/pager.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 7 | "github.com/cloudfoundry/go-cfclient/v3/resource" 8 | ) 9 | 10 | var ErrNoResultsReturned = errors.New("expected 1 or more results, but got 0") 11 | var ErrExactlyOneResultNotReturned = errors.New("expected exactly 1 result, but got less or more than 1") 12 | 13 | type Pager struct { 14 | NextPageReader *path.QuerystringReader 15 | PreviousPageReader *path.QuerystringReader 16 | 17 | TotalResults int 18 | TotalPages int 19 | } 20 | 21 | func NewPager(pagination resource.Pagination) *Pager { 22 | nextPageReader, _ := path.NewQuerystringReader(pagination.Next.Href) 23 | previousPageReader, _ := path.NewQuerystringReader(pagination.Previous.Href) 24 | 25 | return &Pager{ 26 | NextPageReader: nextPageReader, 27 | PreviousPageReader: previousPageReader, 28 | TotalResults: pagination.TotalResults, 29 | TotalPages: pagination.TotalPages, 30 | } 31 | } 32 | 33 | func (p *Pager) HasNextPage() bool { 34 | return p.NextPageReader != nil 35 | } 36 | 37 | func (p *Pager) NextPage(opts ListOptioner) { 38 | if p.HasNextPage() { 39 | page := p.NextPageReader.Int(PageField) 40 | perPage := p.NextPageReader.Int(PerPageField) 41 | opts.CurrentPage(page, perPage) 42 | } 43 | } 44 | 45 | func (p *Pager) HasPreviousPage() bool { 46 | return p.PreviousPageReader != nil 47 | } 48 | 49 | func (p *Pager) PreviousPage(opts ListOptioner) { 50 | if p.HasPreviousPage() { 51 | page := p.PreviousPageReader.Int(PageField) 52 | perPage := p.PreviousPageReader.Int(PerPageField) 53 | opts.CurrentPage(page, perPage) 54 | } 55 | } 56 | 57 | type ListFunc[T ListOptioner, R any] func(opts T) ([]R, *Pager, error) 58 | 59 | func AutoPage[T ListOptioner, R any](opts T, list ListFunc[T, R]) ([]R, error) { 60 | var all []R 61 | for { 62 | page, pager, err := list(opts) 63 | if err != nil { 64 | return nil, err 65 | } 66 | all = append(all, page...) 67 | if !pager.HasNextPage() { 68 | break 69 | } 70 | pager.NextPage(opts) 71 | } 72 | return all, nil 73 | } 74 | 75 | // Single returns a single object from the call to list or an error if matches > 1 or matches < 1 76 | func Single[T ListOptioner, R any](opts T, list ListFunc[T, R]) (R, error) { 77 | matches, _, err := list(opts) 78 | if err != nil { 79 | return *new(R), err 80 | } 81 | if len(matches) != 1 { 82 | return *new(R), ErrExactlyOneResultNotReturned 83 | } 84 | return matches[0], nil 85 | } 86 | 87 | // First returns the first object from the call to list or an error if matches < 1 88 | func First[T ListOptioner, R any](opts T, list ListFunc[T, R]) (R, error) { 89 | matches, _, err := list(opts) 90 | if err != nil { 91 | return *new(R), err 92 | } 93 | if len(matches) < 1 { 94 | return *new(R), ErrNoResultsReturned 95 | } 96 | return matches[0], nil 97 | } 98 | -------------------------------------------------------------------------------- /client/polling.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | var ErrAsyncProcessTimeout = errors.New("timed out after waiting for async process") 10 | 11 | type PollingOptions struct { 12 | Timeout time.Duration 13 | CheckInterval time.Duration 14 | FailedState string 15 | } 16 | 17 | func NewPollingOptions() *PollingOptions { 18 | return &PollingOptions{ 19 | FailedState: "FAILED", 20 | Timeout: time.Minute * 5, 21 | CheckInterval: time.Second, 22 | } 23 | } 24 | 25 | type getStateFunc func() (string, string, error) 26 | 27 | func PollForStateOrTimeout(getState getStateFunc, successState string, opts *PollingOptions) error { 28 | if opts == nil { 29 | opts = NewPollingOptions() 30 | } 31 | 32 | timeout := time.After(opts.Timeout) 33 | ticker := time.NewTicker(opts.CheckInterval) 34 | defer ticker.Stop() 35 | 36 | for { 37 | select { 38 | case <-timeout: 39 | return ErrAsyncProcessTimeout 40 | case <-ticker.C: 41 | state, cfError, err := getState() 42 | if err != nil { 43 | return err 44 | } 45 | switch state { 46 | case successState: 47 | return nil 48 | case opts.FailedState: 49 | return fmt.Errorf("received state %s while waiting for async process: %s", opts.FailedState, cfError) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/polling_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var CustomStagingErr = "StagingError - Staging error: Start command not specified" 11 | 12 | func TestNewPollingOptions(t *testing.T) { 13 | opts := NewPollingOptions() 14 | require.Equal(t, "FAILED", opts.FailedState) 15 | require.Equal(t, time.Minute*5, opts.Timeout) 16 | require.Equal(t, time.Second, opts.CheckInterval) 17 | } 18 | 19 | func TestPollForStateOrTimeout(t *testing.T) { 20 | noWaitOpts := NewPollingOptions() 21 | noWaitOpts.Timeout = time.Second 22 | noWaitOpts.CheckInterval = time.Millisecond 23 | 24 | failedFn := func() (string, string, error) { 25 | return "FAILED", CustomStagingErr, nil 26 | } 27 | successFn := func() (string, string, error) { 28 | return "SUCCESS", "", nil 29 | } 30 | timeoutFn := func() (string, string, error) { 31 | return "PROCESSING", "", nil 32 | } 33 | 34 | err := PollForStateOrTimeout(failedFn, "NOPE", noWaitOpts) 35 | require.Error(t, err) 36 | require.Equal(t, "received state FAILED while waiting for async process: "+CustomStagingErr, err.Error()) 37 | 38 | err = PollForStateOrTimeout(successFn, "SUCCESS", noWaitOpts) 39 | require.NoError(t, err) 40 | 41 | err = PollForStateOrTimeout(timeoutFn, "SUCCESS", noWaitOpts) 42 | require.Equal(t, ErrAsyncProcessTimeout, err) 43 | } 44 | -------------------------------------------------------------------------------- /client/resource_match.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/resource" 7 | ) 8 | 9 | type ResourceMatchClient commonClient 10 | 11 | // Create a list of cached resources from the input list 12 | func (c *ResourceMatchClient) Create(ctx context.Context, toMatch *resource.ResourceMatches) (*resource.ResourceMatches, error) { 13 | var matched resource.ResourceMatches 14 | _, err := c.client.post(ctx, "/v3/resource_matches", toMatch, &matched) 15 | if err != nil { 16 | return nil, err 17 | } 18 | return &matched, nil 19 | } 20 | -------------------------------------------------------------------------------- /client/resource_match_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 10 | ) 11 | 12 | func TestResourceMatches(t *testing.T) { 13 | g := testutil.NewObjectJSONGenerator() 14 | resourceMatch := g.ResourceMatch().JSON 15 | 16 | tests := []RouteTest{ 17 | { 18 | Description: "Create a resource match", 19 | Route: testutil.MockRoute{ 20 | Method: "POST", 21 | Endpoint: "/v3/resource_matches", 22 | Output: g.Single(resourceMatch), 23 | Status: http.StatusOK, 24 | PostForm: `{ 25 | "resources": [ 26 | { 27 | "checksum": { "value": "002d760bea1be268e27077412e11a320d0f164d3" }, 28 | "size_in_bytes": 36, 29 | "path": "C:\\path\\to\\file", 30 | "mode": "645" 31 | }, 32 | { 33 | "checksum": { "value": "a9993e364706816aba3e25717850c26c9cd0d89d" }, 34 | "size_in_bytes": 1, 35 | "path": "path/to/file", 36 | "mode": "644" 37 | } 38 | ] 39 | }`, 40 | }, 41 | Expected: resourceMatch, 42 | Action: func(c *Client, t *testing.T) (any, error) { 43 | toMatch := &resource.ResourceMatches{ 44 | Resources: []resource.ResourceMatch{ 45 | { 46 | Path: `C:\path\to\file`, 47 | Mode: "645", 48 | SizeInBytes: 36, 49 | Checksum: resource.ResourceMatchChecksum{ 50 | Value: "002d760bea1be268e27077412e11a320d0f164d3", 51 | }, 52 | }, 53 | { 54 | Path: `path/to/file`, 55 | Mode: "644", 56 | SizeInBytes: 1, 57 | Checksum: resource.ResourceMatchChecksum{ 58 | Value: "a9993e364706816aba3e25717850c26c9cd0d89d", 59 | }, 60 | }, 61 | }, 62 | } 63 | return c.ResourceMatches.Create(context.Background(), toMatch) 64 | }, 65 | }, 66 | } 67 | ExecuteTests(tests, t) 68 | } 69 | -------------------------------------------------------------------------------- /client/root.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/resource" 7 | ) 8 | 9 | // RootClient queries the global API root / 10 | type RootClient commonClient 11 | 12 | // Get queries the global API root / 13 | // 14 | // These endpoints link to other resources, endpoints, and external services that are relevant to 15 | // authenticated API clients. 16 | func (c *RootClient) Get(ctx context.Context) (*resource.Root, error) { 17 | var root resource.Root 18 | 19 | // NOTE - this will end up needlessly sending an auth header which the endpoint will ignore 20 | err := c.client.get(ctx, "/", &root) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return &root, nil 25 | } 26 | 27 | // GetV3 queries the V3 API root /v3 28 | // 29 | // This endpoint returns links to all the resources available on the v3 API. 30 | func (c *RootClient) GetV3(ctx context.Context) (*resource.V3Root, error) { 31 | var v3Root resource.V3Root 32 | // NOTE - this will end up needlessly sending an auth header which the endpoint will ignore 33 | if err := c.client.get(ctx, "/v3", &v3Root); err != nil { 34 | return nil, err 35 | } 36 | return &v3Root, nil 37 | } 38 | -------------------------------------------------------------------------------- /client/service_offering_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 10 | ) 11 | 12 | func TestServiceOfferings(t *testing.T) { 13 | g := testutil.NewObjectJSONGenerator() 14 | so := g.ServiceOffering().JSON 15 | so2 := g.ServiceOffering().JSON 16 | 17 | tests := []RouteTest{ 18 | { 19 | Description: "Delete service offering", 20 | Route: testutil.MockRoute{ 21 | Method: "DELETE", 22 | Endpoint: "/v3/service_offerings/928a32d9-8101-4b86-85a4-96e06f833c2d", 23 | Status: http.StatusNoContent, 24 | }, 25 | Action: func(c *Client, t *testing.T) (any, error) { 26 | err := c.ServiceOfferings.Delete(context.Background(), "928a32d9-8101-4b86-85a4-96e06f833c2d") 27 | return nil, err 28 | }, 29 | }, 30 | { 31 | Description: "Get service offering", 32 | Route: testutil.MockRoute{ 33 | Method: "GET", 34 | Endpoint: "/v3/service_offerings/928a32d9-8101-4b86-85a4-96e06f833c2d", 35 | Output: g.Single(so), 36 | Status: http.StatusOK}, 37 | Expected: so, 38 | Action: func(c *Client, t *testing.T) (any, error) { 39 | return c.ServiceOfferings.Get(context.Background(), "928a32d9-8101-4b86-85a4-96e06f833c2d") 40 | }, 41 | }, 42 | { 43 | Description: "List all service offerings", 44 | Route: testutil.MockRoute{ 45 | Method: "GET", 46 | Endpoint: "/v3/service_offerings", 47 | Output: g.Paged([]string{so}, []string{so2}), 48 | Status: http.StatusOK}, 49 | Expected: g.Array(so, so2), 50 | Action: func(c *Client, t *testing.T) (any, error) { 51 | return c.ServiceOfferings.ListAll(context.Background(), nil) 52 | }, 53 | }, 54 | { 55 | Description: "Update service offering", 56 | Route: testutil.MockRoute{ 57 | Method: "PATCH", 58 | Endpoint: "/v3/service_offerings/928a32d9-8101-4b86-85a4-96e06f833c2d", 59 | Output: g.Single(so), 60 | Status: http.StatusOK, 61 | PostForm: `{ 62 | "metadata": { 63 | "labels": {"key": "value"}, 64 | "annotations": {"note": "detailed information"} 65 | } 66 | }`, 67 | }, 68 | Expected: so, 69 | Action: func(c *Client, t *testing.T) (any, error) { 70 | r := &resource.ServiceOfferingUpdate{ 71 | Metadata: resource.NewMetadata(). 72 | WithLabel("", "key", "value"). 73 | WithAnnotation("", "note", "detailed information"), 74 | } 75 | return c.ServiceOfferings.Update(context.Background(), "928a32d9-8101-4b86-85a4-96e06f833c2d", r) 76 | }, 77 | }, 78 | } 79 | ExecuteTests(tests, t) 80 | } 81 | -------------------------------------------------------------------------------- /client/service_plan_visibility.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 7 | "github.com/cloudfoundry/go-cfclient/v3/resource" 8 | ) 9 | 10 | type ServicePlanVisibilityClient commonClient 11 | 12 | // Apply a service plan visibility. It behaves similar to the Update service plan visibility endpoint 13 | // but this endpoint will append to the existing list of organizations when the service plan is 14 | // organization visible 15 | func (c *ServicePlanVisibilityClient) Apply(ctx context.Context, servicePlanGUID string, r *resource.ServicePlanVisibility) (*resource.ServicePlanVisibility, error) { 16 | var res resource.ServicePlanVisibility 17 | _, err := c.client.post(ctx, path.Format("/v3/service_plans/%s/visibility", servicePlanGUID), r, &res) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return &res, nil 22 | } 23 | 24 | // Delete an organization from a service plan visibility list of organizations 25 | // It is only defined for service plans which are organization restricted 26 | func (c *ServicePlanVisibilityClient) Delete(ctx context.Context, servicePlanGUID, organizationGUID string) error { 27 | _, err := c.client.delete(ctx, path.Format("/v3/service_plans/%s/visibility/%s", servicePlanGUID, organizationGUID)) 28 | return err 29 | } 30 | 31 | // Get the specified service plan visibility 32 | func (c *ServicePlanVisibilityClient) Get(ctx context.Context, servicePlanGUID string) (*resource.ServicePlanVisibility, error) { 33 | var s resource.ServicePlanVisibility 34 | err := c.client.get(ctx, path.Format("/v3/service_plans/%s/visibility", servicePlanGUID), &s) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return &s, nil 39 | } 40 | 41 | // Update a service plan visibility. It behaves similar to Apply service plan visibility endpoint 42 | // but this endpoint will replace the existing list of organizations when the service plan is 43 | // organization visible 44 | func (c *ServicePlanVisibilityClient) Update(ctx context.Context, servicePlanGUID string, r *resource.ServicePlanVisibility) (*resource.ServicePlanVisibility, error) { 45 | var res resource.ServicePlanVisibility 46 | _, err := c.client.patch(ctx, path.Format("/v3/service_plans/%s/visibility", servicePlanGUID), r, &res) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return &res, nil 51 | } 52 | -------------------------------------------------------------------------------- /client/service_usage_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 9 | ) 10 | 11 | func TestServiceUsages(t *testing.T) { 12 | g := testutil.NewObjectJSONGenerator() 13 | serviceUsage := g.ServiceUsage().JSON 14 | serviceUsage2 := g.ServiceUsage().JSON 15 | serviceUsage3 := g.ServiceUsage().JSON 16 | 17 | tests := []RouteTest{ 18 | { 19 | Description: "Get service usage event", 20 | Route: testutil.MockRoute{ 21 | Method: "GET", 22 | Endpoint: "/v3/service_usage_events/cb4fb5eb-9b72-4696-b7bc-666696dec1b3", 23 | Output: g.Single(serviceUsage), 24 | Status: http.StatusOK}, 25 | Expected: serviceUsage, 26 | Action: func(c *Client, t *testing.T) (any, error) { 27 | return c.ServiceUsageEvents.Get(context.Background(), "cb4fb5eb-9b72-4696-b7bc-666696dec1b3") 28 | }, 29 | }, 30 | { 31 | Description: "List all service usage events", 32 | Route: testutil.MockRoute{ 33 | Method: "GET", 34 | Endpoint: "/v3/service_usage_events", 35 | Output: g.Paged([]string{serviceUsage, serviceUsage2}, []string{serviceUsage3}), 36 | Status: http.StatusOK}, 37 | Expected: g.Array(serviceUsage, serviceUsage2, serviceUsage3), 38 | Action: func(c *Client, t *testing.T) (any, error) { 39 | return c.ServiceUsageEvents.ListAll(context.Background(), nil) 40 | }, 41 | }, 42 | { 43 | Description: "Purge all service usage events", 44 | Route: testutil.MockRoute{ 45 | Method: "POST", 46 | Endpoint: "/v3/service_usage_events/actions/destructively_purge_all_and_reseed", 47 | Status: http.StatusOK}, 48 | Action: func(c *Client, t *testing.T) (any, error) { 49 | err := c.ServiceUsageEvents.Purge(context.Background()) 50 | return nil, err 51 | }, 52 | }, 53 | } 54 | ExecuteTests(tests, t) 55 | } 56 | -------------------------------------------------------------------------------- /client/sidecar_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/resource" 9 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 10 | ) 11 | 12 | func TestSidecars(t *testing.T) { 13 | g := testutil.NewObjectJSONGenerator() 14 | sidecar := g.Sidecar().JSON 15 | sidecar2 := g.Sidecar().JSON 16 | sidecar3 := g.Sidecar().JSON 17 | sidecar4 := g.Sidecar().JSON 18 | 19 | tests := []RouteTest{ 20 | { 21 | Description: "Create a sidecar", 22 | Route: testutil.MockRoute{ 23 | Method: "POST", 24 | Endpoint: "/v3/apps/631b46a1-c3b6-4599-9659-72c9fd54817f/sidecars", 25 | Output: g.Single(sidecar), 26 | Status: http.StatusCreated, 27 | PostForm: `{ 28 | "name": "auth-sidecar", 29 | "command": "bundle exec rackup", 30 | "process_types": ["web", "worker"], 31 | "memory_in_mb": 300 32 | }`, 33 | }, 34 | Expected: sidecar, 35 | Action: func(c *Client, t *testing.T) (any, error) { 36 | r := resource.NewSidecarCreate("auth-sidecar", "bundle exec rackup", []string{"web", "worker"}). 37 | WithMemoryInMB(300) 38 | return c.Sidecars.Create(context.Background(), "631b46a1-c3b6-4599-9659-72c9fd54817f", r) 39 | }, 40 | }, 41 | { 42 | Description: "Delete sidecar", 43 | Route: testutil.MockRoute{ 44 | Method: "DELETE", 45 | Endpoint: "/v3/sidecars/319ac7e8-e34a-4b6f-89da-1753ad3ece93", 46 | Status: http.StatusNoContent, 47 | }, 48 | Action: func(c *Client, t *testing.T) (any, error) { 49 | return nil, c.Sidecars.Delete(context.Background(), "319ac7e8-e34a-4b6f-89da-1753ad3ece93") 50 | }, 51 | }, 52 | { 53 | Description: "Get sidecar", 54 | Route: testutil.MockRoute{ 55 | Method: "GET", 56 | Endpoint: "/v3/sidecars/319ac7e8-e34a-4b6f-89da-1753ad3ece93", 57 | Output: g.Single(sidecar), 58 | Status: http.StatusOK}, 59 | Expected: sidecar, 60 | Action: func(c *Client, t *testing.T) (any, error) { 61 | return c.Sidecars.Get(context.Background(), "319ac7e8-e34a-4b6f-89da-1753ad3ece93") 62 | }, 63 | }, 64 | { 65 | Description: "List all sidecars for app", 66 | Route: testutil.MockRoute{ 67 | Method: "GET", 68 | Endpoint: "/v3/apps/631b46a1-c3b6-4599-9659-72c9fd54817f/sidecars", 69 | Output: g.Paged([]string{sidecar, sidecar2}, []string{sidecar3, sidecar4}), 70 | Status: http.StatusOK}, 71 | Expected: g.Array(sidecar, sidecar2, sidecar3, sidecar4), 72 | Action: func(c *Client, t *testing.T) (any, error) { 73 | return c.Sidecars.ListForAppAll(context.Background(), "631b46a1-c3b6-4599-9659-72c9fd54817f", nil) 74 | }, 75 | }, 76 | { 77 | Description: "List all sidecars for process", 78 | Route: testutil.MockRoute{ 79 | Method: "GET", 80 | Endpoint: "/v3/processes/0d2da177-c801-42a0-a6ca-ee4b10334954/sidecars", 81 | Output: g.Paged([]string{sidecar, sidecar2}, []string{sidecar3, sidecar4}), 82 | Status: http.StatusOK}, 83 | Expected: g.Array(sidecar, sidecar2, sidecar3, sidecar4), 84 | Action: func(c *Client, t *testing.T) (any, error) { 85 | return c.Sidecars.ListForProcessAll(context.Background(), "0d2da177-c801-42a0-a6ca-ee4b10334954", nil) 86 | }, 87 | }, 88 | } 89 | ExecuteTests(tests, t) 90 | } 91 | -------------------------------------------------------------------------------- /client/space_feature.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/internal/path" 7 | "github.com/cloudfoundry/go-cfclient/v3/resource" 8 | ) 9 | 10 | type SpaceFeatureClient commonClient 11 | 12 | // EnableSSH toggles the SSH feature for a space 13 | func (c *SpaceFeatureClient) EnableSSH(ctx context.Context, spaceGUID string, enable bool) error { 14 | r := resource.SpaceFeatureUpdate{ 15 | Enabled: enable, 16 | } 17 | _, err := c.client.patch(ctx, path.Format("/v3/spaces/%s/features/ssh", spaceGUID), r, nil) 18 | return err 19 | } 20 | 21 | // IsSSHEnabled returns true if SSH is enabled for the specified space 22 | func (c *SpaceFeatureClient) IsSSHEnabled(ctx context.Context, spaceGUID string) (bool, error) { 23 | var sf resource.SpaceFeature 24 | err := c.client.get(ctx, path.Format("/v3/spaces/%s/features/ssh", spaceGUID), &sf) 25 | if err != nil { 26 | return false, err 27 | } 28 | return sf.Enabled, nil 29 | } 30 | -------------------------------------------------------------------------------- /client/space_feature_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 9 | ) 10 | 11 | func TestSpaceFeatures(t *testing.T) { 12 | tests := []RouteTest{ 13 | { 14 | Description: "Enable SSH for a space", 15 | Route: testutil.MockRoute{ 16 | Method: "PATCH", 17 | Endpoint: "/v3/spaces/000d1e0c-218e-470b-b5db-84481b89fa92/features/ssh", 18 | Output: []string{`{ 19 | "name": "ssh", 20 | "description": "Enable SSHing into apps in the space.", 21 | "enabled": true 22 | }`}, 23 | Status: http.StatusOK, 24 | PostForm: `{ "enabled": true }`, 25 | }, 26 | Action: func(c *Client, t *testing.T) (any, error) { 27 | err := c.SpaceFeatures.EnableSSH(context.Background(), "000d1e0c-218e-470b-b5db-84481b89fa92", true) 28 | return nil, err 29 | }, 30 | }, 31 | { 32 | Description: "Is SSH enabled for a space", 33 | Route: testutil.MockRoute{ 34 | Method: "GET", 35 | Endpoint: "/v3/spaces/000d1e0c-218e-470b-b5db-84481b89fa92/features/ssh", 36 | Output: []string{`{ 37 | "name": "ssh", 38 | "description": "Enable SSHing into apps in the space.", 39 | "enabled": true 40 | }`}, 41 | Status: http.StatusOK, 42 | }, 43 | Expected: "true", 44 | Action: func(c *Client, t *testing.T) (any, error) { 45 | return c.SpaceFeatures.IsSSHEnabled(context.Background(), "000d1e0c-218e-470b-b5db-84481b89fa92") 46 | }, 47 | }, 48 | } 49 | ExecuteTests(tests, t) 50 | } 51 | -------------------------------------------------------------------------------- /client/test_runner.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/cloudfoundry/go-cfclient/v3/config" 10 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type RouteTest struct { 16 | Description string 17 | Route testutil.MockRoute 18 | Expected string 19 | Expected2 string 20 | Expected3 string 21 | Action func(c *Client, t *testing.T) (any, error) 22 | Action2 func(c *Client, t *testing.T) (any, any, error) 23 | Action3 func(c *Client, t *testing.T) (any, any, any, error) 24 | } 25 | 26 | func ExecuteTests(tests []RouteTest, t *testing.T) { 27 | for _, tt := range tests { 28 | func() { 29 | serverURL := testutil.Setup(tt.Route, t) 30 | defer testutil.Teardown() 31 | details := fmt.Sprintf("%s %s", tt.Route.Method, tt.Route.Endpoint) 32 | if tt.Description != "" { 33 | details = tt.Description + ": " + details 34 | } 35 | 36 | c, _ := config.New(serverURL, config.Token("", "fake-refresh-token")) 37 | cl, err := New(c) 38 | require.NoError(t, err, details) 39 | 40 | assertEq := func(t *testing.T, expected string, obj any) { 41 | if isJSON(expected) { 42 | actualJSON, err := json.Marshal(obj) 43 | require.NoError(t, err, details) 44 | require.JSONEq(t, expected, string(actualJSON), details) 45 | } else { 46 | if s, ok := obj.(string); ok { 47 | require.Equal(t, expected, s, details) 48 | } 49 | } 50 | } 51 | 52 | if tt.Action != nil { 53 | obj1, err := tt.Action(cl, t) 54 | require.NoError(t, err, details) 55 | assertEq(t, tt.Expected, obj1) 56 | } else if tt.Action2 != nil { 57 | obj1, obj2, err := tt.Action2(cl, t) 58 | require.NoError(t, err, details) 59 | assertEq(t, tt.Expected, obj1) 60 | assertEq(t, tt.Expected2, obj2) 61 | } else if tt.Action3 != nil { 62 | obj1, obj2, obj3, err := tt.Action3(cl, t) 63 | require.NoError(t, err, details) 64 | assertEq(t, tt.Expected, obj1) 65 | assertEq(t, tt.Expected2, obj2) 66 | assertEq(t, tt.Expected3, obj3) 67 | } 68 | }() 69 | } 70 | } 71 | 72 | func isJSON(obj string) bool { 73 | return strings.HasPrefix(obj, "{") || strings.HasPrefix(obj, "[") 74 | } 75 | -------------------------------------------------------------------------------- /config/cf_cli.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // cfCLIConfig is the CF CLI configuration. 11 | type cfCLIConfig struct { 12 | AccessToken string 13 | RefreshToken string 14 | Target string 15 | AuthorizationEndpoint string 16 | UaaEndpoint string 17 | UAAOAuthClient string 18 | UAAOAuthClientSecret string 19 | UAAGrantType string 20 | SSHOAuthClient string 21 | SSLDisabled bool 22 | } 23 | 24 | // findCFHomeDir finds the CF Home directory. 25 | func findCFHomeDir() (string, error) { 26 | cfHomeDir := os.Getenv("CF_HOME") 27 | if cfHomeDir != "" { 28 | return cfHomeDir, nil 29 | } 30 | userHomeDir, err := os.UserHomeDir() 31 | if err != nil { 32 | return "", fmt.Errorf("failed to determine user's home directory: %w", err) 33 | } 34 | return userHomeDir, nil 35 | } 36 | 37 | // createConfigFromCFCLIConfig reads the CF Home configuration from the specified directory. 38 | func loadCFCLIConfig(cfHomeDir string) (*cfCLIConfig, error) { 39 | configFile := filepath.Join(filepath.Join(cfHomeDir, ".cf"), "config.json") 40 | cfJSON, err := os.ReadFile(configFile) 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to read %s: %w", configFile, err) 43 | } 44 | var cfgHome cfCLIConfig 45 | if err = json.Unmarshal(cfJSON, &cfgHome); err != nil { 46 | return nil, fmt.Errorf("error while unmarshalling CF CLI config: %w", err) 47 | } 48 | return &cfgHome, nil 49 | } 50 | -------------------------------------------------------------------------------- /config/cf_cli_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestLoadCFCLIConfig(t *testing.T) { 12 | cfHomeDir := writeTestCFCLIConfig(t) 13 | cf, err := loadCFCLIConfig(cfHomeDir) 14 | require.NoError(t, err) 15 | require.Equal(t, "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRlc3QgY2YgdG9rZW4iLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.mLvUvu-ED_lIkyI3UTXS_hUEPPFdI0BdNqRMgMThAhk", cf.AccessToken) 16 | require.Equal(t, "secret-refresh-token", cf.RefreshToken) 17 | require.Equal(t, "https://api.sys.example.com", cf.Target) 18 | require.Equal(t, "https://login.sys.example.com", cf.AuthorizationEndpoint) 19 | require.Equal(t, "https://uaa.sys.example.com", cf.UaaEndpoint) 20 | require.Equal(t, "cf", cf.UAAOAuthClient) 21 | require.Equal(t, "", cf.UAAOAuthClientSecret) 22 | require.Equal(t, "", cf.UAAGrantType) 23 | require.Equal(t, "ssh-proxy", cf.SSHOAuthClient) 24 | require.True(t, cf.SSLDisabled) 25 | } 26 | 27 | func writeTestCFCLIConfig(t *testing.T) string { 28 | cfHomeDir, err := os.MkdirTemp("", "cf_home") 29 | require.NoError(t, err) 30 | 31 | configDir := path.Join(cfHomeDir, ".cf") 32 | err = os.MkdirAll(configDir, 0744) 33 | require.NoError(t, err) 34 | 35 | configPath := path.Join(configDir, "config.json") 36 | err = os.WriteFile(configPath, []byte(cfCLIConfigJSON), 0744) 37 | require.NoError(t, err) 38 | 39 | return cfHomeDir 40 | } 41 | 42 | const cfCLIConfigJSON = ` 43 | { 44 | "ConfigVersion": 3, 45 | "Target": "https://api.sys.example.com", 46 | "APIVersion": "2.164.0", 47 | "AuthorizationEndpoint": "https://login.sys.example.com", 48 | "DopplerEndPoint": "wss://doppler.sys.example.com:443", 49 | "UaaEndpoint": "https://uaa.sys.example.com", 50 | "RoutingAPIEndpoint": "https://api.sys.example.com/routing", 51 | "AccessToken": "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRlc3QgY2YgdG9rZW4iLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.mLvUvu-ED_lIkyI3UTXS_hUEPPFdI0BdNqRMgMThAhk", 52 | "SSHOAuthClient": "ssh-proxy", 53 | "UAAOAuthClient": "cf", 54 | "UAAOAuthClientSecret": "", 55 | "UAAGrantType": "", 56 | "RefreshToken": "secret-refresh-token", 57 | "OrganizationFields": { 58 | "GUID": "42754be1-f558-4d28-9c06-c706f6641245", 59 | "Name": "system", 60 | "QuotaDefinition": { 61 | "guid": "", 62 | "name": "", 63 | "memory_limit": 0, 64 | "instance_memory_limit": 0, 65 | "total_routes": 0, 66 | "total_services": 0, 67 | "non_basic_services_allowed": false, 68 | "app_instance_limit": 0, 69 | "total_reserved_route_ports": 0 70 | } 71 | }, 72 | "SpaceFields": { 73 | "GUID": "e42ccfe9-04bf-4cbc-ae16-f26741778a71", 74 | "Name": "system", 75 | "AllowSSH": true 76 | }, 77 | "SSLDisabled": true, 78 | "AsyncTimeout": 0, 79 | "Trace": "", 80 | "ColorEnabled": "", 81 | "Locale": "", 82 | "PluginRepos": [ 83 | { 84 | "Name": "CF-Community", 85 | "URL": "https://plugins.cloudfoundry.org" 86 | } 87 | ], 88 | "MinCLIVersion": "6.23.0", 89 | "MinRecommendedCLIVersion": "6.23.0" 90 | } 91 | ` 92 | -------------------------------------------------------------------------------- /examples/droplet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/cloudfoundry/go-cfclient/v3/client" 11 | "github.com/cloudfoundry/go-cfclient/v3/config" 12 | ) 13 | 14 | func main() { 15 | err := execute() 16 | if err != nil { 17 | fmt.Println(err) 18 | os.Exit(1) 19 | } 20 | fmt.Println("Done!") 21 | } 22 | 23 | func execute() error { 24 | ctx := context.Background() 25 | conf, err := config.NewFromCFHome(config.SkipTLSValidation()) 26 | if err != nil { 27 | return err 28 | } 29 | cf, err := client.New(conf) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | droplets, err := cf.Droplets.ListAll(ctx, nil) 35 | if err != nil { 36 | return err 37 | } 38 | if len(droplets) < 1 { 39 | return errors.New("error listing droplets, expected at least one droplet") 40 | } 41 | droplet := droplets[0] 42 | 43 | reader, err := cf.Droplets.Download(ctx, droplet.GUID) 44 | if err != nil { 45 | return err 46 | } 47 | defer func() { _ = reader.Close() }() 48 | 49 | dropletFile, err := os.CreateTemp("", "droplet-*.zip") 50 | if err != nil { 51 | return err 52 | } 53 | defer func() { _ = dropletFile.Close() }() 54 | 55 | fmt.Printf("Writing droplet %s to %s\n", droplet.GUID, dropletFile.Name()) 56 | _, err = io.Copy(dropletFile, reader) 57 | return err 58 | } 59 | -------------------------------------------------------------------------------- /examples/metadata/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/client" 9 | "github.com/cloudfoundry/go-cfclient/v3/config" 10 | "github.com/cloudfoundry/go-cfclient/v3/resource" 11 | ) 12 | 13 | func main() { 14 | err := execute() 15 | if err != nil { 16 | fmt.Println(err) 17 | os.Exit(1) 18 | } 19 | fmt.Println("Done!") 20 | } 21 | 22 | func execute() error { 23 | ctx := context.Background() 24 | conf, err := config.NewFromCFHome(config.SkipTLSValidation()) 25 | if err != nil { 26 | return err 27 | } 28 | cf, err := client.New(conf) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | // grab the first org we find 34 | org, err := cf.Organizations.First(ctx, nil) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | // add a label and annotation 40 | fmt.Printf("adding metadata label and annotation to org %s\n", org.Name) 41 | m := &resource.Metadata{} 42 | m.SetLabel("", "example-label1", "short-label") 43 | m.SetLabel("cf.example.org", "example-label2", "prefixed-label") 44 | m.SetAnnotation("", "example-annotation1", "short-annotation") 45 | m.SetAnnotation("cf.example.org", "example-annotation2", "prefixed-annotation") 46 | orgUpdate := &resource.OrganizationUpdate{ 47 | Metadata: m, 48 | } 49 | org, err = cf.Organizations.Update(ctx, org.GUID, orgUpdate) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | fmt.Printf("org %s metadata:\n", org.Name) 55 | printMap(org.Metadata.Labels) 56 | printMap(org.Metadata.Annotations) 57 | 58 | // now clear out the metadata we added 59 | fmt.Printf("clearing metadata label and annotation from org %s\n", org.Name) 60 | orgUpdate.Metadata.Clear() 61 | org, err = cf.Organizations.Update(ctx, org.GUID, orgUpdate) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | fmt.Printf("org %s metadata:\n", org.Name) 67 | printMap(org.Metadata.Labels) 68 | printMap(org.Metadata.Annotations) 69 | 70 | return nil 71 | } 72 | 73 | func printMap(m map[string]*string) { 74 | for k, v := range m { 75 | fmt.Printf("%s=%s\n", k, *v) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/push_app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "gopkg.in/yaml.v3" 9 | 10 | "github.com/cloudfoundry/go-cfclient/v3/client" 11 | "github.com/cloudfoundry/go-cfclient/v3/config" 12 | "github.com/cloudfoundry/go-cfclient/v3/operation" 13 | ) 14 | 15 | func main() { 16 | if len(os.Args) != 4 { 17 | fmt.Println("expected arguments: org, space, /path/to/spring-music.jar") 18 | os.Exit(1) 19 | } 20 | org := os.Args[1] 21 | space := os.Args[2] 22 | pathToZip := os.Args[3] 23 | 24 | err := runPush(org, space, pathToZip) 25 | if err != nil { 26 | fmt.Println(err) 27 | os.Exit(1) 28 | } 29 | fmt.Println("Done!") 30 | } 31 | 32 | func runPush(org, space, pathToZip string) error { 33 | ctx := context.Background() 34 | conf, err := config.NewFromCFHome(config.SkipTLSValidation()) 35 | if err != nil { 36 | return err 37 | } 38 | cf, err := client.New(conf) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | var manifest *operation.Manifest 44 | err = yaml.Unmarshal([]byte(yamlManifest), &manifest) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | zipFile, err := os.Open(pathToZip) 50 | if err != nil { 51 | return err 52 | } 53 | pushOp := operation.NewAppPushOperation(cf, org, space) 54 | app, err := pushOp.Push(ctx, manifest.Applications[0], zipFile) 55 | if err != nil { 56 | return err 57 | } 58 | fmt.Printf("successfully pushed %s, state: %s\n", app.Name, app.State) 59 | return nil 60 | } 61 | 62 | const yamlManifest = ` 63 | --- 64 | applications: 65 | - name: spring-music-example 66 | memory: 1G 67 | random-route: true 68 | stack: cflinuxfs3 69 | buildpacks: 70 | - java_buildpack_offline 71 | ` 72 | -------------------------------------------------------------------------------- /examples/roles/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/client" 9 | "github.com/cloudfoundry/go-cfclient/v3/config" 10 | "github.com/cloudfoundry/go-cfclient/v3/resource" 11 | ) 12 | 13 | func main() { 14 | err := execute() 15 | if err != nil { 16 | fmt.Println(err) 17 | os.Exit(1) 18 | } 19 | fmt.Println("Done!") 20 | } 21 | 22 | func execute() error { 23 | ctx := context.Background() 24 | conf, err := config.NewFromCFHome(config.SkipTLSValidation()) 25 | if err != nil { 26 | return err 27 | } 28 | cf, err := client.New(conf) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | err = listSpaceDevsInSpace(ctx, cf) 34 | if err != nil { 35 | return err 36 | } 37 | return listAllSpaceDevelopers(ctx, cf) 38 | } 39 | 40 | func listSpaceDevsInSpace(ctx context.Context, cf *client.Client) error { 41 | // grab the first space 42 | space, err := cf.Spaces.First(ctx, nil) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // list space developer roles and users in the space 48 | opts := client.NewRoleListOptions() 49 | opts.SpaceGUIDs.EqualTo(space.GUID) 50 | opts.WithSpaceRoleType(resource.SpaceRoleDeveloper) 51 | roles, users, err := cf.Roles.ListIncludeUsersAll(ctx, opts) 52 | if err != nil { 53 | return err 54 | } 55 | for _, r := range roles { 56 | fmt.Printf("%s - %s\n", r.Type, r.GUID) 57 | } 58 | for _, u := range users { 59 | fmt.Printf("%s - %s\n", *u.Username, u.GUID) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func listAllSpaceDevelopers(ctx context.Context, cf *client.Client) error { 66 | opts := client.NewRoleListOptions() 67 | opts.WithSpaceRoleType(resource.SpaceRoleDeveloper) 68 | _, users, err := cf.Roles.ListIncludeUsersAll(ctx, opts) 69 | if err != nil { 70 | return err 71 | } 72 | for _, u := range users { 73 | fmt.Printf("%s - %s\n", *u.Username, u.GUID) 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /examples/ssh_code/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/client" 9 | "github.com/cloudfoundry/go-cfclient/v3/config" 10 | ) 11 | 12 | func main() { 13 | err := execute() 14 | if err != nil { 15 | fmt.Println(err) 16 | os.Exit(1) 17 | } 18 | fmt.Println("Done!") 19 | } 20 | 21 | func execute() error { 22 | ctx := context.Background() 23 | conf, err := config.NewFromCFHome(config.SkipTLSValidation()) 24 | if err != nil { 25 | return err 26 | } 27 | cf, err := client.New(conf) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | code, err := cf.SSHCode(ctx) 33 | if err != nil { 34 | return err 35 | } 36 | fmt.Printf("SSH Code: %s\n", code) 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /examples/svc_creds/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/cloudfoundry/go-cfclient/v3/client" 9 | "github.com/cloudfoundry/go-cfclient/v3/config" 10 | "github.com/cloudfoundry/go-cfclient/v3/resource" 11 | ) 12 | 13 | func main() { 14 | err := execute() 15 | if err != nil { 16 | fmt.Println(err) 17 | os.Exit(1) 18 | } 19 | fmt.Println("Done!") 20 | } 21 | 22 | func execute() error { 23 | ctx := context.Background() 24 | conf, err := config.NewFromCFHome(config.SkipTLSValidation()) 25 | if err != nil { 26 | return err 27 | } 28 | cf, err := client.New(conf) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | bindings, err := cf.ServiceCredentialBindings.ListAll(ctx, nil) 34 | if err != nil { 35 | return err 36 | } 37 | for _, b := range bindings { 38 | fmt.Printf("GUID=%s, App=%s\n", b.GUID, b.Relationships.App.Data.GUID) 39 | details, err := cf.ServiceCredentialBindings.GetDetails(ctx, b.GUID) 40 | if err != nil { 41 | return err 42 | } 43 | fmt.Printf("%s\n", details.Credentials) 44 | params, err := cf.ServiceCredentialBindings.GetParameters(ctx, b.GUID) 45 | if resource.IsServiceFetchBindingParametersNotSupportedError(err) { 46 | fmt.Println(err.(resource.CloudFoundryError).Detail) 47 | } else if err != nil { 48 | return err 49 | } else { 50 | fmt.Printf("%v\n", params) 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudfoundry/go-cfclient/v3 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab 9 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 10 | github.com/stretchr/testify v1.10.0 11 | golang.org/x/oauth2 v0.30.0 12 | gopkg.in/yaml.v2 v2.4.0 13 | gopkg.in/yaml.v3 v3.0.1 14 | ) 15 | 16 | require ( 17 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | github.com/stretchr/objx v0.5.2 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= 2 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= 7 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= 8 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw= 9 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI= 10 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= 11 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 16 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 17 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 18 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 19 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 20 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 21 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 25 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /internal/check/check.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // IsPointer can be used to check arguments passed as type any are in fact of type pointer 8 | func IsPointer(v any) bool { 9 | rv := reflect.ValueOf(v) 10 | return rv.Kind() == reflect.Pointer 11 | } 12 | 13 | // IsNil can be used to check arguments passed as type any are in fact nil 14 | func IsNil(v any) bool { 15 | if v == nil { 16 | return true 17 | } 18 | rv := reflect.ValueOf(v) 19 | if rv.Kind() != reflect.Pointer { 20 | return false 21 | } 22 | return rv.IsNil() 23 | } 24 | -------------------------------------------------------------------------------- /internal/check/check_test.go: -------------------------------------------------------------------------------- 1 | package check_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudfoundry/go-cfclient/v3/internal/check" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type Y struct{} 12 | 13 | func TestIsPointer(t *testing.T) { 14 | var s Y 15 | var p *Y 16 | 17 | require.False(t, check.IsPointer(nil)) 18 | require.False(t, check.IsPointer(s)) 19 | require.True(t, check.IsPointer(&p)) 20 | require.True(t, check.IsPointer(p)) 21 | } 22 | 23 | func TestIsNil(t *testing.T) { 24 | var s Y 25 | var p *Y 26 | 27 | require.False(t, check.IsNil(s)) 28 | require.False(t, check.IsNil(&p)) 29 | require.True(t, check.IsNil(p)) 30 | require.True(t, check.IsNil(nil)) 31 | 32 | } 33 | 34 | func TestIsNilAndIsPointer(t *testing.T) { 35 | var s Y 36 | var p *Y 37 | 38 | require.True(t, !check.IsNil(s) && !check.IsPointer(s)) 39 | require.False(t, !check.IsNil(p) && !check.IsPointer(p)) 40 | require.False(t, !check.IsNil(nil) && !check.IsPointer(nil)) 41 | } 42 | -------------------------------------------------------------------------------- /internal/http/client_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "golang.org/x/oauth2" 7 | gohttp "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/cloudfoundry/go-cfclient/v3/internal/http" 12 | "github.com/cloudfoundry/go-cfclient/v3/testutil" 13 | 14 | "github.com/stretchr/testify/mock" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | type MockedOAuthTokenSourceCreator struct { 19 | mock.Mock 20 | } 21 | 22 | func (tsc *MockedOAuthTokenSourceCreator) CreateOAuth2TokenSource(ctx context.Context) (oauth2.TokenSource, error) { 23 | args := tsc.Called(ctx) 24 | return args.Get(0).(oauth2.TokenSource), args.Error(1) 25 | } 26 | 27 | type MockedOAuthTokenSource struct { 28 | mock.Mock 29 | } 30 | 31 | func (ts *MockedOAuthTokenSource) Token() (*oauth2.Token, error) { 32 | args := ts.Called() 33 | return args.Get(0).(*oauth2.Token), args.Error(1) 34 | } 35 | 36 | func TestOAuthSessionManager(t *testing.T) { 37 | g := testutil.NewObjectJSONGenerator() 38 | serverURL := testutil.SetupMultiple([]testutil.MockRoute{ 39 | { 40 | Method: "POST", 41 | Endpoint: "/v3/organizations", 42 | Output: []string{"auth error", g.Organization().JSON}, 43 | Statuses: []int{401, 201}, 44 | UserAgent: "Go-http-client/1.1", 45 | }, 46 | { 47 | Method: "GET", 48 | Endpoint: "/v3/spaces", 49 | Output: []string{"auth error", "spaces[]"}, 50 | Statuses: []int{401, 200}, 51 | UserAgent: "Go-http-client/1.1", 52 | }, 53 | }, t) 54 | defer testutil.Teardown() 55 | 56 | token := &oauth2.Token{ 57 | AccessToken: "access", 58 | RefreshToken: "refresh", 59 | Expiry: time.Now().Add(time.Minute), 60 | } 61 | 62 | tokenSrc := &MockedOAuthTokenSource{} 63 | tokenSrc.On("Token").Return(token, nil) 64 | 65 | tokenSrcCreator := &MockedOAuthTokenSourceCreator{} 66 | tokenSrcCreator.On("CreateOAuth2TokenSource", context.Background()).Return(tokenSrc, nil) 67 | 68 | client, err := http.NewAuthenticatedClient(context.Background(), gohttp.DefaultClient, tokenSrcCreator) 69 | require.NoError(t, err) 70 | 71 | resp, err := client.Post(serverURL+"/v3/organizations", "application/json", bytes.NewBufferString(g.Organization().JSON)) 72 | require.NoError(t, err) 73 | require.Equal(t, 201, resp.StatusCode) 74 | 75 | // to the caller the retry is transparent on 401 76 | resp, err = client.Get(serverURL + "/v3/spaces") 77 | require.NoError(t, err) 78 | require.Equal(t, 200, resp.StatusCode) 79 | 80 | tokenSrcCreator.AssertNumberOfCalls(t, "CreateOAuth2TokenSource", 3) 81 | } 82 | -------------------------------------------------------------------------------- /internal/http/request.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | ) 12 | 13 | const ( 14 | MaxRedirects = 10 15 | ErrMaxRedirects = "stopped after maximum allowed redirects" 16 | ) 17 | 18 | // contextKey is a private static type to avoid potential collisions. 19 | type contextKey struct{} 20 | 21 | // ignoreRedirectKey is a context key used for storing the redirect ignore flag. 22 | var ignoreRedirectKey = contextKey{} 23 | 24 | // IgnoreRedirect sets a flag in the request's context to indicate that redirects should be ignored. 25 | func IgnoreRedirect(req *http.Request) *http.Request { 26 | if req == nil { 27 | return nil 28 | } 29 | return req.WithContext(context.WithValue(req.Context(), ignoreRedirectKey, true)) 30 | } 31 | 32 | // IsIgnoredRedirect checks if the 'ignore redirect' flag is set in the request's context. 33 | func IsIgnoredRedirect(req *http.Request) bool { 34 | if req == nil { 35 | return false 36 | } 37 | v, ok := req.Context().Value(ignoreRedirectKey).(bool) 38 | return ok && v 39 | } 40 | 41 | // CheckRedirect checks the redirect policy for the HTTP client. 42 | func CheckRedirect(req *http.Request, via []*http.Request) error { 43 | if IsIgnoredRedirect(req) { 44 | return http.ErrUseLastResponse 45 | } 46 | if len(via) >= MaxRedirects { 47 | return errors.New(ErrMaxRedirects) 48 | } 49 | return nil 50 | } 51 | 52 | func EncodeBody(obj any) (io.Reader, error) { 53 | if obj == nil { 54 | return nil, nil 55 | } 56 | buf := new(bytes.Buffer) 57 | if err := json.NewEncoder(buf).Encode(obj); err != nil { 58 | return nil, fmt.Errorf("error encoding object to JSON: %w", err) 59 | } 60 | return buf, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/http/request_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRequest(t *testing.T) { 12 | t.Run("Test IsIgnoredRedirect with default request", func(t *testing.T) { 13 | r, _ := http.NewRequestWithContext(context.Background(), "GET", "/v3/apps", nil) 14 | require.False(t, IsIgnoredRedirect(r)) 15 | }) 16 | 17 | t.Run("Test CheckRedirect with default request", func(t *testing.T) { 18 | r, _ := http.NewRequestWithContext(context.Background(), "GET", "/v3/apps", nil) 19 | require.Nil(t, CheckRedirect(r, nil)) 20 | }) 21 | 22 | t.Run("Test CheckRedirect with max redirects exceeded", func(t *testing.T) { 23 | r, _ := http.NewRequestWithContext(context.Background(), "GET", "/v3/apps", nil) 24 | err := CheckRedirect(r, make([]*http.Request, 11)) 25 | require.Error(t, err) 26 | require.Equal(t, ErrMaxRedirects, err.Error()) 27 | }) 28 | 29 | t.Run("Test IgnoreRedirect and IsIgnoredRedirect", func(t *testing.T) { 30 | r, _ := http.NewRequestWithContext(context.Background(), "GET", "/v3/apps", nil) 31 | r = IgnoreRedirect(r) 32 | require.True(t, IsIgnoredRedirect(r)) 33 | 34 | err := CheckRedirect(r, nil) 35 | require.ErrorIs(t, err, http.ErrUseLastResponse) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /internal/http/response.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/cloudfoundry/go-cfclient/v3/internal/ios" 12 | "github.com/cloudfoundry/go-cfclient/v3/resource" 13 | ) 14 | 15 | // DecodeJobIDAndBody returns the jobGUID if specified in the Location response header and 16 | // unmarshalls the JSON response body to result if available 17 | func DecodeJobIDAndBody(resp *http.Response, result any) (string, error) { 18 | return DecodeJobID(resp), DecodeBody(resp, result) 19 | } 20 | 21 | // DecodeJobIDOrBody returns the jobGUID if specified in the Location response header or 22 | // unmarshalls the JSON response body if no job ID and result is non nil 23 | func DecodeJobIDOrBody(resp *http.Response, result any) (string, error) { 24 | if jobGUID := DecodeJobID(resp); jobGUID != "" { 25 | return jobGUID, nil 26 | } 27 | return "", DecodeBody(resp, result) 28 | } 29 | 30 | // DecodeJobID returns the jobGUID if specified in the Location response header 31 | func DecodeJobID(resp *http.Response) string { 32 | // Return empty if the response is nil 33 | if resp == nil { 34 | return "" 35 | } 36 | 37 | // Extract the Location header 38 | location, err := resp.Location() 39 | if err != nil { 40 | // Return empty if there's an error (e.g., no Location header) 41 | return "" 42 | } 43 | 44 | // Split the path in the URL and check for the 'jobs' segment 45 | parts := strings.Split(location.Path, "/") 46 | numParts := len(parts) 47 | // Ensure 'jobs' is the second last element and return the last element as job ID 48 | if numParts >= 2 && parts[numParts-2] == "jobs" { 49 | return parts[numParts-1] 50 | } 51 | 52 | // Return empty if the URL doesn't follow the expected pattern 53 | return "" 54 | } 55 | 56 | // DecodeBody unmarshalls the JSON response body if the result is non nil 57 | func DecodeBody(resp *http.Response, result any) error { 58 | if result == nil || resp == nil || resp.Body == nil || resp.StatusCode == http.StatusNoContent { 59 | return nil 60 | } 61 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil && err != io.EOF { 62 | return fmt.Errorf("error decoding response JSON: %w", err) 63 | } 64 | return nil 65 | } 66 | 67 | func DecodeError(resp *http.Response) error { 68 | if resp == nil || resp.Body == nil { 69 | return errors.New("response has empty or invalid body") 70 | } 71 | 72 | defer ios.Close(resp.Body) 73 | 74 | body, err := io.ReadAll(resp.Body) 75 | if err == nil { 76 | var cfErrs resource.CloudFoundryErrors 77 | if err = json.Unmarshal(body, &cfErrs); err == nil && len(cfErrs.Errors) > 0 { 78 | return cfErrs.Errors[0] 79 | } 80 | } 81 | return resource.CloudFoundryHTTPError{ 82 | StatusCode: resp.StatusCode, 83 | Status: resp.Status, 84 | Body: body, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/http/status.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "net/http" 4 | 5 | func IsResponseRedirect(statusCode int) bool { 6 | return IsStatusIn(statusCode, 7 | http.StatusTemporaryRedirect, 8 | http.StatusPermanentRedirect, 9 | http.StatusMovedPermanently, 10 | http.StatusFound, 11 | http.StatusSeeOther) 12 | } 13 | 14 | func IsStatusSuccess(statusCode int) bool { 15 | return statusCode >= 200 && statusCode <= 399 16 | } 17 | 18 | func IsStatusIn(statusCode int, statuses ...int) bool { 19 | for _, s := range statuses { 20 | if statusCode == s { 21 | return true 22 | } 23 | } 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /internal/http/status_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestStatus(t *testing.T) { 11 | require.True(t, IsStatusIn(http.StatusOK, http.StatusAccepted, http.StatusOK)) 12 | require.True(t, IsStatusIn(http.StatusAccepted, http.StatusAccepted, http.StatusOK)) 13 | require.False(t, IsStatusIn(http.StatusNoContent, http.StatusAccepted, http.StatusOK)) 14 | 15 | require.True(t, IsResponseRedirect(http.StatusMovedPermanently)) 16 | require.False(t, IsResponseRedirect(http.StatusNoContent)) 17 | 18 | require.True(t, IsStatusSuccess(http.StatusNoContent)) 19 | require.True(t, IsStatusSuccess(http.StatusTemporaryRedirect)) 20 | require.False(t, IsStatusSuccess(http.StatusInternalServerError)) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /internal/ios/close.go: -------------------------------------------------------------------------------- 1 | package ios 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | func Close(c io.Closer) { 9 | if c != nil { 10 | _ = c.Close() 11 | } 12 | } 13 | 14 | func CleanupTempFile(f *os.File) { 15 | if f != nil { 16 | _ = f.Close() 17 | _ = os.Remove(f.Name()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/jwt/token.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | "golang.org/x/oauth2" 12 | ) 13 | 14 | type tokenPayload struct { 15 | Expiration int64 `json:"exp"` 16 | } 17 | 18 | func AccessTokenExpiration(accessToken string) (time.Time, error) { 19 | tp := strings.Split(accessToken, ".") 20 | if len(tp) != 3 { 21 | return time.Time{}, errors.New("access token format is invalid") 22 | } 23 | 24 | // Decode the payload segment 25 | decoded, err := base64.RawURLEncoding.DecodeString(tp[1]) 26 | if err != nil { 27 | return time.Time{}, errors.New("access token base64 encoding is invalid") 28 | } 29 | 30 | var t tokenPayload 31 | if err := json.Unmarshal(decoded, &t); err != nil { 32 | return time.Time{}, fmt.Errorf("access token is invalid: %w", err) 33 | } 34 | 35 | return time.Unix(t.Expiration, 0), nil 36 | } 37 | 38 | // ToOAuth2Token converts access token and refresh token to an oauth2.Token. 39 | func ToOAuth2Token(accessToken, refreshToken string) (*oauth2.Token, error) { 40 | accessToken = strings.TrimSpace(accessToken) 41 | refreshToken = strings.TrimSpace(refreshToken) 42 | if accessToken == "" && refreshToken == "" { 43 | return nil, errors.New("expected a non-empty CF API access token or refresh token") 44 | } 45 | oAuthToken := &oauth2.Token{ 46 | RefreshToken: refreshToken, 47 | TokenType: "bearer", // Default token type 48 | } 49 | if accessToken != "" { 50 | tokens := strings.SplitN(accessToken, " ", 2) 51 | if len(tokens) > 1 { 52 | oAuthToken.TokenType = strings.ToLower(tokens[0]) 53 | } 54 | 55 | token := tokens[len(tokens)-1] 56 | 57 | exp, err := AccessTokenExpiration(token) 58 | if err != nil { 59 | return nil, fmt.Errorf("error decoding token: %w", err) 60 | } 61 | 62 | oAuthToken.AccessToken = token 63 | oAuthToken.Expiry = exp 64 | } 65 | return oAuthToken, nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/jwt/token_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | const accessToken = `bearer ignored.eyJqdGkiOiJhOGE5YTJjNDY5MzY0YzU3YmI2M2QxMWFiYzdhNjgzOSIsInN1YiI6IjJiNmMzM2ZlLTExZTItNGQwMi05OTNhLTdiNjQ5ZjhhMmI5YyIsInNjb3BlIjpbIm9wZW5pZCIsInJvdXRpbmcucm91dGVyX2dyb3Vwcy53cml0ZSIsIm5ldHdvcmsud3JpdGUiLCJzY2ltLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLmFkbWluIiwidWFhLnVzZXIiLCJyb3V0aW5nLnJvdXRlcl9ncm91cHMucmVhZCIsImNsb3VkX2NvbnRyb2xsZXIucmVhZCIsInBhc3N3b3JkLndyaXRlIiwiY2xvdWRfY29udHJvbGxlci53cml0ZSIsIm5ldHdvcmsuYWRtaW4iLCJkb3BwbGVyLmZpcmVob3NlIiwic2NpbS53cml0ZSJdLCJjbGllbnRfaWQiOiJjZiIsImNpZCI6ImNmIiwiYXpwIjoiY2YiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMmI2YzMzZmUtMTFlMi00ZDAyLTk5M2EtN2I2NDlmOGEyYjljIiwib3JpZ2luIjoidWFhIiwidXNlcl9uYW1lIjoiYWRtaW4iLCJlbWFpbCI6ImFkbWluIiwiYXV0aF90aW1lIjoxNjk4MDk2Mzc2LCJyZXZfc2lnIjoiZmNlMmY2MDAiLCJpYXQiOjE2OTgwOTY0MDgsImV4cCI6MTY5ODA5NjQ2OCwiaXNzIjoiaHR0cHM6Ly91YWEuc3lzLmgyby0yLTE5MTQ5Lmgyby52bXdhcmUuY29tL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbImRvcHBsZXIiLCJyb3V0aW5nLnJvdXRlcl9ncm91cHMiLCJvcGVuaWQiLCJjbG91ZF9jb250cm9sbGVyIiwicGFzc3dvcmQiLCJzY2ltIiwidWFhIiwibmV0d29yayIsImNmIl19.ignored` 11 | 12 | func TestToken(t *testing.T) { 13 | t.Run("Test AccessTokenExpiration", func(t *testing.T) { 14 | expireTime, err := AccessTokenExpiration(accessToken) 15 | require.NoError(t, err) 16 | expected := time.Date(2023, 10, 23, 14, 27, 48, 0, time.FixedZone("UTC-7", -7*60*60)) 17 | require.Equal(t, expected.Unix(), expireTime.Unix()) 18 | 19 | _, err = AccessTokenExpiration("") 20 | require.EqualError(t, err, "access token format is invalid") 21 | 22 | _, err = AccessTokenExpiration("not base.64encoded.token") 23 | require.EqualError(t, err, "access token base64 encoding is invalid") 24 | 25 | _, err = AccessTokenExpiration("bearer ignored.eyJqdGkiOiJhOGE5YTJjNDY5MzY0YzU3YmI2M2QxMW.ignored") 26 | require.EqualError(t, err, "access token is invalid: unexpected end of JSON input") 27 | }) 28 | 29 | t.Run("Test ToOAuth2Token", func(t *testing.T) { 30 | _, err := ToOAuth2Token("", "") 31 | require.EqualError(t, err, "expected a non-empty CF API access token or refresh token") 32 | 33 | token, err := ToOAuth2Token(accessToken, "") 34 | require.NoError(t, err) 35 | require.NotNil(t, token) 36 | require.Equal(t, "bearer", token.TokenType) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /internal/path/path.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | func Format(urlFormat string, params ...any) string { 10 | // url encode any querystring params 11 | p := make([]any, len(params)) 12 | for i, u := range params { 13 | switch v := u.(type) { 14 | case url.Values: 15 | p[i] = v.Encode() 16 | default: 17 | p[i] = u 18 | } 19 | } 20 | 21 | s := fmt.Sprintf(urlFormat, p...) 22 | return strings.TrimSuffix(s, "?") 23 | } 24 | 25 | func Join(elem ...string) string { 26 | var sb strings.Builder 27 | for _, e := range elem { 28 | if e == "" { 29 | continue 30 | } 31 | if sb.Len() > 0 { 32 | sb.WriteString("/") 33 | e = strings.TrimPrefix(e, "/") 34 | } 35 | e = strings.TrimSuffix(e, "/") 36 | sb.WriteString(e) 37 | } 38 | return sb.String() 39 | } 40 | -------------------------------------------------------------------------------- /internal/path/path_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "net/url" 6 | "testing" 7 | ) 8 | 9 | func TestPathFormat(t *testing.T) { 10 | qs := url.Values{} 11 | qs.Set("key", "val") 12 | guid := "GUID" 13 | 14 | require.Equal(t, "/v3/apps/GUID/env", 15 | Format("/v3/apps/%s/env", guid)) 16 | 17 | require.Equal(t, "/v3/apps/GUID/env?key=val", 18 | Format("/v3/apps/%s/env?%s", guid, qs)) 19 | 20 | require.Equal(t, "/v3/apps/GUID?key=val", 21 | Format("/v3/apps/%s?%s", guid, qs)) 22 | 23 | require.Equal(t, "/v3/apps/GUID/env", 24 | Format("/v3/apps/%s/env?%s", guid, url.Values{})) 25 | } 26 | 27 | func TestPathJoin(t *testing.T) { 28 | type pathTest struct { 29 | parts []string 30 | expected string 31 | } 32 | tests := []pathTest{ 33 | { 34 | parts: []string{"/v3/apps/", "GUID/env"}, 35 | expected: "/v3/apps/GUID/env", 36 | }, 37 | { 38 | parts: []string{"/v3/apps", "GUID"}, 39 | expected: "/v3/apps/GUID", 40 | }, 41 | { 42 | parts: []string{"/v3/apps/", "/GUID/env"}, 43 | expected: "/v3/apps/GUID/env", 44 | }, 45 | { 46 | parts: []string{"/v3/apps/", "/GUID?key=val"}, 47 | expected: "/v3/apps/GUID?key=val", 48 | }, 49 | { 50 | parts: []string{"https://api.example.org/v3/apps/", "/GUID/env"}, 51 | expected: "https://api.example.org/v3/apps/GUID/env", 52 | }, 53 | { 54 | parts: []string{"https://api.example.org/v3/apps/", ""}, 55 | expected: "https://api.example.org/v3/apps", 56 | }, 57 | } 58 | for _, tt := range tests { 59 | require.Equal(t, tt.expected, Join(tt.parts...)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/path/querystring_reader.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | type QuerystringReader struct { 10 | qs url.Values 11 | } 12 | 13 | func NewQuerystringReader(pageURL string) (*QuerystringReader, error) { 14 | if pageURL == "" { 15 | return nil, errors.New("cannot parse an empty pageURL") 16 | } 17 | u, err := url.Parse(pageURL) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return &QuerystringReader{ 22 | qs: u.Query(), 23 | }, nil 24 | } 25 | 26 | func (r QuerystringReader) String(key string) string { 27 | return r.qs.Get(key) 28 | } 29 | 30 | func (r QuerystringReader) Int(key string) int { 31 | i, _ := strconv.Atoi(r.qs.Get(key)) 32 | return i 33 | } 34 | -------------------------------------------------------------------------------- /internal/path/querystring_reader_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func TestQuerystringReader(t *testing.T) { 9 | u := "https://api.example.org/v3/apps?page=1&per_page=50" 10 | reader, err := NewQuerystringReader(u) 11 | require.NoError(t, err) 12 | require.Equal(t, 1, reader.Int("page")) 13 | require.Equal(t, 50, reader.Int("per_page")) 14 | 15 | u = "https://api.example.org/v3/apps" 16 | reader, err = NewQuerystringReader(u) 17 | require.NoError(t, err) 18 | require.Equal(t, 0, reader.Int("page")) 19 | 20 | u = "https://api.example.org/v3/apps?order_by=id" 21 | reader, err = NewQuerystringReader(u) 22 | require.NoError(t, err) 23 | require.Equal(t, "id", reader.String("order_by")) 24 | 25 | _, err = NewQuerystringReader("") 26 | require.Error(t, err) 27 | } 28 | -------------------------------------------------------------------------------- /resource/app_feature.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type AppFeature struct { 4 | Name string `json:"name"` 5 | Description string `json:"description"` 6 | Enabled bool `json:"enabled"` 7 | } 8 | 9 | type AppFeatureUpdate struct { 10 | Enabled bool `json:"enabled"` 11 | } 12 | 13 | type AppFeatureList struct { 14 | Pagination Pagination `json:"pagination"` 15 | Resources []*AppFeature `json:"resources"` 16 | } 17 | -------------------------------------------------------------------------------- /resource/app_usage.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type AppUsage struct { 4 | // the app that this event pertains to, if applicable 5 | App AppUsageGUIDName `json:"app"` 6 | 7 | // the process that this event pertains to, if applicable 8 | Process AppUsageGUIDType `json:"process"` 9 | 10 | // the space that this event pertains to, if applicable 11 | Space AppUsageGUIDName `json:"space"` 12 | 13 | // the organization that this event pertains to, if applicable 14 | Organization Relationship `json:"organization"` 15 | 16 | // the buildpack that this event pertains to, if applicable 17 | Buildpack AppUsageGUIDName `json:"buildpack"` 18 | 19 | // the task that this event pertains to, if applicable 20 | Task AppUsageGUIDName `json:"task"` 21 | 22 | // state of the app that this event pertains to, if applicable 23 | State AppUsageCurrentPreviousString `json:"state"` 24 | 25 | // memory in MB of the app that this event pertains to, if applicable 26 | MemoryInMbPerInstance AppUsageCurrentPreviousInt `json:"memory_in_mb_per_instance"` 27 | 28 | // instance count of the app that this event pertains to, if applicable 29 | InstanceCount AppUsageCurrentPreviousInt `json:"instance_count"` 30 | 31 | Resource `json:",inline"` 32 | } 33 | 34 | type AppUsageList struct { 35 | Pagination Pagination `json:"pagination"` 36 | Resources []*AppUsage `json:"resources"` 37 | } 38 | 39 | type AppUsageCurrentPreviousString struct { 40 | Current string `json:"current"` 41 | Previous string `json:"previous"` 42 | } 43 | 44 | type AppUsageCurrentPreviousInt struct { 45 | Current int `json:"current"` 46 | Previous int `json:"previous"` 47 | } 48 | 49 | type AppUsageGUIDName struct { 50 | GUID string `json:"guid"` 51 | Name string `json:"name"` 52 | } 53 | 54 | type AppUsageGUIDType struct { 55 | GUID string `json:"guid"` 56 | Type string `json:"type"` 57 | } 58 | -------------------------------------------------------------------------------- /resource/audit_event.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type AuditEvent struct { 8 | Type string `json:"type"` 9 | 10 | Actor AuditEventRelatedObject `json:"actor"` 11 | Target AuditEventRelatedObject `json:"target"` 12 | 13 | Data *json.RawMessage `json:"data"` 14 | Space Relationship `json:"space"` 15 | Organization Relationship `json:"organization"` 16 | 17 | Resource `json:",inline"` 18 | } 19 | 20 | type AuditEventList struct { 21 | Pagination Pagination `json:"pagination"` 22 | Resources []*AuditEvent `json:"resources"` 23 | } 24 | 25 | type AuditEventRelatedObject struct { 26 | GUID string `json:"guid"` 27 | Type string `json:"type"` 28 | Name string `json:"name"` 29 | } 30 | -------------------------------------------------------------------------------- /resource/build.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type BuildState string 4 | 5 | func (b BuildState) String() string { 6 | return string(b) 7 | } 8 | 9 | // The 3 lifecycle states 10 | const ( 11 | BuildStateStaging BuildState = "STAGING" 12 | BuildStateStaged BuildState = "STAGED" 13 | BuildStateFailed BuildState = "FAILED" 14 | ) 15 | 16 | type Build struct { 17 | State BuildState `json:"state"` 18 | Error *string `json:"error"` 19 | 20 | StagingMemoryInMB int `json:"staging_memory_in_mb"` 21 | StagingDiskInMB int `json:"staging_disk_in_mb"` 22 | StagingLogRateLimitBytesPerSecond int `json:"staging_log_rate_limit_bytes_per_second"` 23 | 24 | Lifecycle Lifecycle `json:"lifecycle"` 25 | Package Relationship `json:"package"` 26 | Droplet *Relationship `json:"droplet"` 27 | CreatedBy CreatedBy `json:"created_by"` 28 | Relationships AppRelationship `json:"relationships"` 29 | Metadata *Metadata `json:"metadata"` 30 | Resource `json:",inline"` 31 | } 32 | 33 | type BuildCreate struct { 34 | Package Relationship `json:"package"` 35 | Lifecycle *Lifecycle `json:"lifecycle,omitempty"` 36 | StagingMemoryInMB int `json:"staging_memory_in_mb,omitempty"` 37 | StagingDiskInMB int `json:"staging_disk_in_mb,omitempty"` 38 | StagingLogRateLimitBytesPerSecond int `json:"staging_log_rate_limit_bytes_per_second,omitempty"` 39 | Metadata *Metadata `json:"metadata,omitempty"` 40 | } 41 | 42 | type BuildUpdate struct { 43 | Metadata *Metadata `json:"metadata,omitempty"` 44 | Lifecycle *Lifecycle `json:"lifecycle,omitempty"` 45 | State string `json:"state,omitempty"` 46 | } 47 | 48 | type BuildList struct { 49 | Pagination Pagination `json:"pagination"` 50 | Resources []*Build `json:"resources"` 51 | } 52 | 53 | type CreatedBy struct { 54 | GUID string `json:"guid"` 55 | Name string `json:"name"` 56 | Email string `json:"email"` 57 | } 58 | 59 | func NewBuildCreate(packageGUID string) *BuildCreate { 60 | return &BuildCreate{ 61 | Package: Relationship{ 62 | GUID: packageGUID, 63 | }, 64 | } 65 | } 66 | 67 | func NewBuildUpdate() *BuildUpdate { 68 | return &BuildUpdate{} 69 | } 70 | -------------------------------------------------------------------------------- /resource/buildpack.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Buildpack struct { 4 | Name string `json:"name"` // The name of the buildpack; to be used by app buildpack field (only alphanumeric characters) 5 | State string `json:"state"` // The state of the buildpack Valid value is: AWAITING_UPLOAD 6 | Filename *string `json:"filename"` // The filename of the buildpack, if any 7 | Stack *string `json:"stack"` // The name of the stack that the buildpack will use 8 | Position int `json:"position"` // The order in which the buildpacks are checked during buildpack auto-detection 9 | Enabled bool `json:"enabled"` // Whether the buildpack can be used for staging 10 | Locked bool `json:"locked"` // Whether the buildpack is locked to prevent updating the bits 11 | 12 | Metadata *Metadata `json:"metadata"` 13 | Resource `json:",inline"` 14 | } 15 | 16 | type BuildpackCreateOrUpdate struct { 17 | Name *string `json:"name,omitempty"` // The name of the buildpack; to be used by app buildpack field (only alphanumeric characters) 18 | Position *int `json:"position,omitempty"` // The order in which the buildpacks are checked during buildpack auto-detection 19 | Enabled *bool `json:"enabled,omitempty"` // Whether the buildpack can be used for staging 20 | Locked *bool `json:"locked,omitempty"` // Whether the buildpack is locked to prevent updating the bits 21 | Stack *string `json:"stack"` // The name of the stack that the buildpack will use 22 | Metadata *Metadata `json:"metadata,omitempty"` 23 | } 24 | 25 | type BuildpackList struct { 26 | Pagination Pagination `json:"pagination"` 27 | Resources []*Buildpack `json:"resources"` 28 | } 29 | 30 | func NewBuildpackCreate(name string) *BuildpackCreateOrUpdate { 31 | return &BuildpackCreateOrUpdate{ 32 | Name: &name, 33 | } 34 | } 35 | 36 | func (bp *BuildpackCreateOrUpdate) WithName(name string) *BuildpackCreateOrUpdate { 37 | bp.Name = &name 38 | return bp 39 | } 40 | 41 | func (bp *BuildpackCreateOrUpdate) WithPosition(position int) *BuildpackCreateOrUpdate { 42 | bp.Position = &position 43 | return bp 44 | } 45 | 46 | func (bp *BuildpackCreateOrUpdate) WithStack(stack string) *BuildpackCreateOrUpdate { 47 | bp.Stack = &stack 48 | return bp 49 | } 50 | 51 | func (bp *BuildpackCreateOrUpdate) WithEnabled(enabled bool) *BuildpackCreateOrUpdate { 52 | bp.Enabled = &enabled 53 | return bp 54 | } 55 | 56 | func (bp *BuildpackCreateOrUpdate) WithLocked(locked bool) *BuildpackCreateOrUpdate { 57 | bp.Locked = &locked 58 | return bp 59 | } 60 | 61 | func NewBuildpackUpdate() *BuildpackCreateOrUpdate { 62 | return &BuildpackCreateOrUpdate{} 63 | } 64 | -------------------------------------------------------------------------------- /resource/deployment.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Deployment struct { 4 | Status DeploymentStatus `json:"status"` 5 | Strategy string `json:"strategy"` 6 | Droplet Relationship `json:"droplet"` 7 | PreviousDroplet Relationship `json:"previous_droplet"` 8 | NewProcesses []ProcessReference `json:"new_processes"` 9 | Revision DeploymentRevision `json:"revision"` 10 | Metadata *Metadata `json:"metadata"` 11 | Relationships AppRelationship `json:"relationships"` 12 | Resource `json:",inline"` 13 | } 14 | 15 | type DeploymentCreate struct { 16 | Relationships AppRelationship `json:"relationships"` 17 | Droplet *Relationship `json:"droplet,omitempty"` 18 | Revision *DeploymentRevision `json:"revision,omitempty"` 19 | Strategy string `json:"strategy,omitempty"` 20 | Metadata *Metadata `json:"metadata,omitempty"` 21 | } 22 | 23 | type DeploymentUpdate struct { 24 | Metadata *Metadata `json:"metadata"` 25 | } 26 | 27 | type DeploymentList struct { 28 | Pagination Pagination `json:"pagination"` 29 | Resources []*Deployment `json:"resources"` 30 | } 31 | 32 | type DeploymentRevision struct { 33 | GUID string `json:"guid"` 34 | Version *int `json:"version,omitempty"` 35 | } 36 | 37 | type ProcessReference struct { 38 | GUID string `json:"guid"` 39 | Type string `json:"type"` 40 | } 41 | 42 | type DeploymentStatus struct { 43 | Value string `json:"value"` 44 | Reason string `json:"reason"` 45 | Details map[string]string `json:"details"` 46 | } 47 | 48 | func NewDeploymentCreate(appGUID string) *DeploymentCreate { 49 | return &DeploymentCreate{ 50 | Relationships: AppRelationship{ 51 | App: ToOneRelationship{ 52 | Data: &Relationship{ 53 | GUID: appGUID, 54 | }, 55 | }, 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /resource/domain.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Domain struct { 4 | Name string `json:"name"` 5 | Internal bool `json:"internal"` 6 | RouterGroup *Relationship `json:"router_group"` 7 | SupportedProtocols []string `json:"supported_protocols"` 8 | Relationships DomainRelationships `json:"relationships"` 9 | Metadata *Metadata `json:"metadata"` 10 | Resource `json:",inline"` 11 | } 12 | 13 | type DomainCreate struct { 14 | Name string `json:"name"` 15 | 16 | Internal *bool `json:"internal,omitempty"` 17 | RouterGroup *Relationship `json:"router_group,omitempty"` 18 | Relationships *DomainRelationships `json:"relationships,omitempty"` 19 | Metadata *Metadata `json:"metadata,omitempty"` 20 | } 21 | 22 | type DomainUpdate struct { 23 | Metadata *Metadata `json:"metadata"` 24 | } 25 | 26 | type DomainList struct { 27 | Pagination Pagination `json:"pagination"` 28 | Resources []*Domain `json:"resources"` 29 | } 30 | 31 | type DomainRelationships struct { 32 | Organization *ToOneRelationship `json:"organization,omitempty"` 33 | SharedOrganizations *ToManyRelationships `json:"shared_organizations,omitempty"` 34 | } 35 | 36 | func NewDomainCreate(name string) *DomainCreate { 37 | return &DomainCreate{ 38 | Name: name, 39 | } 40 | } 41 | 42 | func NewDomainShare(orgGUID string) *ToManyRelationships { 43 | return &ToManyRelationships{ 44 | Data: []Relationship{ 45 | { 46 | GUID: orgGUID, 47 | }, 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /resource/droplet.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type DropletState string 4 | 5 | // The 3 lifecycle states 6 | const ( 7 | DropletStateAwaitingUpload BuildState = "AWAITING_UPLOAD" 8 | DropletStateProcessingUpload BuildState = "PROCESSING_UPLOAD" 9 | DropletStateStaged BuildState = "STAGED" 10 | DropletStateCopying BuildState = "COPYING" 11 | DropletStateFailed BuildState = "FAILED" 12 | DropletStateExpired BuildState = "EXPIRED" 13 | ) 14 | 15 | // Droplet is the result of staging an application package. 16 | // There are two types (lifecycles) of droplets: buildpack and 17 | // docker. In the case of buildpacks, the droplet contains the 18 | // bits produced by the buildpack. 19 | type Droplet struct { 20 | State DropletState `json:"state"` 21 | Error *string `json:"error"` 22 | Lifecycle Lifecycle `json:"lifecycle"` 23 | ExecutionMetadata string `json:"execution_metadata"` 24 | ProcessTypes map[string]string `json:"process_types"` 25 | Metadata *Metadata `json:"metadata"` 26 | Relationships AppRelationship `json:"relationships"` 27 | 28 | // Only specified when the droplet is using the Docker lifecycle. 29 | Image *string `json:"image"` 30 | 31 | // The following fields are specified when the droplet is using 32 | // the buildpack lifecycle. 33 | Checksum struct { 34 | Type string `json:"type"` 35 | Value string `json:"value"` 36 | } `json:"checksum"` 37 | Stack string `json:"stack"` 38 | Buildpacks []DetectedBuildpack `json:"buildpacks"` 39 | Resource `json:",inline"` 40 | } 41 | 42 | type DropletCreate struct { 43 | Relationships AppRelationship `json:"relationships"` 44 | ProcessTypes map[string]string `json:"process_types"` 45 | } 46 | 47 | type DropletList struct { 48 | Pagination Pagination `json:"pagination,omitempty"` 49 | Resources []*Droplet `json:"resources,omitempty"` 50 | } 51 | 52 | type DropletUpdate struct { 53 | Metadata *Metadata `json:"metadata,omitempty"` 54 | Image string `json:"image,omitempty"` 55 | } 56 | 57 | type DropletCurrent struct { 58 | Data Relationship `json:"data"` 59 | Links map[string]Link `json:"links"` 60 | } 61 | 62 | type DropletCopy struct { 63 | Relationships AppRelationship `json:"relationships"` 64 | } 65 | 66 | type DetectedBuildpack struct { 67 | Name string `json:"name"` // system buildpack name 68 | BuildpackName string `json:"buildpack_name"` // name reported by the buildpack 69 | DetectOutput string `json:"detect_output"` // output during detect process 70 | Version string `json:"version"` 71 | } 72 | 73 | func NewDropletCreate(appGUID string) *DropletCreate { 74 | return &DropletCreate{ 75 | Relationships: AppRelationship{ 76 | App: ToOneRelationship{ 77 | Data: &Relationship{ 78 | GUID: appGUID, 79 | }, 80 | }, 81 | }, 82 | ProcessTypes: make(map[string]string), 83 | } 84 | } 85 | 86 | func NewDropletCopy(appGUID string) *DropletCopy { 87 | return &DropletCopy{ 88 | Relationships: AppRelationship{ 89 | App: ToOneRelationship{ 90 | Data: &Relationship{ 91 | GUID: appGUID, 92 | }, 93 | }, 94 | }, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /resource/envar_group.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "time" 4 | 5 | type EnvVarGroup struct { 6 | UpdatedAt time.Time `json:"updated_at"` 7 | Name string `json:"name"` // The name of the group; can only be running or staging 8 | Var map[string]string `json:"var"` 9 | Links map[string]Link `json:"links"` 10 | } 11 | 12 | type EnvVarGroupUpdate struct { 13 | Var map[string]string `json:"var"` 14 | } 15 | -------------------------------------------------------------------------------- /resource/error.go: -------------------------------------------------------------------------------- 1 | //go:generate go run ../tools/gen_error.go 2 | 3 | package resource 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type CloudFoundryHTTPError struct { 11 | StatusCode int 12 | Status string 13 | Body []byte 14 | } 15 | 16 | func (e CloudFoundryHTTPError) Error() string { 17 | return fmt.Sprintf("cfclient: HTTP error (%d): %s", e.StatusCode, e.Status) 18 | } 19 | 20 | type CloudFoundryErrors struct { 21 | Errors []CloudFoundryError `json:"errors"` 22 | } 23 | 24 | func (e CloudFoundryErrors) Error() string { 25 | var sb strings.Builder 26 | for _, err := range e.Errors { 27 | sb.WriteString(err.Error()) 28 | } 29 | return sb.String() 30 | } 31 | 32 | type CloudFoundryError struct { 33 | Code int `json:"code"` 34 | Title string `json:"title"` 35 | Detail string `json:"detail"` 36 | } 37 | 38 | func (e CloudFoundryError) Error() string { 39 | return fmt.Sprintf("cfclient error (%s|%d): %s", e.Title, e.Code, e.Detail) 40 | } 41 | -------------------------------------------------------------------------------- /resource/error_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestIsSpaceNotFoundError(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | error 13 | want bool 14 | }{ 15 | {"std/errors error", errors.New("is not"), false}, 16 | {"unwrapped CloudFoundry error", CloudFoundryError{ 17 | Code: 40004, 18 | }, true}, 19 | {"std wrapped CloudFoundry error", fmt.Errorf("%w", CloudFoundryError{ 20 | Code: 40004, 21 | }), true}, 22 | } 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | got := IsSpaceNotFoundError(tt.error) 26 | if got != tt.want { 27 | t.Errorf("got %v, want %v", got, tt.want) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /resource/isolation_segment.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type IsolationSegment struct { 4 | Name string `json:"name"` 5 | Metadata *Metadata `json:"metadata"` 6 | Resource `json:",inline"` 7 | } 8 | 9 | type IsolationSegmentCreate struct { 10 | Name string `json:"name"` 11 | Metadata *Metadata `json:"metadata,omitempty"` 12 | } 13 | 14 | type IsolationSegmentUpdate struct { 15 | Name *string `json:"name,omitempty"` 16 | Metadata *Metadata `json:"metadata,omitempty"` 17 | } 18 | 19 | type IsolationSegmentRelationship struct { 20 | Data []Relationship `json:"data"` 21 | Links map[string]Link `json:"links"` 22 | } 23 | 24 | type IsolationSegmentList struct { 25 | Pagination Pagination `json:"pagination"` 26 | Resources []*IsolationSegment `json:"resources"` 27 | } 28 | 29 | func NewIsolationSegmentCreate(name string) *IsolationSegmentCreate { 30 | return &IsolationSegmentCreate{ 31 | Name: name, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resource/job.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type JobState string 4 | 5 | // The 3 lifecycle states 6 | const ( 7 | JobStateProcessing JobState = "PROCESSING" 8 | JobStatePolling JobState = "POLLING" 9 | JobStateComplete JobState = "COMPLETE" 10 | JobStateFailed JobState = "FAILED" 11 | ) 12 | 13 | type Job struct { 14 | Operation string `json:"operation"` // Current desired operation of the job on a model 15 | State JobState `json:"state"` // State of the job; valid values are PROCESSING, POLLING, COMPLETE, or FAILED 16 | Errors []CloudFoundryError `json:"errors"` // Array of errors that occurred while processing the job 17 | Warnings []JobWarning `json:"warnings"` // Array of warnings that occurred while processing the job 18 | Resource `json:",inline"` 19 | } 20 | 21 | type JobWarning struct { 22 | Detail string `json:"detail"` 23 | } 24 | -------------------------------------------------------------------------------- /resource/manifest.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type ManifestDiff struct { 4 | Diff []ManifestDiffItem `json:"diff"` 5 | } 6 | 7 | type ManifestDiffItem struct { 8 | Op string `json:"op"` 9 | Path string `json:"path"` 10 | Was string `json:"was,omitempty"` 11 | Value string `json:"value,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /resource/metadata.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Metadata allows you to tag API resources with information that does not directly affect its functionality. 9 | type Metadata struct { 10 | Labels map[string]*string `json:"labels"` 11 | Annotations map[string]*string `json:"annotations"` 12 | } 13 | 14 | // NewMetadata creates a new metadata instance 15 | func NewMetadata() *Metadata { 16 | return &Metadata{} 17 | } 18 | 19 | // WithAnnotation is a fluent method alias for SetAnnotation 20 | func (m *Metadata) WithAnnotation(prefix, key string, v string) *Metadata { 21 | m.SetAnnotation(prefix, key, v) 22 | return m 23 | } 24 | 25 | // WithLabel is a fluent method alias for SetLabel 26 | func (m *Metadata) WithLabel(prefix, key string, v string) *Metadata { 27 | m.SetLabel(prefix, key, v) 28 | return m 29 | } 30 | 31 | // SetAnnotation to the metadata instance 32 | // 33 | // The prefix and value are optional and may be an empty string. The key must be at least 1 character in length. 34 | func (m *Metadata) SetAnnotation(prefix, key string, v string) { 35 | if m.Annotations == nil { 36 | m.Annotations = make(map[string]*string) 37 | } 38 | if len(prefix) > 0 { 39 | m.Annotations[fmt.Sprintf("%s/%s", prefix, key)] = &v 40 | } else { 41 | m.Annotations[key] = &v 42 | } 43 | } 44 | 45 | // RemoveAnnotation removes an annotation by setting the specified key's value to nil which can then be passed to the API 46 | func (m *Metadata) RemoveAnnotation(prefix, key string) { 47 | if m.Annotations == nil { 48 | m.Annotations = make(map[string]*string) 49 | } 50 | if len(prefix) > 0 { 51 | m.Annotations[fmt.Sprintf("%s/%s", prefix, key)] = nil 52 | } else { 53 | m.Annotations[key] = nil 54 | } 55 | } 56 | 57 | // SetLabel to the metadata instance 58 | // 59 | // The prefix and value are optional and may be an empty string. The key must be at least 1 character in length. 60 | func (m *Metadata) SetLabel(prefix, key string, v string) { 61 | if m.Labels == nil { 62 | m.Labels = make(map[string]*string) 63 | } 64 | if len(prefix) > 0 { 65 | m.Labels[fmt.Sprintf("%s/%s", prefix, key)] = &v 66 | } else { 67 | m.Labels[key] = &v 68 | } 69 | } 70 | 71 | // RemoveLabel removes a label by setting the specified key's value to nil which can then be passed to the API 72 | func (m *Metadata) RemoveLabel(prefix, key string) { 73 | if m.Labels == nil { 74 | m.Labels = make(map[string]*string) 75 | } 76 | if len(prefix) > 0 { 77 | m.Labels[fmt.Sprintf("%s/%s", prefix, key)] = nil 78 | } else { 79 | m.Labels[key] = nil 80 | } 81 | } 82 | 83 | // Clear automatically calls Remove on all annotations and labels present in the metadata instance 84 | func (m *Metadata) Clear() { 85 | splitKey := func(k string) (string, string) { 86 | p := strings.Split(k, "/") 87 | if len(p) == 1 { 88 | return "", p[0] 89 | } 90 | return p[0], p[1] 91 | } 92 | for k := range m.Annotations { 93 | prefix, key := splitKey(k) 94 | m.RemoveAnnotation(prefix, key) 95 | } 96 | for k := range m.Labels { 97 | prefix, key := splitKey(k) 98 | m.RemoveLabel(prefix, key) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /resource/metadata_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/cloudfoundry/go-cfclient/v3/resource" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestMetadata(t *testing.T) { 13 | type metaTest struct { 14 | prefix string 15 | key string 16 | value string 17 | } 18 | tests := []metaTest{ 19 | { 20 | prefix: "pre", 21 | key: "key", 22 | value: "val", 23 | }, 24 | { 25 | prefix: "", 26 | key: "empty-pre-key", 27 | value: "val", 28 | }, 29 | { 30 | prefix: "cf.example.org", 31 | key: "key", 32 | value: "val", 33 | }, 34 | { 35 | prefix: "pre", 36 | key: "no-val-key", 37 | value: "", 38 | }, 39 | { 40 | prefix: "", 41 | key: "only-key", 42 | value: "", 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | k := fmt.Sprintf("%s/%s", tt.prefix, tt.key) 48 | if tt.prefix == "" { 49 | k = tt.key 50 | } 51 | 52 | // add some annotations and labels 53 | m := resource.Metadata{} 54 | m.SetAnnotation(tt.prefix, tt.key, tt.value) 55 | m.SetLabel(tt.prefix, tt.key, tt.value) 56 | require.Equal(t, tt.value, *m.Annotations[k], "key: %s", k) 57 | require.Equal(t, tt.value, *m.Labels[k], "key: %s", k) 58 | 59 | // remove them 60 | m.RemoveAnnotation(tt.prefix, tt.key) 61 | m.RemoveLabel(tt.prefix, tt.key) 62 | require.Nil(t, m.Annotations[k], "key: %s", k) 63 | require.Nil(t, m.Labels[k], "key: %s", k) 64 | 65 | // new annotations and labels 66 | m = resource.Metadata{} 67 | m.SetAnnotation(tt.prefix, tt.key, tt.value) 68 | m.SetLabel(tt.prefix, tt.key, tt.value) 69 | require.Equal(t, tt.value, *m.Annotations[k], "key: %s", k) 70 | require.Equal(t, tt.value, *m.Labels[k], "key: %s", k) 71 | 72 | // clear 73 | m.Clear() 74 | require.Nil(t, m.Annotations[k], "key: %s", k) 75 | require.Nil(t, m.Labels[k], "key: %s", k) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /resource/organization.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Organization struct { 4 | Name string `json:"name"` 5 | Suspended bool `json:"suspended"` 6 | Relationships QuotaRelationship `json:"relationships,omitempty"` 7 | Metadata *Metadata `json:"metadata,omitempty"` 8 | Resource `json:",inline"` 9 | } 10 | 11 | type OrganizationCreate struct { 12 | Name string `json:"name"` 13 | Suspended *bool `json:"suspended,omitempty"` 14 | Metadata *Metadata `json:"metadata,omitempty"` 15 | } 16 | 17 | type OrganizationUpdate struct { 18 | Name string `json:"name,omitempty"` 19 | Suspended *bool `json:"suspended,omitempty"` 20 | Metadata *Metadata `json:"metadata,omitempty"` 21 | } 22 | 23 | type OrganizationUsageSummary struct { 24 | UsageSummary UsageSummary `json:"usage_summary"` 25 | Links map[string]Link `json:"links,omitempty"` 26 | } 27 | 28 | type OrganizationList struct { 29 | Pagination Pagination `json:"pagination,omitempty"` 30 | Resources []*Organization `json:"resources,omitempty"` 31 | } 32 | 33 | type UsageSummary struct { 34 | StartedInstances int `json:"started_instances"` 35 | MemoryInMb int `json:"memory_in_mb"` 36 | Routes int `json:"routes"` 37 | ServiceInstances int `json:"service_instances"` 38 | ReservedPorts int `json:"reserved_ports"` 39 | Domains int `json:"domains"` 40 | PerAppTasks int `json:"per_app_tasks"` 41 | ServiceKeys int `json:"service_keys"` 42 | } 43 | 44 | type QuotaRelationship struct { 45 | Quota ToOneRelationship `json:"quota"` 46 | } 47 | 48 | func NewOrganizationCreate(name string) *OrganizationCreate { 49 | return &OrganizationCreate{ 50 | Name: name, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /resource/quota.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type AppsQuota struct { 4 | // The collective memory allocation permitted for all initiated processes and active tasks. 5 | TotalMemoryInMB *int `json:"total_memory_in_mb"` 6 | 7 | // The upper limit for memory allocation per individual process or task. 8 | PerProcessMemoryInMB *int `json:"per_process_memory_in_mb"` 9 | 10 | // The cumulative log rate limit permitted for all initiated processes and active tasks. 11 | LogRateLimitInBytesPerSecond *int `json:"log_rate_limit_in_bytes_per_second"` 12 | 13 | // The maximum allowable total instances of all started processes. 14 | TotalInstances *int `json:"total_instances"` 15 | 16 | // The maximum limit for the number of tasks currently running. 17 | PerAppTasks *int `json:"per_app_tasks"` 18 | } 19 | 20 | type ServicesQuota struct { 21 | // Specifies if instances of paid service plans are permitted to be created. 22 | PaidServicesAllowed bool `json:"paid_services_allowed"` 23 | 24 | // The maximum number of service instances permitted. 25 | TotalServiceInstances *int `json:"total_service_instances"` 26 | 27 | // The maximum number of service keys permitted within an organization. 28 | TotalServiceKeys *int `json:"total_service_keys"` 29 | } 30 | 31 | type RoutesQuota struct { 32 | // The maximum number of routes permitted within an organization. 33 | TotalRoutes *int `json:"total_routes"` 34 | 35 | // The maximum number of ports that can be reserved by routes within an organization. 36 | TotalReservedPorts *int `json:"total_reserved_ports"` 37 | } 38 | 39 | type DomainsQuota struct { 40 | // The maximum number of domains that can be associated with an organization. 41 | TotalDomains *int `json:"total_domains"` 42 | } 43 | -------------------------------------------------------------------------------- /resource/resource_match.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type ResourceMatches struct { 4 | Resources []ResourceMatch `json:"resources"` 5 | } 6 | 7 | type ResourceMatchChecksum struct { 8 | Value string `json:"value"` 9 | } 10 | 11 | type ResourceMatch struct { 12 | Checksum ResourceMatchChecksum `json:"checksum"` 13 | 14 | SizeInBytes int `json:"size_in_bytes"` 15 | Path string `json:"path"` 16 | Mode string `json:"mode"` 17 | } 18 | -------------------------------------------------------------------------------- /resource/revision.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Revision struct { 4 | Version int `json:"version"` 5 | Droplet Relationship `json:"droplet"` 6 | Processes RevisionProcesses `json:"processes"` 7 | Sidecars []RevisionSidecar `json:"sidecars"` 8 | Description string `json:"description"` 9 | Deployable bool `json:"deployable"` 10 | Relationships AppRelationship `json:"relationships"` 11 | Metadata *Metadata `json:"metadata"` 12 | Resource `json:",inline"` 13 | } 14 | 15 | type RevisionList struct { 16 | Pagination Pagination `json:"pagination"` 17 | Resources []*Revision `json:"resources"` 18 | } 19 | 20 | type RevisionUpdate struct { 21 | Metadata *Metadata `json:"metadata"` 22 | } 23 | 24 | type RevisionProcess struct { 25 | Command string `json:"command"` 26 | } 27 | 28 | type RevisionProcesses struct { 29 | Web *RevisionProcess `json:"web,omitempty"` 30 | Worker *RevisionProcess `json:"worker,omitempty"` 31 | } 32 | 33 | type RevisionSidecar struct { 34 | Name string `json:"name"` 35 | Command string `json:"command"` 36 | ProcessTypes []string `json:"process_types"` 37 | MemoryInMB int `json:"memory_in_mb"` 38 | } 39 | -------------------------------------------------------------------------------- /resource/root.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | // Root links to other resources, endpoints, and external services that are relevant to API clients. 4 | type Root struct { 5 | Links RootLinks `json:"links"` 6 | } 7 | 8 | type RootLinks struct { 9 | Self Link `json:"self"` 10 | 11 | CloudControllerV2 RootCloudController `json:"cloud_controller_v2"` 12 | CloudControllerV3 RootCloudController `json:"cloud_controller_v3"` 13 | 14 | NetworkPolicyV0 Link `json:"network_policy_v0"` 15 | NetworkPolicyV1 Link `json:"network_policy_v1"` 16 | Login Link `json:"login"` 17 | Uaa Link `json:"uaa"` 18 | Credhub Link `json:"credhub"` 19 | Routing Link `json:"routing"` 20 | Logging Link `json:"logging"` 21 | LogCache Link `json:"log_cache"` 22 | LogStream Link `json:"log_stream"` 23 | AppSSH RootAppSSH `json:"app_ssh"` 24 | } 25 | 26 | type RootCloudController struct { 27 | Link 28 | Meta struct { 29 | Version string `json:"version"` 30 | } `json:"meta"` 31 | } 32 | 33 | type RootAppSSH struct { 34 | Link 35 | Meta struct { 36 | HostKeyFingerprint string `json:"host_key_fingerprint"` 37 | OauthClient string `json:"oauth_client"` 38 | } `json:"meta"` 39 | } 40 | 41 | type V3Root struct { 42 | Links Links `json:"links"` 43 | } 44 | -------------------------------------------------------------------------------- /resource/service_broker.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type ServiceBroker struct { 4 | Name string `json:"name"` 5 | URL string `json:"url"` 6 | Relationships SpaceRelationship `json:"relationships"` 7 | Metadata *Metadata `json:"metadata"` 8 | Resource `json:",inline"` 9 | } 10 | 11 | type ServiceBrokerList struct { 12 | Pagination Pagination `json:"pagination"` 13 | Resources []*ServiceBroker `json:"resources"` 14 | } 15 | 16 | type ServiceBrokerCreate struct { 17 | Name string `json:"name"` 18 | URL string `json:"url"` 19 | Authentication ServiceBrokerCredentials `json:"authentication"` 20 | 21 | Relationships *SpaceRelationship `json:"relationships,omitempty"` 22 | Metadata *Metadata `json:"metadata,omitempty"` 23 | } 24 | 25 | type ServiceBrokerUpdate struct { 26 | Name *string `json:"name,omitempty"` 27 | URL *string `json:"url,omitempty"` 28 | Authentication *ServiceBrokerCredentials `json:"authentication,omitempty"` 29 | Metadata *Metadata `json:"metadata,omitempty"` 30 | } 31 | 32 | type ServiceBrokerCredentials struct { 33 | Type string `json:"type"` // basic 34 | Credentials ServiceBrokerBasicAuthCredentials `json:"credentials"` 35 | } 36 | 37 | type ServiceBrokerBasicAuthCredentials struct { 38 | Username string `json:"username"` 39 | Password string `json:"password"` 40 | } 41 | 42 | func NewServiceBrokerCreate(name, url, username, password string) *ServiceBrokerCreate { 43 | return &ServiceBrokerCreate{ 44 | Name: name, 45 | URL: url, 46 | Authentication: ServiceBrokerCredentials{ 47 | Type: "basic", 48 | Credentials: ServiceBrokerBasicAuthCredentials{ 49 | Username: username, 50 | Password: password, 51 | }, 52 | }, 53 | } 54 | } 55 | 56 | func (s *ServiceBrokerCreate) WithSpace(guid string) *ServiceBrokerCreate { 57 | s.Relationships = &SpaceRelationship{ 58 | Space: ToOneRelationship{ 59 | Data: &Relationship{ 60 | GUID: guid, 61 | }, 62 | }, 63 | } 64 | return s 65 | } 66 | 67 | func NewServiceBrokerUpdate() *ServiceBrokerUpdate { 68 | return &ServiceBrokerUpdate{} 69 | } 70 | 71 | func (s *ServiceBrokerUpdate) WithURL(url string) *ServiceBrokerUpdate { 72 | s.URL = &url 73 | return s 74 | } 75 | 76 | func (s *ServiceBrokerUpdate) WithName(name string) *ServiceBrokerUpdate { 77 | s.Name = &name 78 | return s 79 | } 80 | 81 | func (s *ServiceBrokerUpdate) WithCredentials(username, password string) *ServiceBrokerUpdate { 82 | if s.Authentication == nil { 83 | s.Authentication = &ServiceBrokerCredentials{} 84 | } 85 | s.Authentication.Type = "basic" 86 | s.Authentication.Credentials.Username = username 87 | s.Authentication.Credentials.Password = password 88 | return s 89 | } 90 | -------------------------------------------------------------------------------- /resource/service_offering.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // ServiceOffering represent the services offered by service brokers 8 | type ServiceOffering struct { 9 | Name string `json:"name"` // Name of the service offering 10 | Description string `json:"description"` // Description of the service offering 11 | Available bool `json:"available"` // Whether the service offering is available 12 | Tags []string `json:"tags"` // Descriptive tags for the service offering 13 | 14 | // A list of permissions that the user would have to give the service, if they provision it; 15 | // the only permissions currently supported are syslog_drain, route_forwarding and volume_mount 16 | Requires []string `json:"requires"` 17 | 18 | // Whether service Instances of this service offering can be shared across organizations and spaces 19 | Shareable bool `json:"shareable"` 20 | 21 | // URL that points to a documentation page for the service offering, 22 | // if provided by the service broker as part of the metadata field 23 | DocumentationURL string `json:"documentation_url"` 24 | 25 | // This object contains information obtained from the service broker catalog 26 | BrokerCatalog ServiceOfferingBrokerCatalog `json:"broker_catalog"` 27 | 28 | Relationships ServiceBrokerRelationship `json:"relationships"` 29 | Metadata *Metadata `json:"metadata"` 30 | Resource `json:",inline"` 31 | } 32 | 33 | type ServiceOfferingList struct { 34 | Pagination Pagination `json:"pagination"` 35 | Resources []*ServiceOffering `json:"resources"` 36 | } 37 | 38 | type ServiceOfferingUpdate struct { 39 | Metadata *Metadata `json:"metadata,omitempty"` 40 | } 41 | 42 | type ServiceOfferingBrokerCatalog struct { 43 | // The identifier that the service broker provided for this service offering 44 | ID string `json:"id"` 45 | 46 | // https://github.com/openservicebrokerapi/servicebroker/blob/master/profile.md#service-metadata-fields 47 | Metadata *json.RawMessage `json:"metadata"` 48 | 49 | Features ServiceOfferingFeatures `json:"features"` 50 | } 51 | 52 | type ServiceOfferingFeatures struct { 53 | // Whether the service offering supports upgrade/downgrade for service plans by default; service plans can override this field 54 | PlanUpdateable bool `json:"plan_updateable"` 55 | 56 | // Specifies whether service Instances of the service can be bound to applications 57 | Bindable bool `json:"bindable"` 58 | 59 | // Specifies whether the Fetching a service instance endpoint is supported for all service plans 60 | InstancesRetrievable bool `json:"instances_retrievable"` 61 | 62 | // Specifies whether the Fetching a service binding endpoint is supported for all service plans 63 | BindingsRetrievable bool `json:"bindings_retrievable"` 64 | 65 | // Specifies whether service instance updates relating only to context are propagated to the service broker 66 | AllowContextUpdates bool `json:"allow_context_updates"` 67 | } 68 | 69 | type ServiceBrokerRelationship struct { 70 | ServiceBroker ToOneRelationship `json:"service_broker"` 71 | } 72 | -------------------------------------------------------------------------------- /resource/service_plan_visibility.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "fmt" 4 | 5 | type ServicePlanVisibility struct { 6 | // Denotes the visibility of the plan; can be public, admin, organization, space 7 | Type string `json:"type"` 8 | 9 | // List of organizations whose members can access the plan; present if type is organization 10 | Organizations []ServicePlanVisibilityRelation `json:"organizations,omitempty"` 11 | 12 | // space whose members can access the plan; present if type is space 13 | Space *ServicePlanVisibilityRelation `json:"space,omitempty"` 14 | } 15 | 16 | type ServicePlanVisibilityRelation struct { 17 | // org or space GUID 18 | GUID string `json:"guid"` 19 | 20 | // org or space name, only used in responses 21 | Name *string `json:"name,omitempty"` 22 | } 23 | 24 | type ServicePlanVisibilityType int 25 | 26 | const ( 27 | ServicePlanVisibilityNone ServicePlanVisibilityType = iota 28 | ServicePlanVisibilityPublic 29 | ServicePlanVisibilityAdmin 30 | ServicePlanVisibilityOrganization 31 | ServicePlanVisibilitySpace 32 | ) 33 | 34 | func (s ServicePlanVisibilityType) String() string { 35 | switch s { 36 | case ServicePlanVisibilityPublic: 37 | return "public" 38 | case ServicePlanVisibilityAdmin: 39 | return "admin" 40 | case ServicePlanVisibilityOrganization: 41 | return "organization" 42 | case ServicePlanVisibilitySpace: 43 | return "space" 44 | default: 45 | return "" 46 | } 47 | } 48 | 49 | func ParseServicePlanVisibilityType(visibilityType string) (ServicePlanVisibilityType, error) { 50 | switch visibilityType { 51 | case "public": 52 | return ServicePlanVisibilityPublic, nil 53 | case "admin": 54 | return ServicePlanVisibilityAdmin, nil 55 | case "organization": 56 | return ServicePlanVisibilityOrganization, nil 57 | case "space": 58 | return ServicePlanVisibilitySpace, nil 59 | default: 60 | return ServicePlanVisibilityNone, fmt.Errorf("could not parse %s into a valid ServicePlanVisibilityType", visibilityType) 61 | } 62 | } 63 | 64 | func NewServicePlanVisibilityUpdate(visibilityType ServicePlanVisibilityType) *ServicePlanVisibility { 65 | return &ServicePlanVisibility{ 66 | Type: visibilityType.String(), 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /resource/service_route_binding.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type ServiceRouteBinding struct { 8 | LastOperation LastOperation `json:"last_operation"` 9 | 10 | // The URL for the route service 11 | RouteServiceURL string `json:"route_service_url"` 12 | 13 | // The route and service instance that the service route is bound to 14 | Relationships ServiceRouteBindingRelationships `json:"relationships"` 15 | 16 | Metadata *Metadata `json:"metadata"` 17 | Resource `json:",inline"` 18 | } 19 | 20 | type ServiceRouteBindingList struct { 21 | Pagination Pagination `json:"pagination"` 22 | Resources []*ServiceRouteBinding `json:"resources"` 23 | Included *ServiceRouteBindingIncluded `json:"included"` 24 | } 25 | 26 | type ServiceRouteBindingCreate struct { 27 | Relationships ServiceRouteBindingRelationships `json:"relationships"` 28 | 29 | Metadata *Metadata `json:"metadata,omitempty"` 30 | Parameters *json.RawMessage `json:"parameters,omitempty"` 31 | } 32 | 33 | type ServiceRouteBindingUpdate struct { 34 | Metadata *Metadata `json:"metadata"` 35 | } 36 | 37 | type ServiceRouteBindingWithIncluded struct { 38 | ServiceRouteBinding 39 | Included *ServiceRouteBindingIncluded `json:"included"` 40 | } 41 | 42 | type ServiceRouteBindingIncluded struct { 43 | Routes []*Route `json:"routes"` 44 | ServiceInstances []*ServiceInstance `json:"service_instances"` 45 | } 46 | 47 | type ServiceRouteBindingRelationships struct { 48 | // The service instance that the route is bound to 49 | ServiceInstance ToOneRelationship `json:"service_instance"` 50 | 51 | // The route that the service instance is bound to 52 | Route ToOneRelationship `json:"route"` 53 | } 54 | 55 | // ServiceRouteBindingIncludeType https://v3-apidocs.cloudfoundry.org/version/3.126.0/index.html#include 56 | type ServiceRouteBindingIncludeType int 57 | 58 | const ( 59 | ServiceRouteBindingIncludeNone ServiceRouteBindingIncludeType = iota 60 | ServiceRouteBindingIncludeRoute 61 | ServiceRouteBindingIncludeServiceInstance 62 | ) 63 | 64 | func (a ServiceRouteBindingIncludeType) String() string { 65 | switch a { 66 | case ServiceRouteBindingIncludeRoute: 67 | return IncludeRoute 68 | case ServiceRouteBindingIncludeServiceInstance: 69 | return IncludeServiceInstance 70 | default: 71 | return IncludeNone 72 | } 73 | } 74 | 75 | func NewServiceRouteBindingCreate(routeGUID, serviceInstanceGUID string) *ServiceRouteBindingCreate { 76 | return &ServiceRouteBindingCreate{ 77 | Relationships: ServiceRouteBindingRelationships{ 78 | ServiceInstance: ToOneRelationship{ 79 | Data: &Relationship{ 80 | GUID: serviceInstanceGUID, 81 | }, 82 | }, 83 | Route: ToOneRelationship{ 84 | Data: &Relationship{ 85 | GUID: routeGUID, 86 | }, 87 | }, 88 | }, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /resource/service_usage.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type ServiceUsage struct { 4 | // Current state of the service that this event pertains to, if applicable 5 | State *string `json:"state"` 6 | 7 | // space that this event pertains to, if applicable 8 | Space ServiceUsageGUIDName `json:"space"` 9 | 10 | // organization that this event pertains to, if applicable 11 | Organization NullableRelationship `json:"organization"` 12 | 13 | // service instance that this event pertains to, if applicable 14 | ServiceInstance ServiceUsageGUIDNameType `json:"service_instance"` 15 | 16 | // service plan that this event pertains to, if applicable 17 | ServicePlan ServiceUsageGUIDName `json:"service_plan"` 18 | 19 | // service offering that this event pertains to, if applicable 20 | ServiceOffering ServiceUsageGUIDName `json:"service_offering"` 21 | 22 | // service broker that this event pertains to, if applicable 23 | ServiceBroker ServiceUsageGUIDName `json:"service_broker"` 24 | 25 | Resource `json:",inline"` 26 | } 27 | 28 | type ServiceUsageList struct { 29 | Pagination Pagination `json:"pagination"` 30 | Resources []*ServiceUsage `json:"resources"` 31 | } 32 | 33 | type ServiceUsageGUIDName struct { 34 | GUID *string `json:"guid"` 35 | Name *string `json:"name"` 36 | } 37 | 38 | type ServiceUsageGUIDNameType struct { 39 | GUID *string `json:"guid"` 40 | Name *string `json:"name"` 41 | Type *string `json:"type"` 42 | } 43 | -------------------------------------------------------------------------------- /resource/sidecar.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Sidecar struct { 4 | // Human-readable name for the sidecar 5 | Name string `json:"name"` 6 | 7 | // The command used to start the sidecar 8 | Command string `json:"command"` 9 | 10 | // A list of process types the sidecar applies to 11 | ProcessTypes []string `json:"process_types"` 12 | 13 | // Reserved memory for sidecar in MB 14 | MemoryInMB int `json:"memory_in_mb"` 15 | 16 | // Specifies whether the sidecar was created by the user or via the buildpack 17 | Origin string `json:"origin"` 18 | 19 | // The app the sidecar is associated with 20 | Relationships AppRelationship `json:"relationships"` 21 | 22 | Resource `json:",inline"` 23 | } 24 | 25 | type SidecarList struct { 26 | Pagination Pagination `json:"pagination"` 27 | Resources []*Sidecar `json:"resources"` 28 | } 29 | 30 | type SidecarCreate struct { 31 | // Human-readable name for the sidecar 32 | Name string `json:"name"` 33 | 34 | // The command used to start the sidecar 35 | Command string `json:"command"` 36 | 37 | // A list of process types the sidecar applies to 38 | ProcessTypes []string `json:"process_types"` 39 | 40 | // Reserved memory for sidecar in MB 41 | MemoryInMB *int `json:"memory_in_mb,omitempty"` 42 | } 43 | 44 | type SidecarUpdate struct { 45 | // Human-readable name for the sidecar 46 | Name *string `json:"name,omitempty"` 47 | 48 | // The command used to start the sidecar 49 | Command *string `json:"command,omitempty"` 50 | 51 | // A list of process types the sidecar applies to 52 | ProcessTypes []string `json:"process_types,omitempty"` 53 | 54 | // Reserved memory for sidecar in MB 55 | MemoryInMB *int `json:"memory_in_mb,omitempty"` 56 | } 57 | 58 | func NewSidecarCreate(name, command string, processTypes []string) *SidecarCreate { 59 | return &SidecarCreate{ 60 | Name: name, 61 | Command: command, 62 | ProcessTypes: processTypes, 63 | } 64 | } 65 | 66 | func (s *SidecarCreate) WithMemoryInMB(mb int) *SidecarCreate { 67 | s.MemoryInMB = &mb 68 | return s 69 | } 70 | 71 | func NewSidecarUpdate() *SidecarUpdate { 72 | return &SidecarUpdate{} 73 | } 74 | 75 | func (s *SidecarUpdate) WithMemoryInMB(mb int) *SidecarUpdate { 76 | s.MemoryInMB = &mb 77 | return s 78 | } 79 | 80 | func (s *SidecarUpdate) WithName(name string) *SidecarUpdate { 81 | s.Name = &name 82 | return s 83 | } 84 | 85 | func (s *SidecarUpdate) WithCommand(command string) *SidecarUpdate { 86 | s.Command = &command 87 | return s 88 | } 89 | 90 | func (s *SidecarUpdate) WithProcessTypes(processTypes []string) *SidecarUpdate { 91 | s.ProcessTypes = processTypes 92 | return s 93 | } 94 | -------------------------------------------------------------------------------- /resource/space.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Space struct { 4 | Name string `json:"name"` 5 | Relationships *SpaceRelationships `json:"relationships"` 6 | Metadata *Metadata `json:"metadata"` 7 | Resource `json:",inline"` 8 | } 9 | 10 | type SpaceCreate struct { 11 | Name string `json:"name"` 12 | Relationships *SpaceRelationships `json:"relationships"` 13 | Metadata *Metadata `json:"metadata,omitempty"` 14 | } 15 | 16 | type SpaceUpdate struct { 17 | Name string `json:"name,omitempty"` 18 | Metadata *Metadata `json:"metadata,omitempty"` 19 | } 20 | 21 | type SpaceList struct { 22 | Pagination Pagination `json:"pagination"` 23 | Resources []*Space `json:"resources"` 24 | Included *SpaceIncluded `json:"included"` 25 | } 26 | 27 | type SpaceRelationships struct { 28 | Organization *ToOneRelationship `json:"organization"` 29 | Quota *ToOneRelationship `json:"quota,omitempty"` 30 | } 31 | 32 | type SpaceWithIncluded struct { 33 | Space 34 | Included *SpaceIncluded `json:"included"` 35 | } 36 | 37 | type SpaceIncluded struct { 38 | Organizations []*Organization `json:"organizations"` 39 | } 40 | 41 | const ( 42 | SpaceIncludeNone SpaceIncludeType = iota 43 | SpaceIncludeOrganization 44 | ) 45 | 46 | type SpaceIncludeType int 47 | 48 | func (s SpaceIncludeType) String() string { 49 | switch s { 50 | case SpaceIncludeOrganization: 51 | return IncludeOrganization 52 | default: 53 | return IncludeNone 54 | } 55 | } 56 | 57 | func NewSpaceCreate(name, orgGUID string) *SpaceCreate { 58 | return &SpaceCreate{ 59 | Name: name, 60 | Relationships: &SpaceRelationships{ 61 | Organization: &ToOneRelationship{ 62 | Data: &Relationship{ 63 | GUID: orgGUID, 64 | }, 65 | }, 66 | }, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /resource/space_feature.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type SpaceFeature struct { 4 | Name string `json:"name"` 5 | Enabled bool `json:"enabled"` 6 | Description string `json:"description"` 7 | } 8 | 9 | type SpaceFeatureUpdate struct { 10 | Enabled bool `json:"enabled"` 11 | } 12 | -------------------------------------------------------------------------------- /resource/stack.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | // Stack implements stack object. Stacks are the base operating system and file system that your 4 | // application will execute in. A stack is how you configure applications to run against different 5 | // operating systems (like Windows or Linux) and different versions of those operating systems. 6 | type Stack struct { 7 | Name string `json:"name"` 8 | Description *string `json:"description"` 9 | RunRootfsImage string `json:"run_rootfs_image"` 10 | BuildRootfsImage string `json:"build_rootfs_image"` 11 | Default bool `json:"default"` 12 | Metadata *Metadata `json:"metadata"` 13 | Resource `json:",inline"` 14 | } 15 | 16 | type StackCreate struct { 17 | Name string `json:"name"` 18 | Description *string `json:"description,omitempty"` 19 | Metadata *Metadata `json:"metadata,omitempty"` 20 | } 21 | 22 | type StackUpdate struct { 23 | Metadata *Metadata `json:"metadata,omitempty"` 24 | } 25 | 26 | type StackList struct { 27 | Pagination Pagination `json:"pagination"` 28 | Resources []*Stack `json:"resources"` 29 | } 30 | -------------------------------------------------------------------------------- /resource/types.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | IncludeNone = "" 9 | IncludeSpaceOrganization = "space.organization" 10 | IncludeSpace = "Space" 11 | IncludeUser = "user" 12 | IncludeOrganization = "organization" 13 | IncludeDomain = "domain" 14 | IncludeServiceOffering = "service_offering" 15 | IncludeApp = "app" 16 | IncludeServiceInstance = "service_instance" 17 | IncludeRoute = "route" 18 | ) 19 | 20 | type Resource struct { 21 | GUID string `json:"guid"` 22 | CreatedAt time.Time `json:"created_at"` 23 | UpdatedAt time.Time `json:"updated_at"` 24 | Links Links `json:"links,omitempty"` 25 | } 26 | 27 | // Pagination is used by the apis to page list results 28 | type Pagination struct { 29 | TotalResults int `json:"total_results"` 30 | TotalPages int `json:"total_pages"` 31 | First Link `json:"first"` 32 | Last Link `json:"last"` 33 | Next Link `json:"next"` 34 | Previous Link `json:"previous"` 35 | } 36 | 37 | type Links map[string]Link 38 | 39 | func (l Links) Self() Link { 40 | return l["self"] 41 | } 42 | 43 | // Link is a HATEOAS-style link for apis 44 | type Link struct { 45 | Href string `json:"href"` 46 | Method string `json:"method,omitempty"` 47 | } 48 | 49 | type SpaceRelationship struct { 50 | Space ToOneRelationship `json:"space"` 51 | } 52 | 53 | type AppRelationships struct { 54 | Space ToOneRelationship `json:"space"` 55 | CurrentDroplet ToOneRelationship `json:"current_droplet"` 56 | } 57 | 58 | type AppRelationship struct { 59 | App ToOneRelationship `json:"app"` 60 | } 61 | 62 | // ToOneRelationship is a relationship to a single object 63 | type ToOneRelationship struct { 64 | Data *Relationship `json:"data"` 65 | } 66 | 67 | // ToManyRelationships is a relationship to multiple objects 68 | type ToManyRelationships struct { 69 | Data []Relationship `json:"data"` 70 | } 71 | 72 | type Relationship struct { 73 | GUID string `json:"guid,omitempty"` 74 | } 75 | 76 | type NullableToOneRelationship struct { 77 | Data *NullableRelationship `json:"data"` 78 | } 79 | 80 | type NullableRelationship struct { 81 | GUID *string `json:"guid"` 82 | } 83 | 84 | type LastOperation struct { 85 | Type string `json:"type"` 86 | State string `json:"state"` 87 | Description string `json:"description,omitempty"` 88 | UpdatedAt time.Time `json:"updated_at"` 89 | CreatedAt time.Time `json:"created_at"` 90 | } 91 | 92 | func NewToManyRelationships(guids []string) *ToManyRelationships { 93 | r := &ToManyRelationships{ 94 | Data: make([]Relationship, len(guids)), 95 | } 96 | for i, g := range guids { 97 | r.Data[i] = Relationship{ 98 | GUID: g, 99 | } 100 | } 101 | return r 102 | } 103 | -------------------------------------------------------------------------------- /resource/user.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | // User implements the user object 4 | type User struct { 5 | Username *string `json:"username"` 6 | PresentationName string `json:"presentation_name"` 7 | Origin *string `json:"origin"` 8 | Metadata *Metadata `json:"metadata"` 9 | Resource `json:",inline"` 10 | } 11 | 12 | // UserCreate is used to create a new user in the Cloud Controller database 13 | // 14 | // Creating a user requires one value, a GUID. This creates a user in the Cloud Controller database. 15 | // Generally, the GUID should match the GUID of an already-created 16 | // user in the UAA database, though this is not required. 17 | type UserCreate struct { 18 | GUID string `json:"guid"` 19 | Metadata *Metadata `json:"metadata,omitempty"` 20 | } 21 | 22 | type UserCreateWithUsername struct { 23 | Username string `json:"username"` 24 | Origin string `json:"origin"` 25 | Metadata *Metadata `json:"metadata,omitempty"` 26 | } 27 | 28 | type UserUpdate struct { 29 | Metadata *Metadata `json:"metadata,omitempty"` 30 | } 31 | 32 | type UserList struct { 33 | Pagination Pagination `json:"pagination"` 34 | Resources []*User `json:"resources"` 35 | } 36 | 37 | func NewUserCreateWithGUID(userGUID string) *UserCreate { 38 | return &UserCreate{ 39 | GUID: userGUID, 40 | } 41 | } 42 | 43 | func NewUserCreateWithUsername(userName string, origin string) *UserCreateWithUsername { 44 | return &UserCreateWithUsername{ 45 | Username: userName, 46 | Origin: origin, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/helloworld/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudfoundry/go-cfclient/helloworld 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /test/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | port := os.Getenv("PORT") 11 | if port == "" { 12 | port = "8080" 13 | } 14 | address := fmt.Sprintf(":%s", port) 15 | fmt.Printf("serving on %s", address) 16 | 17 | http.HandleFunc("/", helloServer) 18 | err := http.ListenAndServe(address, nil) 19 | if err != nil { 20 | _, _ = fmt.Fprintf(os.Stderr, "error executing: %s", err) 21 | os.Exit(1) 22 | } 23 | } 24 | 25 | func helloServer(w http.ResponseWriter, r *http.Request) { 26 | _, _ = fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:]) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /test/helloworld/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: go-cfclient-hello-world 4 | memory: 64M 5 | buildpacks: 6 | - go_buildpack 7 | 8 | -------------------------------------------------------------------------------- /testutil/name_generator.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "math/big" 7 | ) 8 | 9 | var left = []string{ 10 | "awesome", 11 | "awkward", 12 | "brave", 13 | "boring", 14 | "cute", 15 | "cuddly", 16 | "delightful", 17 | "diligent", 18 | "elegant", 19 | "enlightened", 20 | "furry", 21 | "friendly", 22 | "giant", 23 | "gracious", 24 | "hilarious", 25 | "hungry", 26 | "intimidating", 27 | "ill", 28 | "jokingly", 29 | "janky", 30 | "killer", 31 | "kafkaesque", 32 | "lame", 33 | "luscious", 34 | "menacing", 35 | "masterful", 36 | "naked", 37 | "nutritious", 38 | "objective", 39 | "overt", 40 | "pugnacious", 41 | "perplexed", 42 | "quick", 43 | "quiet", 44 | "random", 45 | "rough", 46 | "solemn", 47 | "sarcastic", 48 | "tactical", 49 | "tactful", 50 | "unfortunate", 51 | "ubiquitous", 52 | "vulnerable", 53 | "vain", 54 | "weird", 55 | "wretched", 56 | "xenophobic", 57 | "xiphoid", 58 | "zealous", 59 | "zany", 60 | } 61 | 62 | var right = []string{ 63 | "acamar", 64 | "acubens", 65 | "baekdu", 66 | "beid", 67 | "cebalrai", 68 | "castor", 69 | "dalim", 70 | "dombay", 71 | "ebla", 72 | "electra", 73 | "fang", 74 | "fawaris", 75 | "gacrux", 76 | "gomeisa", 77 | "hadar", 78 | "hatysa", 79 | "iklil", 80 | "intercrus", 81 | "jabbah", 82 | "jishui", 83 | "kaffaljidhma", 84 | "kang", 85 | "larawag", 86 | "lerna", 87 | "maasym", 88 | "maia", 89 | "nahn", 90 | "natasha", 91 | "ogma", 92 | "okab", 93 | "peacock", 94 | "pincoya", 95 | "ran", 96 | "rasalas", 97 | "sabik", 98 | "sadr", 99 | "taiyangshou", 100 | "tapecue", 101 | "ukdah", 102 | "unukalhai", 103 | "vega", 104 | "veritate", 105 | "wasat", 106 | "wazn", 107 | "xamidimura", 108 | "xuange", 109 | "yedposterior", 110 | "yedprior", 111 | "zaniah", 112 | "zaurak", 113 | } 114 | 115 | func RandomGUID() string { 116 | b := make([]byte, 16) 117 | _, err := rand.Read(b) 118 | if err != nil { 119 | panic(err) 120 | } 121 | return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 122 | } 123 | 124 | func RandomName() string { 125 | getRandomInt := func(max int) int { 126 | n, err := rand.Int(rand.Reader, big.NewInt(int64(max))) 127 | if err != nil { 128 | panic(err) 129 | } 130 | return int(n.Int64()) 131 | } 132 | 133 | name := left[getRandomInt(len(left))] + "_" + right[getRandomInt(len(right))] 134 | return fmt.Sprintf("%s%d", name, getRandomInt(10)) 135 | } 136 | -------------------------------------------------------------------------------- /testutil/pointer.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | func BoolPtr(b bool) *bool { 4 | return &b 5 | } 6 | 7 | func IntPtr(i int) *int { 8 | return &i 9 | } 10 | 11 | func StringPtr(s string) *string { 12 | return &s 13 | } 14 | -------------------------------------------------------------------------------- /testutil/template/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2022-10-19T18:26:05Z", 4 | "updated_at": "2022-10-19T18:26:57Z", 5 | "name": "{{.Name}}", 6 | "state": "STARTED", 7 | "lifecycle": { 8 | "type": "buildpack", 9 | "data": { 10 | "buildpacks": [ 11 | "java_buildpack_offline" 12 | ], 13 | "stack": "cflinuxfs3" 14 | } 15 | }, 16 | "relationships": { 17 | "space": { 18 | "data": { 19 | "guid": "5c1b65d8-abdc-471b-962d-b60a6d8646b0" 20 | } 21 | }, 22 | "current_droplet": { 23 | "data": { 24 | "guid": "e4398f42-5ac5-46fb-961e-ad419f14cf83" 25 | } 26 | } 27 | }, 28 | "metadata": { 29 | "labels": {}, 30 | "annotations": {} 31 | }, 32 | "links": { 33 | "self": { 34 | "href": "https://api.example.org/v3/apps/{{.GUID}}" 35 | }, 36 | "environment_variables": { 37 | "href": "https://api.example.org/v3/apps/{{.GUID}}/environment_variables" 38 | }, 39 | "space": { 40 | "href": "https://api.example.org/v3/spaces/5c1b65d8-abdc-471b-962d-b60a6d8646b0" 41 | }, 42 | "processes": { 43 | "href": "https://api.example.org/v3/apps/{{.GUID}}/processes" 44 | }, 45 | "packages": { 46 | "href": "https://api.example.org/v3/apps/{{.GUID}}/packages" 47 | }, 48 | "current_droplet": { 49 | "href": "https://api.example.org/v3/apps/{{.GUID}}/droplets/current" 50 | }, 51 | "droplets": { 52 | "href": "https://api.example.org/v3/apps/{{.GUID}}/droplets" 53 | }, 54 | "tasks": { 55 | "href": "https://api.example.org/v3/apps/{{.GUID}}/tasks" 56 | }, 57 | "start": { 58 | "href": "https://api.example.org/v3/apps/{{.GUID}}/actions/start", 59 | "method": "POST" 60 | }, 61 | "stop": { 62 | "href": "https://api.example.org/v3/apps/{{.GUID}}/actions/stop", 63 | "method": "POST" 64 | }, 65 | "revisions": { 66 | "href": "https://api.example.org/v3/apps/{{.GUID}}/revisions" 67 | }, 68 | "deployed_revisions": { 69 | "href": "https://api.example.org/v3/apps/{{.GUID}}/revisions/deployed" 70 | }, 71 | "features": { 72 | "href": "https://api.example.org/v3/apps/{{.GUID}}/features" 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /testutil/template/app_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "staging_env_json": { 3 | "GEM_CACHE": "http://gem-cache.example.org", 4 | "SOME_BOOLEAN": true, 5 | "SOME_INT": 5, 6 | "SOME_FLOAT64": 10.4 7 | }, 8 | "running_env_json": { 9 | "HTTP_PROXY": "http://proxy.example.org", 10 | "SOME_BOOLEAN": true, 11 | "SOME_INT": 5, 12 | "SOME_FLOAT64": 10.4 13 | }, 14 | "environment_variables": { 15 | "RAILS_ENV": "production", 16 | "SOME_BOOLEAN": true, 17 | "SOME_INT": 5, 18 | "SOME_FLOAT64": 10.4 19 | }, 20 | "system_env_json": { 21 | "VCAP_SERVICES": { 22 | "mysql": [ 23 | { 24 | "name": "db-for-my-app", 25 | "binding_id": "0e85b634-e043-4b43-96da-f83dfe83ab33", 26 | "binding_name": "db-for-my-app", 27 | "instance_id": "07fca01c-f789-4d45-80b4-e19ba3ca862c", 28 | "instance_name": "my-mysql-service", 29 | "label": "mysql", 30 | "tags": ["relational", "sql"], 31 | "plan": "xlarge", 32 | "credentials": { 33 | "username": "user", 34 | "password": "top-secret" 35 | }, 36 | "syslog_drain_url": "https://syslog.example.org/drain", 37 | "volume_mounts": [], 38 | "provider": null 39 | } 40 | ] 41 | } 42 | }, 43 | "application_env_json": { 44 | "VCAP_APPLICATION": { 45 | "limits": { 46 | "fds": 16384 47 | }, 48 | "application_name": "{{.Name}}", 49 | "application_uris": [ "{{.Name}}.example.org" ], 50 | "name": "{{.Name}}", 51 | "space_name": "my_space", 52 | "space_id": "2f35885d-0c9d-4423-83ad-fd05066f8576", 53 | "uris": [ "my_app.example.org" ], 54 | "users": null 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /testutil/template/app_environment_expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "staging_env_json": { 3 | "GEM_CACHE": "http://gem-cache.example.org", 4 | "SOME_BOOLEAN": "true", 5 | "SOME_INT": "5", 6 | "SOME_FLOAT64": "10.4" 7 | }, 8 | "running_env_json": { 9 | "HTTP_PROXY": "http://proxy.example.org", 10 | "SOME_BOOLEAN": "true", 11 | "SOME_INT": "5", 12 | "SOME_FLOAT64": "10.4" 13 | }, 14 | "environment_variables": { 15 | "RAILS_ENV": "production", 16 | "SOME_BOOLEAN": "true", 17 | "SOME_INT": "5", 18 | "SOME_FLOAT64": "10.4" 19 | }, 20 | "system_env_json": { 21 | "VCAP_SERVICES": { 22 | "mysql": [ 23 | { 24 | "name": "db-for-my-app", 25 | "binding_id": "0e85b634-e043-4b43-96da-f83dfe83ab33", 26 | "binding_name": "db-for-my-app", 27 | "instance_id": "07fca01c-f789-4d45-80b4-e19ba3ca862c", 28 | "instance_name": "my-mysql-service", 29 | "label": "mysql", 30 | "tags": ["relational", "sql"], 31 | "plan": "xlarge", 32 | "credentials": { 33 | "username": "user", 34 | "password": "top-secret" 35 | }, 36 | "syslog_drain_url": "https://syslog.example.org/drain", 37 | "volume_mounts": [], 38 | "provider": null 39 | } 40 | ] 41 | } 42 | }, 43 | "application_env_json": { 44 | "VCAP_APPLICATION": { 45 | "limits": { 46 | "fds": 16384 47 | }, 48 | "application_name": "{{.Name}}", 49 | "application_uris": [ "{{.Name}}.example.org" ], 50 | "name": "{{.Name}}", 51 | "space_name": "my_space", 52 | "space_id": "2f35885d-0c9d-4423-83ad-fd05066f8576", 53 | "uris": [ "my_app.example.org" ], 54 | "users": null 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /testutil/template/app_envvar.json: -------------------------------------------------------------------------------- 1 | { 2 | "var": { 3 | "RAILS_ENV": "production", 4 | "SOME_BOOLEAN": true, 5 | "SOME_INT": 5, 6 | "SOME_FLOAT64": 10.4 7 | }, 8 | "links": { 9 | "self": { 10 | "href": "https://api.example.org/v3/apps/[guid]/environment_variables" 11 | }, 12 | "app": { 13 | "href": "https://api.example.org/v3/apps/[guid]" 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /testutil/template/app_feature.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssh", 3 | "description": "Enable SSHing into the app.", 4 | "enabled": true 5 | } -------------------------------------------------------------------------------- /testutil/template/app_permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "read_basic_data": true, 3 | "read_sensitive_data": false 4 | } -------------------------------------------------------------------------------- /testutil/template/app_ssh.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": false, 3 | "reason": "Disabled globally" 4 | } -------------------------------------------------------------------------------- /testutil/template/app_update_envvar.json: -------------------------------------------------------------------------------- 1 | { 2 | "var": { 3 | "RAILS_ENV": "production", 4 | "DEBUG": "false" 5 | }, 6 | "links": { 7 | "self": { 8 | "href": "https://api.example.org/v3/apps/{{.GUID}}/environment_variables" 9 | }, 10 | "app": { 11 | "href": "https://api.example.org/v3/apps/{{.GUID}}" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /testutil/template/app_usage.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2020-05-28T16:41:23Z", 4 | "updated_at": "2020-05-28T16:41:26Z", 5 | "state": { 6 | "current": "STARTED", 7 | "previous": "STOPPED" 8 | }, 9 | "app": { 10 | "guid": "guid-f93250f7-7ef5-4b02-8d33-353919ce8358", 11 | "name": "name-1982" 12 | }, 13 | "process": { 14 | "guid": "guid-e9d2d5a0-69a6-46ef-bac5-43f3ed177614", 15 | "type": "type-1983" 16 | }, 17 | "space": { 18 | "guid": "guid-5e28f12f-9d80-473e-b826-537b148eb338", 19 | "name": "name-1664" 20 | }, 21 | "organization": { 22 | "guid": "guid-036444f4-f2f5-4ea8-a353-e73330ca0f0a" 23 | }, 24 | "buildpack": { 25 | "guid": "guid-34916716-31d7-40c1-9afd-f312996c9654", 26 | "name": "label-64" 27 | }, 28 | "task": { 29 | "guid": "guid-7cc11646-bf38-4f4e-b6e0-9581916a74d9", 30 | "name": "name-2929" 31 | }, 32 | "memory_in_mb_per_instance": { 33 | "current": 512, 34 | "previous": 256 35 | }, 36 | "instance_count": { 37 | "current": 10, 38 | "previous": 5 39 | }, 40 | "links": { 41 | "self": { 42 | "href": "https://api.example.org/v3/app_usage_events/{{.GUID}}" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /testutil/template/audit_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2016-06-08T16:41:23Z", 4 | "updated_at": "2016-06-08T16:41:26Z", 5 | "type": "audit.app.update", 6 | "actor": { 7 | "guid": "d144abe3-3d7b-40d4-b63f-2584798d3ee5", 8 | "type": "user", 9 | "name": "admin" 10 | }, 11 | "target": { 12 | "guid": "2e3151ba-9a63-4345-9c5b-6d8c238f4e55", 13 | "type": "app", 14 | "name": "my-app" 15 | }, 16 | "data": { 17 | "request": { 18 | "recursive": true 19 | } 20 | }, 21 | "space": { 22 | "guid": "cb97dd25-d4f7-4185-9e6f-ad6e585c207c" 23 | }, 24 | "organization": { 25 | "guid": "d9be96f5-ea8f-4549-923f-bec882e32e3c" 26 | }, 27 | "links": { 28 | "self": { 29 | "href": "https://api.example.org/v3/audit_events/{{.GUID}}" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /testutil/template/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2016-03-28T23:39:34Z", 4 | "updated_at": "2016-06-08T16:41:26Z", 5 | "created_by": { 6 | "guid": "3cb4e243-bed4-49d5-8739-f8b45abdec1c", 7 | "name": "bill", 8 | "email": "bill@example.com" 9 | }, 10 | "state": "{{index .Params "state"}}", 11 | "staging_memory_in_mb": 1024, 12 | "staging_disk_in_mb": 1024, 13 | "staging_log_rate_limit_bytes_per_second": 1024, 14 | "error": null, 15 | "lifecycle": { 16 | "type": "buildpack", 17 | "data": { 18 | "buildpacks": [ "ruby_buildpack" ], 19 | "stack": "cflinuxfs3" 20 | } 21 | }, 22 | "package": { 23 | "guid": "8e4da443-f255-499c-8b47-b3729b5b7432" 24 | }, 25 | "droplet": null, 26 | "relationships": { 27 | "app": { 28 | "data": { 29 | "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" 30 | } 31 | } 32 | }, 33 | "metadata": { 34 | "labels": { }, 35 | "annotations": { } 36 | }, 37 | "links": { 38 | "self": { 39 | "href": "https://api.example.org/v3/builds/{{.GUID}}" 40 | }, 41 | "app": { 42 | "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /testutil/template/buildpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2016-03-18T23:26:46Z", 4 | "updated_at": "2016-10-17T20:00:42Z", 5 | "name": "{{.Name}}_buildpack", 6 | "state": "AWAITING_UPLOAD", 7 | "filename": null, 8 | "stack": "cflinuxfs3", 9 | "position": 42, 10 | "enabled": true, 11 | "locked": false, 12 | "metadata": { 13 | "labels": { }, 14 | "annotations": { } 15 | }, 16 | "links": { 17 | "self": { 18 | "href": "https://api.example.org/v3/buildpacks/{{.GUID}}" 19 | }, 20 | "upload": { 21 | "href": "https://api.example.org/v3/buildpacks/{{.GUID}}/upload", 22 | "method": "POST" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /testutil/template/deployment.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "status": { 4 | "value": "ACTIVE", 5 | "reason": "DEPLOYING", 6 | "details": { 7 | "last_successful_healthcheck": "2018-04-25T22:42:10Z" 8 | } 9 | }, 10 | "strategy": "rolling", 11 | "droplet": { 12 | "guid": "44ccfa61-dbcf-4a0d-82fe-f668e9d2a962" 13 | }, 14 | "previous_droplet": { 15 | "guid": "cc6bc315-bd06-49ce-92c2-bc3ad45268c2" 16 | }, 17 | "new_processes": [ 18 | { 19 | "guid": "fd5d3e60-f88c-4c37-b1ae-667cfc65a856", 20 | "type": "web" 21 | } 22 | ], 23 | "revision": { 24 | "guid": "56126cba-656a-4eba-a81e-7e9951b2df57", 25 | "version": 1 26 | }, 27 | "created_at": "2018-04-25T22:42:10Z", 28 | "updated_at": "2018-04-25T22:42:10Z", 29 | "metadata": { 30 | "labels": { }, 31 | "annotations": { } 32 | }, 33 | "relationships": { 34 | "app": { 35 | "data": { 36 | "guid": "305cea31-5a44-45ca-b51b-e89c7a8ef8b2" 37 | } 38 | } 39 | }, 40 | "links": { 41 | "self": { 42 | "href": "https://api.example.org/v3/deployments/{{.GUID}}" 43 | }, 44 | "app": { 45 | "href": "https://api.example.org/v3/apps/305cea31-5a44-45ca-b51b-e89c7a8ef8b2" 46 | }, 47 | "cancel": { 48 | "href": "https://api.example.org/v3/deployments/{{.GUID}}/actions/cancel", 49 | "method": "POST" 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /testutil/template/domain.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2019-03-08T01:06:19Z", 4 | "updated_at": "2019-03-08T01:06:19Z", 5 | "name": "test-domain.com", 6 | "internal": false, 7 | "router_group": null, 8 | "supported_protocols": ["http"], 9 | "metadata": { 10 | "labels": { }, 11 | "annotations": { } 12 | }, 13 | "relationships": { 14 | "organization": { 15 | "data": { "guid": "3a3f3d89-3f89-4f05-8188-751b298c79d5" } 16 | }, 17 | "shared_organizations": { 18 | "data": [ 19 | {"guid": "404f3d89-3f89-6z72-8188-751b298d88d5"}, 20 | {"guid": "416d3d89-3f89-8h67-2189-123b298d3592"} 21 | ] 22 | } 23 | }, 24 | "links": { 25 | "self": { 26 | "href": "https://api.example.org/v3/domains/{{.GUID}}" 27 | }, 28 | "organization": { 29 | "href": "https://api.example.org/v3/organizations/3a3f3d89-3f89-4f05-8188-751b298c79d5" 30 | }, 31 | "route_reservations": { 32 | "href": "https://api.example.org/v3/domains/{{.GUID}}/route_reservations" 33 | }, 34 | "shared_organizations": { 35 | "href": "https://api.example.org/v3/domains/{{.GUID}}/relationships/shared_organizations" 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /testutil/template/domain_shared.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | {"guid": "404f3d89-3f89-6z72-8188-751b298d88d5"}, 4 | {"guid": "416d3d89-3f89-8h67-2189-123b298d3592"} 5 | ] 6 | } -------------------------------------------------------------------------------- /testutil/template/droplet.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "state": "STAGED", 4 | "error": null, 5 | "lifecycle": { 6 | "type": "buildpack", 7 | "data": {} 8 | }, 9 | "execution_metadata": "", 10 | "process_types": { 11 | "rake": "bundle exec rake", 12 | "web": "bundle exec rackup config.ru -p $PORT" 13 | }, 14 | "checksum": { 15 | "type": "sha256", 16 | "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 17 | }, 18 | "buildpacks": [ 19 | { 20 | "name": "ruby_buildpack", 21 | "detect_output": "ruby 1.6.14", 22 | "version": "1.1.1.", 23 | "buildpack_name": "ruby" 24 | } 25 | ], 26 | "stack": "cflinuxfs3", 27 | "image": null, 28 | "created_at": "2016-03-28T23:39:34Z", 29 | "updated_at": "2016-03-28T23:39:47Z", 30 | "relationships": { 31 | "app": { 32 | "data": { 33 | "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" 34 | } 35 | } 36 | }, 37 | "links": { 38 | "self": { 39 | "href": "https://api.example.org/v3/droplets/{{.GUID}}" 40 | }, 41 | "package": { 42 | "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" 43 | }, 44 | "app": { 45 | "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" 46 | }, 47 | "assign_current_droplet": { 48 | "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", 49 | "method": "PATCH" 50 | }, 51 | "download": { 52 | "href": "https://api.example.org/v3/droplets/{{.GUID}}/download" 53 | } 54 | }, 55 | "metadata": { 56 | "labels": {}, 57 | "annotations": {} 58 | } 59 | } -------------------------------------------------------------------------------- /testutil/template/droplet_association.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "guid": "{{.GUID}}" 4 | }, 5 | "links": { 6 | "self": { 7 | "href": "https://api.example.org/v3/apps/bf75e72f-f1ed-4815-9e28-048595a35b6c/relationships/current_droplet" 8 | }, 9 | "related": { 10 | "href": "https://api.example.org/v3/apps/bf75e72f-f1ed-4815-9e28-048595a35b6c/droplets/current" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /testutil/template/environment_variable_group.json: -------------------------------------------------------------------------------- 1 | { 2 | "updated_at": "2016-05-04T17:00:41Z", 3 | "name": "running", 4 | "var": { 5 | "foo": "bar" 6 | }, 7 | "links": { 8 | "self": { 9 | "href": "https://api.example.org/v3/environment_variable_groups/running" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /testutil/template/feature_flag.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{.Name}}_flag", 3 | "enabled": true, 4 | "updated_at": "2016-10-17T20:00:42Z", 5 | "custom_error_message": "error message the user sees", 6 | "links": { 7 | "self": { 8 | "href": "https://api.example.org/v3/feature_flags/my_feature_flag" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /testutil/template/isolation_segment.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "name": "an_isolation_segment", 4 | "created_at": "2016-10-19T20:25:04Z", 5 | "updated_at": "2016-11-08T16:41:26Z", 6 | "links": { 7 | "self": { 8 | "href": "https://api.example.org/v3/isolation_segments/{{.GUID}}" 9 | }, 10 | "organizations": { 11 | "href": "https://api.example.org/v3/isolation_segments/{{.GUID}}/organizations" 12 | } 13 | }, 14 | "metadata": { 15 | "annotations": {}, 16 | "labels": {} 17 | } 18 | } -------------------------------------------------------------------------------- /testutil/template/isolation_segment_relationships.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "guid": "68d54d31-9b3a-463b-ba94-e8e4c32edbac" 5 | }, 6 | { 7 | "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c" 8 | } 9 | ], 10 | "links": { 11 | "self": { 12 | "href": "https://api.example.org/v3/isolation_segments/{{.GUID}}/relationships/organizations" 13 | }, 14 | "related": { 15 | "href": "https://api.example.org/v3/isolation_segments/{{.GUID}}/organizations" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /testutil/template/job.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2016-10-19T20:25:04Z", 4 | "updated_at": "2016-11-08T16:41:26Z", 5 | "operation": "app.create", 6 | "state": "{{index .Params "state"}}", 7 | "links": { 8 | "self": { 9 | "href": "https://api.example.org/v3/jobs/{{.GUID}}" 10 | } 11 | }, 12 | "errors": [], 13 | "warnings": [] 14 | } -------------------------------------------------------------------------------- /testutil/template/job_failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2016-10-19T20:25:04Z", 4 | "updated_at": "2016-11-08T16:41:26Z", 5 | "operation": "app.create", 6 | "state": "FAILED", 7 | "links": { 8 | "self": { 9 | "href": "https://api.example.org/v3/jobs/{{.GUID}}" 10 | } 11 | }, 12 | "errors": [ 13 | { 14 | "code": 10008, 15 | "title": "CF-UnprocessableEntity", 16 | "detail": "something went wrong" 17 | }, 18 | { 19 | "code": 10001, 20 | "title": "UnknownError", 21 | "detail": "unexpected error occurred" 22 | } 23 | ], 24 | "warnings": [ 25 | { 26 | "detail": "some warning" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /testutil/template/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | applications: 4 | - name: app1 5 | buildpacks: 6 | - ruby_buildpack 7 | - java_buildpack 8 | env: 9 | VAR1: value1 10 | VAR2: value2 11 | routes: 12 | - route: route.example.com 13 | - route: another-route.example.com 14 | protocol: http2 15 | services: 16 | - my-service1 17 | - my-service2 18 | - name: my-service-with-arbitrary-params 19 | binding_name: my-binding 20 | parameters: 21 | key1: value1 22 | key2: value2 23 | stack: cflinuxfs3 24 | metadata: 25 | annotations: 26 | contact: "bob@example.com jane@example.com" 27 | labels: 28 | sensitive: true 29 | processes: 30 | - type: web 31 | command: start-web.sh 32 | disk_quota: 512M 33 | health-check-http-endpoint: /healthcheck 34 | health-check-type: http 35 | health-check-invocation-timeout: 10 36 | instances: 3 37 | memory: 500M 38 | log-rate-limit-per-second: 1KB 39 | timeout: 10 40 | - type: worker 41 | command: start-worker.sh 42 | disk_quota: 1G 43 | health-check-type: process 44 | instances: 2 45 | memory: 256M 46 | log-rate-limit-per-second: 1KB 47 | timeout: 15 48 | - name: app2 49 | env: 50 | VAR1: value1 51 | processes: 52 | - type: web 53 | instances: 1 54 | memory: 256M 55 | log-rate-limit-per-second: 1KB 56 | sidecars: 57 | - name: authenticator 58 | process_types: [ 'web', 'worker' ] 59 | command: bundle exec run-authenticator 60 | memory: 800M 61 | 62 | - name: upcaser 63 | process_types: [ 'worker' ] 64 | command: ./tr-server 65 | memory: 2G -------------------------------------------------------------------------------- /testutil/template/manifest_diff.json: -------------------------------------------------------------------------------- 1 | { 2 | "diff": [ 3 | { 4 | "op": "remove", 5 | "path": "/applications/0/routes/1", 6 | "was": {"route": "route.example.com"} 7 | }, 8 | { 9 | "op": "add", 10 | "path": "/applications/1/buildpacks/2", 11 | "value": "java_buildpack" 12 | }, 13 | { 14 | "op": "replace", 15 | "path": "/applications/2/processes/1/memory", 16 | "was": "256M", 17 | "value": "512M" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /testutil/template/org.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2017-02-01T01:33:58Z", 4 | "updated_at": "2017-02-01T01:33:58Z", 5 | "name": "{{.Name}}", 6 | "suspended": false, 7 | "relationships": { 8 | "quota": { 9 | "data": { 10 | "guid": "b7887f5c-34bb-40c5-9778-577572e4fb2d" 11 | } 12 | } 13 | }, 14 | "links": { 15 | "self": { 16 | "href": "https://api.example.org/v3/organizations/{{.GUID}}" 17 | }, 18 | "domains": { 19 | "href": "https://api.example.org/v3/organizations/{{.GUID}}/domains" 20 | }, 21 | "default_domain": { 22 | "href": "https://api.example.org/v3/organizations/{{.GUID}}/domains/default" 23 | }, 24 | "quota": { 25 | "href": "https://api.example.org/v3/organization_quotas/b7887f5c-34bb-40c5-9778-577572e4fb2d" 26 | } 27 | }, 28 | "metadata": { 29 | "labels": {}, 30 | "annotations": {} 31 | } 32 | } -------------------------------------------------------------------------------- /testutil/template/org_quota.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2016-05-04T17:00:41Z", 4 | "updated_at": "2016-05-04T17:00:41Z", 5 | "name": "don-quixote", 6 | "apps": { 7 | "total_memory_in_mb": 5120, 8 | "per_process_memory_in_mb": 1024, 9 | "log_rate_limit_in_bytes_per_second": 1024, 10 | "total_instances": 10, 11 | "per_app_tasks": 5 12 | }, 13 | "services": { 14 | "paid_services_allowed": true, 15 | "total_service_instances": 10, 16 | "total_service_keys": 20 17 | }, 18 | "routes": { 19 | "total_routes": 8, 20 | "total_reserved_ports": 4 21 | }, 22 | "domains": { 23 | "total_domains": 7 24 | }, 25 | "relationships": { 26 | "organizations": { 27 | "data": [ 28 | { "guid": "9b370018-c38e-44c9-86d6-155c76801104" } 29 | ] 30 | } 31 | }, 32 | "links": { 33 | "self": { "href": "https://api.example.org/v3/organization_quotas/{{.GUID}}" } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /testutil/template/org_usage_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "usage_summary": { 3 | "started_instances": 3, 4 | "memory_in_mb": 50, 5 | "routes": 4, 6 | "service_instances": 2, 7 | "reserved_ports": 1, 8 | "domains": 4, 9 | "per_app_tasks": 2, 10 | "service_keys": 1 11 | }, 12 | "links": { 13 | "self": { 14 | "href": "https://api.example.org/v3/organizations/d4c91047-7b29-4fda-b7f9-04033e5c9c9f/usage_summary" 15 | }, 16 | "organization": { 17 | "href": "https://api.example.org/v3/organizations/d4c91047-7b29-4fda-b7f9-04033e5c9c9f" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testutil/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "type": "bits", 4 | "data": { 5 | "checksum": { 6 | "type": "sha256", 7 | "value": null 8 | }, 9 | "error": null 10 | }, 11 | "state": "{{index .Params "state"}}", 12 | "created_at": "2015-11-13T17:02:56Z", 13 | "updated_at": "2016-06-08T16:41:26Z", 14 | "relationships": { 15 | "app": { 16 | "data": { 17 | "guid": "1d3bf0ec-5806-43c4-b64e-8364dba1086a" 18 | } 19 | } 20 | }, 21 | "links": { 22 | "self": { 23 | "href": "https://api.example.org/v3/packages/{{.GUID}}" 24 | }, 25 | "upload": { 26 | "href": "https://api.example.org/v3/packages/{{.GUID}}/upload", 27 | "method": "POST" 28 | }, 29 | "download": { 30 | "href": "https://api.example.org/v3/packages/{{.GUID}}/download", 31 | "method": "GET" 32 | }, 33 | "app": { 34 | "href": "https://api.example.org/v3/apps/1d3bf0ec-5806-43c4-b64e-8364dba1086a" 35 | } 36 | }, 37 | "metadata": { 38 | "labels": { }, 39 | "annotations": { } 40 | } 41 | } -------------------------------------------------------------------------------- /testutil/template/package_docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "type": "docker", 4 | "data": { 5 | "image": "registry/image:latest", 6 | "username": "username", 7 | "password": "secret" 8 | }, 9 | "state": "PROCESSING_UPLOAD", 10 | "created_at": "2015-11-13T17:02:56Z", 11 | "updated_at": "2016-06-08T16:41:26Z", 12 | "relationships": { 13 | "app": { 14 | "data": { 15 | "guid": "1d3bf0ec-5806-43c4-b64e-8364dba1086a" 16 | } 17 | } 18 | }, 19 | "links": { 20 | "self": { 21 | "href": "https://api.example.org/v3/packages/{{.GUID}}" 22 | }, 23 | "upload": { 24 | "href": "https://api.example.org/v3/packages/{{.GUID}}/upload", 25 | "method": "POST" 26 | }, 27 | "download": { 28 | "href": "https://api.example.org/v3/packages/{{.GUID}}/download", 29 | "method": "GET" 30 | }, 31 | "app": { 32 | "href": "https://api.example.org/v3/apps/1d3bf0ec-5806-43c4-b64e-8364dba1086a" 33 | } 34 | }, 35 | "metadata": { 36 | "labels": { }, 37 | "annotations": { } 38 | } 39 | } -------------------------------------------------------------------------------- /testutil/template/process.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "type": "web", 4 | "command": "rackup", 5 | "instances": 5, 6 | "memory_in_mb": 256, 7 | "disk_in_mb": 1024, 8 | "log_rate_limit_in_bytes_per_second": 1024, 9 | "health_check": { 10 | "type": "http", 11 | "data": { 12 | "timeout": 60, 13 | "invocation_timeout": 5, 14 | "interval": 10, 15 | "endpoint": "/health" 16 | } 17 | }, 18 | "readiness_health_check": { 19 | "type": "http", 20 | "data": { 21 | "invocation_timeout": 15, 22 | "interval": 30, 23 | "endpoint": "/ready" 24 | } 25 | }, 26 | "relationships": { 27 | "app": { 28 | "data": { 29 | "guid": "ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" 30 | } 31 | }, 32 | "revision": { 33 | "data": { 34 | "guid": "885735b5-aea4-4cf5-8e44-961af0e41920" 35 | } 36 | } 37 | }, 38 | "metadata": { 39 | "labels": { }, 40 | "annotations": { } 41 | }, 42 | "created_at": "2016-03-23T18:48:22Z", 43 | "updated_at": "2016-03-23T18:48:42Z", 44 | "links": { 45 | "self": { 46 | "href": "https://api.example.org/v3/processes/{{.GUID}}" 47 | }, 48 | "scale": { 49 | "href": "https://api.example.org/v3/processes/{{.GUID}}/actions/scale", 50 | "method": "POST" 51 | }, 52 | "app": { 53 | "href": "https://api.example.org/v3/apps/ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" 54 | }, 55 | "space": { 56 | "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" 57 | }, 58 | "stats": { 59 | "href": "https://api.example.org/v3/processes/{{.GUID}}/stats" 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /testutil/template/process_stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": [ 3 | { 4 | "type": "web", 5 | "index": 0, 6 | "state": "RUNNING", 7 | "usage": { 8 | "time": "2016-03-23T23:17:30.476314154Z", 9 | "cpu": 0.00038711029163348665, 10 | "cpu_entitlement": 0.01117396940977856, 11 | "mem": 19177472, 12 | "disk": 69705728, 13 | "log_rate": 0 14 | }, 15 | "host": "10.244.16.10", 16 | "instance_ports": [ 17 | { 18 | "external": 64546, 19 | "internal": 8080, 20 | "external_tls_proxy_port": 61002, 21 | "internal_tls_proxy_port": 61003 22 | } 23 | ], 24 | "uptime": 9042, 25 | "mem_quota": 268435456, 26 | "disk_quota": 1073741824, 27 | "fds_quota": 16384, 28 | "isolation_segment": "example_iso_segment", 29 | "details": null 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /testutil/template/resource_match.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": [ 3 | { 4 | "checksum": { "value": "a9993e364706816aba3e25717850c26c9cd0d89d" }, 5 | "size_in_bytes": 1, 6 | "path": "path/to/file", 7 | "mode": "644" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /testutil/template/revision.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "version": 1, 4 | "droplet": { 5 | "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16" 6 | }, 7 | "processes": { 8 | "web": { 9 | "command": "bundle exec rackup" 10 | } 11 | }, 12 | "sidecars": [ 13 | { 14 | "name": "auth-sidecar", 15 | "command": "bundle exec sidecar", 16 | "process_types": ["web"], 17 | "memory_in_mb": 300 18 | } 19 | ], 20 | "description": "Initial revision.", 21 | "deployable": true, 22 | "relationships": { 23 | "app": { 24 | "data": { 25 | "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446" 26 | } 27 | } 28 | }, 29 | "created_at": "2017-02-01T01:33:58Z", 30 | "updated_at": "2017-02-01T01:33:58Z", 31 | "metadata": { 32 | "labels": { }, 33 | "annotations": { } 34 | }, 35 | "links": { 36 | "self": { 37 | "href": "https://api.example.org/v3/revisions/{{.GUID}}" 38 | }, 39 | "app": { 40 | "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446" 41 | }, 42 | "environment_variables": { 43 | "href": "https://api.example.org/v3/revisions/{{.GUID}}/environment_variables" 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /testutil/template/role.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2019-10-10T17:19:12Z", 4 | "updated_at": "2019-10-10T17:19:12Z", 5 | "type": "organization_auditor", 6 | "relationships": { 7 | "user": { 8 | "data": { 9 | "guid": "59eadb5f-fc13-414f-84ba-77a35e239cc8" 10 | } 11 | }, 12 | "organization": { 13 | "data": { 14 | "guid": "05c5da3b-6cbc-421c-87c3-20bb3c41ab7c" 15 | } 16 | }, 17 | "space": { 18 | "data": null 19 | } 20 | }, 21 | "links": { 22 | "self": { 23 | "href": "https://api.example.org/v3/roles/{{.GUID}}" 24 | }, 25 | "user": { 26 | "href": "https://api.example.org/v3/users/59eadb5f-fc13-414f-84ba-77a35e239cc8" 27 | }, 28 | "organization": { 29 | "href": "https://api.example.org/v3/organizations/05c5da3b-6cbc-421c-87c3-20bb3c41ab7c" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /testutil/template/route.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "protocol": "tcp", 4 | "port": 6666, 5 | "created_at": "2019-05-10T17:17:48Z", 6 | "updated_at": "2019-05-10T17:17:48Z", 7 | "host": "a-hostname", 8 | "path": "/some_path", 9 | "url": "a-hostname.a-domain.com/some_path", 10 | "destinations": [ 11 | { 12 | "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", 13 | "app": { 14 | "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", 15 | "process": { 16 | "type": "web" 17 | } 18 | }, 19 | "weight": null, 20 | "port": 8080, 21 | "protocol": "tcp" 22 | }, 23 | { 24 | "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", 25 | "app": { 26 | "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", 27 | "process": { 28 | "type": "web" 29 | } 30 | }, 31 | "weight": null, 32 | "port": 8080, 33 | "protocol": "tcp" 34 | } 35 | ], 36 | "metadata": { 37 | "labels": { }, 38 | "annotations": { } 39 | }, 40 | "relationships": { 41 | "space": { 42 | "data": { 43 | "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" 44 | } 45 | }, 46 | "domain": { 47 | "data": { 48 | "guid": "0b5f3633-194c-42d2-9408-972366617e0e" 49 | } 50 | } 51 | }, 52 | "links": { 53 | "self": { 54 | "href": "https://api.example.org/v3/routes/{{.GUID}}" 55 | }, 56 | "space": { 57 | "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" 58 | }, 59 | "domain": { 60 | "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" 61 | }, 62 | "destinations": { 63 | "href": "https://api.example.org/v3/routes/{{.GUID}}/destinations" 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /testutil/template/route_destination_with_links.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "89323d4e-2e84-43e7-83e9-adbf50a20c0e", 3 | "app": { 4 | "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446", 5 | "process": { 6 | "type": "web" 7 | } 8 | }, 9 | "weight": 61, 10 | "port": 8080, 11 | "protocol": "http2", 12 | "links": { 13 | "self": { 14 | "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" 15 | }, 16 | "route": { 17 | "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /testutil/template/route_destinations.json: -------------------------------------------------------------------------------- 1 | { 2 | "destinations": [ 3 | { 4 | "guid": "89323d4e-2e84-43e7-83e9-adbf50a20c0e", 5 | "app": { 6 | "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446", 7 | "process": { 8 | "type": "web" 9 | } 10 | }, 11 | "weight": null, 12 | "port": 8080, 13 | "protocol": "http2" 14 | }, 15 | { 16 | "guid": "fbef10a2-8ee7-11e9-aa2d-abeeaf7b83c5", 17 | "app": { 18 | "guid": "01856e12-8ee8-11e9-98a5-bb397dbc818f", 19 | "process": { 20 | "type": "api" 21 | } 22 | }, 23 | "weight": null, 24 | "port": 9000, 25 | "protocol": "http1" 26 | } 27 | ], 28 | "links": { 29 | "self": { 30 | "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" 31 | }, 32 | "route": { 33 | "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /testutil/template/route_space_relationships.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "guid": "68d54d31-9b3a-463b-ba94-e8e4c32edbac" 5 | }, 6 | { 7 | "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c" 8 | } 9 | ], 10 | "links": { 11 | "self": { 12 | "href": "https://api.example.org/v3/service_instances/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/relationships/shared_spaces" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /testutil/template/security_group.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2020-02-20T17:42:08Z", 4 | "updated_at": "2020-02-20T17:42:08Z", 5 | "name": "{{.Name}}", 6 | "globally_enabled": { 7 | "running": true, 8 | "staging": false 9 | }, 10 | "rules": [ 11 | { 12 | "protocol": "tcp", 13 | "destination": "10.10.10.0/24", 14 | "ports": "443,80,8080" 15 | }, 16 | { 17 | "protocol": "icmp", 18 | "destination": "10.10.10.0/24", 19 | "type": 8, 20 | "code": 0, 21 | "description": "Allow ping requests to private services" 22 | } 23 | ], 24 | "relationships": { 25 | "staging_spaces": { 26 | "data": [ 27 | { "guid": "space-guid-1" }, 28 | { "guid": "space-guid-2" } 29 | ] 30 | }, 31 | "running_spaces": { 32 | "data": [] 33 | } 34 | }, 35 | "links": { 36 | "self": { 37 | "href": "https://api.example.org/v3/security_groups/{{.GUID}}" 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /testutil/template/service_broker.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "name": "{{.Name}}", 4 | "url": "https://example.service-broker.com", 5 | "created_at": "2015-11-13T17:02:56Z", 6 | "updated_at": "2016-06-08T16:41:26Z", 7 | "relationships": { 8 | "space": { 9 | "data": { 10 | "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" 11 | } 12 | } 13 | }, 14 | "metadata": { 15 | "labels": { 16 | "type": "dev" 17 | }, 18 | "annotations": {} 19 | }, 20 | "links": { 21 | "self": { 22 | "href": "https://api.example.org/v3/service_brokers/{{.GUID}}" 23 | }, 24 | "service_offerings": { 25 | "href": "https://api.example.org/v3/service_offerings?service_broker_guids={{.GUID}}" 26 | }, 27 | "space": { 28 | "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /testutil/template/service_credential_binding.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2015-11-13T17:02:56Z", 4 | "updated_at": "2016-06-08T16:41:26Z", 5 | "name": "{{.Name}}", 6 | "type": "app", 7 | "last_operation": { 8 | "type": "create", 9 | "state": "succeeded", 10 | "created_at": "2015-11-13T17:02:56Z", 11 | "updated_at": "2016-06-08T16:41:26Z" 12 | }, 13 | "metadata": { 14 | "annotations": { 15 | "foo": "bar" 16 | }, 17 | "labels": { 18 | "baz": "qux" 19 | } 20 | }, 21 | "relationships": { 22 | "app": { 23 | "data": { 24 | "guid": "74f7c078-0934-470f-9883-4fddss5b8f13" 25 | } 26 | }, 27 | "service_instance": { 28 | "data": { 29 | "guid": "8bfe4c1b-9e18-45b1-83be-124163f31f9e" 30 | } 31 | } 32 | }, 33 | "links": { 34 | "self": { 35 | "href": "https://api.example.org/v3/service_credential_bindings/{{.GUID}}" 36 | }, 37 | "details": { 38 | "href": "https://api.example.org/v3/service_credential_bindings/{{.GUID}}/details" 39 | }, 40 | "parameters": { 41 | "href": "https://api.example.org/v3/service_credential_bindings/{{.GUID}}/parameters" 42 | }, 43 | "service_instance": { 44 | "href": "https://api.example.org/v3/service_instances/8bfe4c1b-9e18-45b1-83be-124163f31f9e" 45 | }, 46 | "app": { 47 | "href": "https://api.example.org/v3/apps/74f7c078-0934-470f-9883-4fddss5b8f13" 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /testutil/template/service_credential_binding_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "credentials": { 3 | "hostname": "9afad6ce-7dbd-482e-991e-13dec5208e16.mysql.service.internal", 4 | "jdbcUrl": "jdbc:mysql://9afad6ce-7dbd-482e-991e-13dec5208e16.mysql.service.internal:3306/service_instance_db?user=bd4650060752443abe4f9dbabf456184&password=9wkbbxk7auviqsqm&useSSL=false", 5 | "name": "service_instance_db", 6 | "password": "supers3cret", 7 | "port": 3306, 8 | "uri": "mysql://user:supers3cret@9afad6ce-7dbd-482e-991e-13dec5208e16.mysql.service.internal:3306/service_instance_db?reconnect=true", 9 | "username": "user" 10 | }, 11 | "syslog_drain_url": "http://syslog.example.com/drain", 12 | "volume_mounts": ["/vcap/data", "store"] 13 | } -------------------------------------------------------------------------------- /testutil/template/service_instance.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2020-03-10T15:49:29Z", 4 | "updated_at": "2020-03-10T15:49:29Z", 5 | "name": "{{.Name}}", 6 | "tags": [], 7 | "type": "managed", 8 | "maintenance_info": { 9 | "version": "1.0.0" 10 | }, 11 | "upgrade_available": false, 12 | "dashboard_url": "https://service-broker.example.org/dashboard", 13 | "last_operation": { 14 | "type": "create", 15 | "state": "succeeded", 16 | "description": "Operation succeeded", 17 | "updated_at": "2020-03-10T15:49:32Z", 18 | "created_at": "2020-03-10T15:49:29Z" 19 | }, 20 | "relationships": { 21 | "service_plan": { 22 | "data": { 23 | "guid": "5358d122-638e-11ea-afca-bf6e756684ac" 24 | } 25 | }, 26 | "space": { 27 | "data": { 28 | "guid": "5a84d315-9513-4d74-95e5-f6a5501eeef7" 29 | } 30 | } 31 | }, 32 | "metadata": { 33 | "labels": {}, 34 | "annotations": {} 35 | }, 36 | "links": { 37 | "self": { 38 | "href": "https://api.example.org/v3/service_instances/{{.GUID}}" 39 | }, 40 | "service_plan": { 41 | "href": "https://api.example.org/v3/service_plans/5358d122-638e-11ea-afca-bf6e756684ac" 42 | }, 43 | "space": { 44 | "href": "https://api.example.org/v3/spaces/5a84d315-9513-4d74-95e5-f6a5501eeef7" 45 | }, 46 | "parameters": { 47 | "href": "https://api.example.org/v3/service_instances/{{.GUID}}/parameters" 48 | }, 49 | "shared_spaces": { 50 | "href": "https://api.example.org/v3/service_instances/{{.GUID}}/relationships/shared_spaces" 51 | }, 52 | "service_credential_bindings": { 53 | "href": "https://api.example.org/v3/service_credential_bindings?service_instance_guids={{.GUID}}" 54 | }, 55 | "service_route_bindings": { 56 | "href": "https://api.example.org/v3/service_route_bindings?service_instance_guids={{.GUID}}" 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /testutil/template/service_instance_space_relationships.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "guid": "68d54d31-9b3a-463b-ba94-e8e4c32edbac" 5 | }, 6 | { 7 | "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c" 8 | } 9 | ], 10 | "links": { 11 | "self": { 12 | "href": "https://api.example.org/v3/service_instances/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/relationships/shared_spaces" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /testutil/template/service_instance_usage_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "usage_summary": [ 3 | { 4 | "space": { 5 | "guid": "68d54d31-9b3a-463b-ba94-e8e4c32edbac" 6 | }, 7 | "bound_app_count": 2 8 | }, 9 | { 10 | "space": { 11 | "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c" 12 | }, 13 | "bound_app_count": 0 14 | } 15 | ], 16 | "links": { 17 | "self": { 18 | "href": "https://api.example.org/v3/service_instances/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/relationships/shared_spaces/usage_summary" 19 | }, 20 | "shared_spaces": { 21 | "href": "https://api.example.org/v3/service_instances/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/relationships/shared_spaces" 22 | }, 23 | "service_instance": { 24 | "href": "https://api.example.org/v3/service_instances/bdeg4371-cbd3-4155-b156-dc0c2a431b4c" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /testutil/template/service_instance_user_provided.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2020-03-10T15:56:08Z", 4 | "updated_at": "2020-03-10T15:56:08Z", 5 | "last_operation": { 6 | "type": "create", 7 | "state": "succeeded", 8 | "description": "Operation succeeded", 9 | "updated_at": "2020-03-10T15:49:32Z", 10 | "created_at": "2020-03-10T15:49:29Z" 11 | }, 12 | "name": "{{.Name}}", 13 | "tags": ["sql"], 14 | "type": "user-provided", 15 | "syslog_drain_url": "http://logs.example.com", 16 | "route_service_url": "https://routes.example.com", 17 | "relationships": { 18 | "space": { 19 | "data": { 20 | "guid": "5a84d315-9513-4d74-95e5-f6a5501eeef7" 21 | } 22 | } 23 | }, 24 | "metadata": { 25 | "labels": {}, 26 | "annotations": {} 27 | }, 28 | "links": { 29 | "self": { 30 | "href": "https://api.example.org/v3/service_instances/88ce23e5-27c3-4381-a2df-32a28ec43133" 31 | }, 32 | "space": { 33 | "href": "https://api.example.org/v3/spaces/5a84d315-9513-4d74-95e5-f6a5501eeef7" 34 | }, 35 | "credentials": { 36 | "href": "https://api.example.org/v3/service_instances/88ce23e5-27c3-4381-a2df-32a28ec43133/credentials" 37 | }, 38 | "service_credential_bindings": { 39 | "href": "https://api.example.org/v3/service_credential_bindings?service_instance_guids=88ce23e5-27c3-4381-a2df-32a28ec43133" 40 | }, 41 | "service_route_bindings": { 42 | "href": "https://api.example.org/v3/service_route_bindings?service_instance_guids=88ce23e5-27c3-4381-a2df-32a28ec43133" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /testutil/template/service_offering.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "name": "{{.Name}}_service_offering", 4 | "description": "Provides my service", 5 | "available": true, 6 | "tags": ["relational", "caching"], 7 | "requires": [], 8 | "created_at": "2019-11-28T13:44:02Z", 9 | "updated_at": "2019-11-28T13:44:02Z", 10 | "shareable": true, 11 | "documentation_url": "https://some-documentation-link.io", 12 | "broker_catalog": { 13 | "id": "db730a8c-11e5-11ea-838a-0f4fff3b1cfb", 14 | "metadata": { 15 | "shareable": true 16 | }, 17 | "features": { 18 | "plan_updateable": true, 19 | "bindable": true, 20 | "instances_retrievable": true, 21 | "bindings_retrievable": true, 22 | "allow_context_updates": false 23 | } 24 | }, 25 | "relationships": { 26 | "service_broker": { 27 | "data": { 28 | "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" 29 | } 30 | } 31 | }, 32 | "metadata": { 33 | "labels": {}, 34 | "annotations": {} 35 | }, 36 | "links": { 37 | "self": { 38 | "href": "https://api.example.org/v3/service_offerings/bf7eb420-11e5-11ea-b7db-4b5d5e7976a" 39 | }, 40 | "service_plans": { 41 | "href": "https://api.example.org/v3/service_plans?service_offering_guids=bf7eb420-11e5-11ea-b7db-4b5d5e7976a" 42 | }, 43 | "service_broker": { 44 | "href": "https://api.example.org/v3/service_brokers/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /testutil/template/service_plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "name": "{{.Name}}_service_plan", 4 | "description": "Big", 5 | "visibility_type": "public", 6 | "available": true, 7 | "free": false, 8 | "costs": [ 9 | { 10 | "currency": "USD", 11 | "amount": 199.99, 12 | "unit": "Monthly" 13 | } 14 | ], 15 | "created_at": "2019-11-28T13:44:02Z", 16 | "updated_at": "2019-11-28T13:44:02Z", 17 | "maintenance_info": { 18 | "version": "1.0.0+dev4", 19 | "description": "Database version 7.8.0" 20 | }, 21 | "broker_catalog": { 22 | "id": "db730a8c-11e5-11ea-838a-0f4fff3b1cfb", 23 | "metadata": { 24 | "custom-key": "custom-information" 25 | }, 26 | "maximum_polling_duration": null, 27 | "features": { 28 | "plan_updateable": true, 29 | "bindable": true 30 | } 31 | }, 32 | "schemas": { 33 | "service_instance": { 34 | "create": { 35 | "parameters": { 36 | "$schema": "http://json-schema.org/draft-04/schema#", 37 | "type": "object", 38 | "properties": { 39 | "billing-account": { 40 | "description": "Billing account number used to charge use of shared fake server.", 41 | "type": "string" 42 | } 43 | } 44 | } 45 | }, 46 | "update": { 47 | "parameters": {} 48 | } 49 | }, 50 | "service_binding": { 51 | "create": { 52 | "parameters": {} 53 | } 54 | } 55 | }, 56 | "relationships": { 57 | "service_offering": { 58 | "data": { 59 | "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" 60 | } 61 | } 62 | }, 63 | "metadata": { 64 | "labels": {}, 65 | "annotations": {} 66 | }, 67 | "links": { 68 | "self": { 69 | "href": "https://api.example.org/v3/service_plans/{{.GUID}}" 70 | }, 71 | "service_offering": { 72 | "href": "https://api.example.org/v3/service_offerings/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" 73 | }, 74 | "visibility": { 75 | "href": "https://api.example.org/v3/service_plans/{{.GUID}}/visibility" 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /testutil/template/service_plan_visibility.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "organization", 3 | "organizations": [ 4 | { 5 | "guid": "bf7eb420-11e5-11ea-b7db-4b5d5e7976a9", 6 | "name": "my_org" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /testutil/template/service_route_binding.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2015-11-13T17:02:56Z", 4 | "updated_at": "2016-06-08T16:41:26Z", 5 | "route_service_url": "https://route-service-url.io", 6 | "last_operation": { 7 | "type": "create", 8 | "state": "succeeded", 9 | "description": "Operation succeeded", 10 | "updated_at": "2020-03-10T15:49:32Z", 11 | "created_at": "2020-03-10T15:49:29Z" 12 | }, 13 | "metadata": { 14 | "annotations": { 15 | "foo": "bar" 16 | }, 17 | "labels": { 18 | "baz": "qux" 19 | } 20 | }, 21 | "relationships": { 22 | "service_instance": { 23 | "data": { 24 | "guid": "74f7c078-0934-470f-9883-4fddss5b8f13" 25 | } 26 | }, 27 | "route": { 28 | "data": { 29 | "guid": "8bfe4c1b-9e18-45b1-83be-124163f31f9e" 30 | } 31 | } 32 | }, 33 | "links": { 34 | "self": { 35 | "href": "https://api.example.org/v3/service_route_bindings/{{.GUID}}" 36 | }, 37 | "service_instance": { 38 | "href": "https://api.example.org/v3/service_instances/8bfe4c1b-9e18-45b1-83be-124163f31f9e" 39 | }, 40 | "route": { 41 | "href": "https://api.example.org/v3/routes/74f7c078-0934-470f-9883-4fddss5b8f13" 42 | }, 43 | "parameters": { 44 | "href": "https://api.example.org/v3/service_route_bindings/b3536566-63e2-428f-8f87-a1b99864ada6/parameters" 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /testutil/template/service_usage.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2020-05-28T12:34:56Z", 4 | "updated_at": "2020-05-28T12:34:56Z", 5 | "state": "CREATED", 6 | "space": { 7 | "guid": "guid-5e28f12f-9d80-473e-b826-537b148eb338", 8 | "name": "name-1664" 9 | }, 10 | "organization": { 11 | "guid": "guid-036444f4-f2f5-4ea8-a353-e73330ca0f0a" 12 | }, 13 | "service_instance": { 14 | "guid": "guid-f93250f7-7ef5-4b02-8d33-353919ce8358", 15 | "name": "name-1982", 16 | "type": "managed_service_instance" 17 | }, 18 | "service_plan": { 19 | "guid": "guid-e9d2d5a0-69a6-46ef-bac5-43f3ed177614", 20 | "name": "name-1983" 21 | }, 22 | "service_offering": { 23 | "guid": "guid-34916716-31d7-40c1-9afd-f312996c9654", 24 | "name": "label-64" 25 | }, 26 | "service_broker": { 27 | "guid": "guid-7cc11646-bf38-4f4e-b6e0-9581916a74d9", 28 | "name": "name-2929" 29 | }, 30 | "links": { 31 | "self": { 32 | "href": "https://api.example.org/v3/service_usage_events/{{.GUID}}" 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /testutil/template/sidecar.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "name": "{{.Name}}-sidecar", 4 | "command": "bundle exec rackup", 5 | "process_types": ["web", "worker"], 6 | "memory_in_mb": 300, 7 | "origin": "user", 8 | "relationships": { 9 | "app": { 10 | "data": { 11 | "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446" 12 | } 13 | } 14 | }, 15 | "created_at": "2017-02-01T01:33:58Z", 16 | "updated_at": "2017-02-01T01:33:58Z" 17 | } -------------------------------------------------------------------------------- /testutil/template/space.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2017-02-01T01:33:58Z", 4 | "updated_at": "2017-02-01T01:33:58Z", 5 | "name": "{{.Name}}", 6 | "relationships": { 7 | "organization": { 8 | "data": { 9 | "guid": "e00705b9-7b42-4561-ae97-2520399d2133" 10 | } 11 | }, 12 | "quota": { 13 | "data": null 14 | } 15 | }, 16 | "links": { 17 | "self": { 18 | "href": "https://api.example.org/v3/spaces/{{.GUID}}" 19 | }, 20 | "features": { 21 | "href": "https://api.example.org/v3/spaces/{{.GUID}}/features" 22 | }, 23 | "organization": { 24 | "href": "https://api.example.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" 25 | }, 26 | "apply_manifest": { 27 | "href": "https://api.example.org/v3/spaces/{{.GUID}}/actions/apply_manifest", 28 | "method": "POST" 29 | } 30 | }, 31 | "metadata": { 32 | "labels": {}, 33 | "annotations": {} 34 | } 35 | } -------------------------------------------------------------------------------- /testutil/template/space_quota.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2016-05-04T17:00:41Z", 4 | "updated_at": "2016-05-04T17:00:41Z", 5 | "name": "{{.Name}}", 6 | "apps": { 7 | "total_memory_in_mb": 5120, 8 | "per_process_memory_in_mb": 1024, 9 | "log_rate_limit_in_bytes_per_second": 1024, 10 | "total_instances": 10, 11 | "per_app_tasks": null 12 | }, 13 | "services": { 14 | "paid_services_allowed": true, 15 | "total_service_instances": 10, 16 | "total_service_keys": 20 17 | }, 18 | "routes": { 19 | "total_routes": 8, 20 | "total_reserved_ports": 20 21 | }, 22 | "relationships": { 23 | "organization": { 24 | "data": { 25 | "guid": "9b370018-c38e-44c9-86d6-155c76801104" 26 | } 27 | }, 28 | "spaces": { 29 | "data": [ 30 | { 31 | "guid": "45bb0018-c38e-44c9-86d6-155c76803600" 32 | } 33 | ] 34 | } 35 | }, 36 | "links": { 37 | "self": { 38 | "href": "https://api.example.org/v3/space_quotas/{{.GUID}}" 39 | }, 40 | "organization": { 41 | "href": "https://api.example.org/v3/organizations/9b370018-c38e-44c9-86d6-155c76801104" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /testutil/template/stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2018-11-09T22:43:28Z", 4 | "updated_at": "2018-11-09T22:43:28Z", 5 | "name": "{{.Name}}", 6 | "description": "Here is my stack!", 7 | "build_rootfs_image": "{{.Name}}", 8 | "run_rootfs_image": "{{.Name}}", 9 | "default": false, 10 | "metadata": { 11 | "labels": { }, 12 | "annotations": { } 13 | }, 14 | "links": { 15 | "self": { 16 | "href": "https://api.example.com/v3/stacks/{{.GUID}}" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /testutil/template/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "sequence_id": 1, 4 | "name": "migrate", 5 | "command": "rake db:migrate", 6 | "state": "RUNNING", 7 | "memory_in_mb": 512, 8 | "disk_in_mb": 1024, 9 | "log_rate_limit_in_bytes_per_second": 1024, 10 | "result": { 11 | "failure_reason": null 12 | }, 13 | "droplet_guid": "740ebd2b-162b-469a-bd72-3edb96fabd9a", 14 | "metadata": { 15 | "labels": { }, 16 | "annotations": { } 17 | }, 18 | "created_at": "2016-05-04T17:00:41Z", 19 | "updated_at": "2016-05-04T17:00:42Z", 20 | "relationships": { 21 | "app": { 22 | "data": { 23 | "guid": "ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" 24 | } 25 | } 26 | }, 27 | "links": { 28 | "self": { 29 | "href": "https://api.example.org/v3/tasks/{{.GUID}}" 30 | }, 31 | "app": { 32 | "href": "https://api.example.org/v3/apps/ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" 33 | }, 34 | "cancel": { 35 | "href": "https://api.example.org/v3/tasks/{{.GUID}}/actions/cancel", 36 | "method": "POST" 37 | }, 38 | "droplet": { 39 | "href": "https://api.example.org/v3/droplets/740ebd2b-162b-469a-bd72-3edb96fabd9a" 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /testutil/template/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "{{.GUID}}", 3 | "created_at": "2019-03-08T01:06:19Z", 4 | "updated_at": "2019-03-08T01:06:19Z", 5 | "username": "{{.Name}}", 6 | "presentation_name": "{{.Name}}", 7 | "origin": "uaa", 8 | "metadata": { 9 | "labels": {}, 10 | "annotations":{} 11 | }, 12 | "links": { 13 | "self": { 14 | "href": "https://api.example.org/v3/users/{{.GUID}}" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | _ "gopkg.in/yaml.v2" 5 | ) 6 | --------------------------------------------------------------------------------