├── .gitignore ├── ISSUE_TEMPLATE.md ├── .github ├── dependabot.yml └── workflows │ ├── dependabot-test.yml │ └── run-tests.yml ├── tools └── tools.go ├── bosh ├── vms.go ├── bosh_suite_test.go ├── fixtures │ ├── manifest_template.yml │ └── manifest.yml ├── job_test.go ├── job.go ├── bosh_manifest.go └── bosh_manifest_test.go ├── ci ├── unit-tests.sh └── unit-tests.yml ├── scripts └── run-tests.sh ├── go.mod ├── integration_tests ├── integration_tests_suite_test.go └── testharness │ ├── testvariables │ └── variables.go │ └── main.go ├── CONTRIBUTING.md ├── README.md ├── serviceadapter ├── generate_plan_schemas.go ├── dashboard_url.go ├── service_release_test.go ├── service_deployment_test.go ├── delete_binding.go ├── create_binding.go ├── instance_group_mapping.go ├── fakes │ ├── dashboard_url_generator.go │ ├── schema_generator.go │ ├── manifest_generator.go │ └── binder.go ├── instance_group_mapping_test.go ├── command_line_handler.go ├── dashboard_url_test.go ├── generate_plan_schemas_test.go ├── generate_manifest.go ├── serviceadapter_suite_test.go ├── delete_binding_test.go ├── create_binding_test.go ├── domain.go └── generate_manifest_test.go ├── CODE-OF-CONDUCT.md ├── go.sum └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.coverprofile 3 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | We recommend you open issues [in the ODB release repo](https://github.com/pivotal-cf/on-demand-service-broker-release). 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 8 | ) 9 | 10 | // This file imports packages that are used when running go generate, or used 11 | // during the development process but not otherwise depended on by built code. 12 | -------------------------------------------------------------------------------- /bosh/vms.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package bosh 17 | 18 | type BoshVMs map[string][]string 19 | -------------------------------------------------------------------------------- /ci/unit-tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -eu 2 | 3 | # Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 4 | 5 | # This program and the accompanying materials are made available under 6 | # the terms of the under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | pushd on-demand-services-sdk 19 | scripts/run-tests.sh 20 | popd 21 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 4 | 5 | # This program and the accompanying materials are made available under 6 | # the terms of the under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | set -eu 20 | 21 | go run github.com/onsi/ginkgo/v2/ginkgo -r . 22 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-test.yml: -------------------------------------------------------------------------------- 1 | name: dependabot-pr-merge 2 | on: 3 | workflow_call: 4 | inputs: 5 | pr_number: 6 | description: "The PR number" 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | automerge: 12 | name: Merge Dependabot Pull Pequest 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | if: ${{ github.actor == 'dependabot[bot]' }} 17 | steps: 18 | - name: Merge 19 | uses: actions/github-script@v7 20 | with: 21 | github-token: ${{secrets.GITHUB_TOKEN}} 22 | script: | 23 | var pr_number = ${{ inputs.pr_number }} 24 | github.rest.pulls.merge({ 25 | owner: context.repo.owner, 26 | repo: context.repo.repo, 27 | pull_number: pr_number, 28 | merge_method: 'squash' 29 | }) 30 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | pr_number: 7 | description: "The PR number" 8 | value: ${{ jobs.test.outputs.pr_number }} 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | version: [ 'oldstable', 'stable' ] 19 | name: Go ${{ matrix.version }} 20 | outputs: 21 | pr_number: ${{ github.event.number }} 22 | steps: 23 | - uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.version }} 26 | - uses: actions/checkout@v4 27 | - run: ./scripts/run-tests.sh 28 | call-dependabot-pr-workflow: 29 | needs: test 30 | if: ${{ success() && github.actor == 'dependabot[bot]' }} 31 | uses: ./.github/workflows/dependabot-test.yml 32 | with: 33 | pr_number: ${{ github.event.number }} 34 | -------------------------------------------------------------------------------- /ci/unit-tests.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | # This program and the accompanying materials are made available under 4 | # the terms of the under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | --- 17 | platform: linux 18 | 19 | image_resource: 20 | type: registry-image 21 | source: {repository: dedicatedmysql/odb-ci} 22 | 23 | inputs: 24 | - name: on-demand-services-sdk 25 | 26 | run: 27 | path: on-demand-services-sdk/ci/unit-tests.sh 28 | -------------------------------------------------------------------------------- /bosh/bosh_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package bosh_test 17 | 18 | import ( 19 | "testing" 20 | 21 | . "github.com/onsi/ginkgo/v2" 22 | . "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestBosh(t *testing.T) { 26 | RegisterFailHandler(Fail) 27 | RunSpecs(t, "Bosh Suite") 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pivotal-cf/on-demand-services-sdk 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | code.cloudfoundry.org/brokerapi/v13 v13.0.15 9 | github.com/go-playground/validator/v10 v10.29.0 10 | github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1 11 | github.com/onsi/ginkgo/v2 v2.27.3 12 | github.com/onsi/gomega v1.38.3 13 | github.com/pkg/errors v0.9.1 14 | gopkg.in/yaml.v2 v2.4.0 15 | ) 16 | 17 | require ( 18 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 19 | github.com/gabriel-vasile/mimetype v1.4.11 // indirect 20 | github.com/go-logr/logr v1.4.3 // indirect 21 | github.com/go-playground/locales v0.14.1 // indirect 22 | github.com/go-playground/universal-translator v0.18.1 // indirect 23 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 24 | github.com/google/go-cmp v0.7.0 // indirect 25 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | go.yaml.in/yaml/v3 v3.0.4 // indirect 28 | golang.org/x/crypto v0.45.0 // indirect 29 | golang.org/x/mod v0.30.0 // indirect 30 | golang.org/x/net v0.47.0 // indirect 31 | golang.org/x/sync v0.18.0 // indirect 32 | golang.org/x/sys v0.38.0 // indirect 33 | golang.org/x/text v0.31.0 // indirect 34 | golang.org/x/tools v0.39.0 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /integration_tests/integration_tests_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package integration_tests_test 17 | 18 | import ( 19 | "testing" 20 | 21 | . "github.com/onsi/ginkgo/v2" 22 | . "github.com/onsi/gomega" 23 | "github.com/onsi/gomega/gexec" 24 | ) 25 | 26 | var adapterBin string 27 | 28 | var _ = BeforeSuite(func() { 29 | var err error 30 | adapterBin, err = gexec.Build("github.com/pivotal-cf/on-demand-services-sdk/integration_tests/testharness") 31 | Expect(err).NotTo(HaveOccurred()) 32 | }) 33 | 34 | var _ = AfterSuite(func() { 35 | gexec.CleanupBuildArtifacts() 36 | }) 37 | 38 | func TestIntegrationTests(t *testing.T) { 39 | RegisterFailHandler(Fail) 40 | RunSpecs(t, "IntegrationTests Suite") 41 | } 42 | -------------------------------------------------------------------------------- /integration_tests/testharness/testvariables/variables.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package testvariables 17 | 18 | import "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 19 | 20 | const ( 21 | OperationFailsKey = "OPERATION_FAILS" 22 | 23 | DoNotImplementInterfacesKey = "DO_NOT_IMPLEMENT_INTERFACES" 24 | 25 | ErrAppGuidNotProvided = "no app guid" 26 | ErrBindingAlreadyExists = "binding already exists" 27 | ErrBindingNotFound = "binding not found" 28 | ) 29 | 30 | var SuccessfulBinding = serviceadapter.Binding{ 31 | RouteServiceURL: "a route", 32 | SyslogDrainURL: "a url", 33 | BackupAgentURL: "another url", 34 | Credentials: map[string]interface{}{ 35 | "binding": "this binds", 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /bosh/fixtures/manifest_template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: deployment-name 3 | 4 | releases: 5 | - name: a-release 6 | version: latest 7 | 8 | stemcells: 9 | - alias: greatest 10 | os: Windows 11 | version: "3.1" 12 | {{ if ne (index . "StemcellName") "" -}} 13 | name: {{.StemcellName}} 14 | {{- end }} 15 | 16 | instance_groups: 17 | - name: jerb 18 | instances: 1 19 | jobs: 20 | - name: broker 21 | release: a-release 22 | provides: 23 | some_link: {as: link-name} 24 | consumes: 25 | another_link: {from: jerb-link} 26 | nullified_link: nil 27 | properties: 28 | some_property: some_value 29 | vm_type: massive 30 | vm_extensions: [extended] 31 | persistent_disk_type: big 32 | azs: [az1, az2] 33 | stemcell: greatest 34 | networks: 35 | - name: a-network 36 | static_ips: [10.0.0.0] 37 | default: [dns] 38 | migrated_from: 39 | - name: old-instance-group-name 40 | env: 41 | bosh: 42 | password: passwerd 43 | keep_root_password: true 44 | remove_dev_tools: false 45 | remove_static_libraries: false 46 | swap_size: 0 47 | something_else: foo 48 | 49 | - name: an-errand 50 | lifecycle: errand 51 | instances: 1 52 | jobs: [{name: a-release, release: a-release}] 53 | vm_type: small 54 | stemcell: greatest 55 | networks: [{name: a-network}] 56 | 57 | properties: 58 | foo: bar 59 | 60 | variables: 61 | - name: admin_password 62 | type: password 63 | - name: default_ca 64 | type: certificate 65 | options: 66 | is_ca: true 67 | common_name: some-ca 68 | alternative_names: [some-other-ca] 69 | update: 70 | canaries: 1 71 | canary_watch_time: 30000-180000 72 | update_watch_time: 30000-180000 73 | max_in_flight: {{.MaxInFlight}} 74 | serial: false 75 | tags: 76 | quadrata: parrot 77 | secondTag: tagValue 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to On-Demand Service Broker 2 | 3 | The On-Demand Service Broker project team welcomes contributions from the community. Before you start working with 4 | the On-Demand Service Broker, please 5 | read our [Developer Certificate of Origin](https://cla.vmware.com/dco). All contributions to this repository must be 6 | signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on 7 | as an open-source patch. 8 | 9 | ## Contribution Flow 10 | 11 | This is a rough outline of what a contributor's workflow looks like: 12 | 13 | - Create a topic branch from where you want to base your work 14 | - Make commits of logical units 15 | - Make sure your commit messages are in the proper format (see below) 16 | - Push your changes to a topic branch in your fork of the repository 17 | - Submit a pull request 18 | 19 | Example: 20 | 21 | ``` shell 22 | git remote add upstream https://github.com/vmware/@(project).git 23 | git checkout -b my-new-feature main 24 | git commit -a 25 | git push origin my-new-feature 26 | ``` 27 | 28 | ### Updating pull requests 29 | 30 | If your PR fails to pass CI or needs changes based on code review, you'll most likely want to squash these changes into 31 | existing commits. 32 | 33 | If your pull request contains a single commit or your changes are related to the most recent commit, you can simply 34 | amend the commit. 35 | 36 | Be sure to add a comment to the PR indicating your new changes are ready to review, as GitHub does not generate a 37 | notification when you git push. 38 | 39 | ### Formatting Commit Messages 40 | 41 | We follow the conventions on [Conventional Commits](https://www.conventionalcommits.org/) and 42 | [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/). 43 | 44 | Be sure to include any related GitHub issue references in the commit message. See 45 | [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues 46 | and commits. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Service Adapters for the Cloud Foundry On-Demand Service Broker (ODB) in Golang 2 | 3 | --- 4 | 5 | This is an SDK for writing service adapters for [ODB](https://docs.pivotal.io/svc-sdk/odb) in Golang. It encapsulates the command line invocation handling, parameter parsing, response serialization and error handling so the adapter authors can focus on the service-specific logic in the adapter. This will speed up the time to meeting the service author deliverables outlined [here](https://docs.pivotal.io/svc-sdk/odb/creating.html). 6 | 7 | Before creating a service adapter you will need to have [BOSH release](https://bosh.io/docs) for the service that you wish to deploy. 8 | 9 | After creating the service adapter and service BOSH release, you will be able to [configure the ODB](https://docs.pivotal.io/svc-sdk/odb/operating.html) provision new dedicated service instances from Cloud Foundry! 10 | 11 | --- 12 | 13 | ## Usage 14 | 15 | Please use the SDK tag that matches the ODB release you are targeting. 16 | 17 | For example if using ODB 0.15.1 release, use the [0.15.1 SDK tag](https://github.com/pivotal-cf/on-demand-services-sdk/tree/v0.15.1). 18 | 19 | ### Getting Started 20 | 21 | Follow [this guide](https://docs.pivotal.io/svc-sdk/odb/getting-started.html) to try out an example product. 22 | 23 | ### Examples Service Adapters 24 | 25 | Kafka Service Adapter: https://github.com/pivotal-cf-experimental/kafka-example-service-adapter 26 | 27 | Redis Service Adapter: https://github.com/pivotal-cf-experimental/redis-example-service-adapter 28 | 29 | ### Packaging 30 | 31 | To integrate with the ODB we recommend that you package the service adapter in a BOSH release. 32 | 33 | #### Examples 34 | 35 | Kafka Service Adapter Release: https://github.com/pivotal-cf-experimental/kafka-example-service-adapter-release 36 | 37 | Redis Service Adapter Release: https://github.com/pivotal-cf-experimental/redis-example-service-adapter-release 38 | 39 | --- 40 | 41 | ### Documentation 42 | 43 | SDK Documentation: https://docs.pivotal.io/on-demand-service-broker/creating.html#sdk 44 | 45 | On-Demand Services Documentation: https://docs.pivotal.io/on-demand-service-broker/index.html 46 | 47 | --- 48 | 49 | On Demand Services SDK 50 | 51 | Copyright (c) 2016 - Present Pivotal Software, Inc. All Rights Reserved. 52 | 53 | This product is licensed to you under the Apache License, Version 2.0 (the "License"). 54 | You may not use this product except in compliance with the License. 55 | 56 | This product may include a number of subcomponents with separate copyright notices 57 | and license terms. Your use of these subcomponents is subject to the terms and 58 | conditions of the subcomponent's license, as noted in the LICENSE file. 59 | -------------------------------------------------------------------------------- /serviceadapter/generate_plan_schemas.go: -------------------------------------------------------------------------------- 1 | package serviceadapter 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type GeneratePlanSchemasAction struct { 13 | schemaGenerator SchemaGenerator 14 | errorWriter io.Writer 15 | } 16 | 17 | func NewGeneratePlanSchemasAction(schemaGenerator SchemaGenerator, errorWriter io.Writer) *GeneratePlanSchemasAction { 18 | a := GeneratePlanSchemasAction{ 19 | schemaGenerator: schemaGenerator, 20 | errorWriter: errorWriter, 21 | } 22 | return &a 23 | } 24 | 25 | func (g *GeneratePlanSchemasAction) IsImplemented() bool { 26 | return g.schemaGenerator != nil 27 | } 28 | 29 | func (g *GeneratePlanSchemasAction) ParseArgs(reader io.Reader, args []string) (InputParams, error) { 30 | var inputParams InputParams 31 | 32 | if len(args) > 0 { 33 | fs := flag.NewFlagSet("", flag.ContinueOnError) 34 | planJSON := fs.String("plan-json", "", "Plan JSON") 35 | fs.SetOutput(g.errorWriter) 36 | 37 | err := fs.Parse(args) 38 | if err != nil { 39 | return inputParams, err 40 | } 41 | 42 | if *planJSON == "" { 43 | return inputParams, NewMissingArgsError("-plan-json ") 44 | } 45 | 46 | inputParams = InputParams{ 47 | TextOutput: true, 48 | GeneratePlanSchemas: GeneratePlanSchemasJSONParams{ 49 | Plan: *planJSON, 50 | }, 51 | } 52 | return inputParams, nil 53 | } 54 | 55 | data, err := io.ReadAll(reader) 56 | if err != nil { 57 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error reading input params JSON, error: %s", err)} 58 | } 59 | 60 | if len(data) > 0 { 61 | err = json.Unmarshal(data, &inputParams) 62 | if err != nil { 63 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error unmarshalling input params JSON, error: %s", err)} 64 | } 65 | 66 | return inputParams, err 67 | } 68 | 69 | return inputParams, CLIHandlerError{ErrorExitCode, "expecting parameters to be passed via stdin"} 70 | } 71 | 72 | func (g *GeneratePlanSchemasAction) Execute(inputParams InputParams, outputWriter io.Writer) (err error) { 73 | var plan Plan 74 | if err := json.Unmarshal([]byte(inputParams.GeneratePlanSchemas.Plan), &plan); err != nil { 75 | return errors.Wrap(err, "error unmarshalling plan JSON") 76 | } 77 | if err := plan.Validate(); err != nil { 78 | return errors.Wrap(err, "error validating plan JSON") 79 | } 80 | schema, err := g.schemaGenerator.GeneratePlanSchema(GeneratePlanSchemaParams{Plan: plan}) 81 | if err != nil { 82 | fmt.Fprint(outputWriter, err.Error()) 83 | return CLIHandlerError{ErrorExitCode, err.Error()} 84 | } 85 | 86 | err = json.NewEncoder(outputWriter).Encode(schema) 87 | if err != nil { 88 | return errors.Wrap(err, "error marshalling plan schema") 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /serviceadapter/dashboard_url.go: -------------------------------------------------------------------------------- 1 | package serviceadapter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/pkg/errors" 9 | "gopkg.in/yaml.v2" 10 | 11 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 12 | ) 13 | 14 | type DashboardUrlAction struct { 15 | dashboardUrlGenerator DashboardUrlGenerator 16 | } 17 | 18 | func NewDashboardUrlAction(dashboardUrlGenerator DashboardUrlGenerator) *DashboardUrlAction { 19 | return &DashboardUrlAction{ 20 | dashboardUrlGenerator: dashboardUrlGenerator, 21 | } 22 | } 23 | 24 | func (d *DashboardUrlAction) IsImplemented() bool { 25 | return d.dashboardUrlGenerator != nil 26 | } 27 | 28 | func (d *DashboardUrlAction) ParseArgs(reader io.Reader, args []string) (InputParams, error) { 29 | var inputParams InputParams 30 | 31 | if len(args) > 0 { 32 | if len(args) < 3 { 33 | return inputParams, NewMissingArgsError(" ") 34 | } 35 | 36 | inputParams = InputParams{ 37 | DashboardUrl: DashboardUrlJSONParams{ 38 | InstanceId: args[0], 39 | Plan: args[1], 40 | Manifest: args[2], 41 | }, 42 | } 43 | return inputParams, nil 44 | } 45 | 46 | data, err := io.ReadAll(reader) 47 | if err != nil { 48 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error reading input params JSON, error: %s", err)} 49 | } 50 | 51 | if len(data) > 0 { 52 | err = json.Unmarshal(data, &inputParams) 53 | if err != nil { 54 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error unmarshalling input params JSON, error: %s", err)} 55 | } 56 | 57 | return inputParams, err 58 | } 59 | 60 | return inputParams, CLIHandlerError{ErrorExitCode, "expecting parameters to be passed via stdin"} 61 | } 62 | 63 | func (d *DashboardUrlAction) Execute(inputParams InputParams, outputWriter io.Writer) error { 64 | var plan Plan 65 | if err := json.Unmarshal([]byte(inputParams.DashboardUrl.Plan), &plan); err != nil { 66 | return errors.Wrap(err, "unmarshalling service plan") 67 | } 68 | if err := plan.Validate(); err != nil { 69 | return errors.Wrap(err, "validating service plan") 70 | } 71 | 72 | var manifest bosh.BoshManifest 73 | if err := yaml.Unmarshal([]byte(inputParams.DashboardUrl.Manifest), &manifest); err != nil { 74 | return errors.Wrap(err, "unmarshalling manifest YAML") 75 | } 76 | 77 | params := DashboardUrlParams{ 78 | InstanceID: inputParams.DashboardUrl.InstanceId, 79 | Plan: plan, 80 | Manifest: manifest, 81 | } 82 | dashboardUrl, err := d.dashboardUrlGenerator.DashboardUrl(params) 83 | if err != nil { 84 | fmt.Fprint(outputWriter, err.Error()) 85 | return CLIHandlerError{ErrorExitCode, err.Error()} 86 | } 87 | 88 | if err := json.NewEncoder(outputWriter).Encode(dashboardUrl); err != nil { 89 | return errors.Wrap(err, "marshalling dashboardUrl") 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /bosh/job_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package bosh_test 17 | 18 | import ( 19 | . "github.com/onsi/ginkgo/v2" 20 | . "github.com/onsi/gomega" 21 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 22 | ) 23 | 24 | var _ = Describe("bosh jobs", func() { 25 | It("can add consumer links", func() { 26 | job := bosh.Job{}. 27 | AddConsumesLink("foo", "a-job"). 28 | AddConsumesLink("bar", "other-job") 29 | Expect(job.Consumes["foo"]).To(Equal(bosh.ConsumesLink{From: "a-job"})) 30 | Expect(job.Consumes["bar"]).To(Equal(bosh.ConsumesLink{From: "other-job"})) 31 | }) 32 | 33 | It("can add provider links", func() { 34 | job := bosh.Job{}. 35 | AddSharedProvidesLink("foo"). 36 | AddSharedProvidesLink("bar") 37 | Expect(job.Provides["foo"]).To(Equal(bosh.ProvidesLink{Shared: true})) 38 | Expect(job.Provides["bar"]).To(Equal(bosh.ProvidesLink{Shared: true})) 39 | }) 40 | 41 | It("can add provider links with aliases", func() { 42 | alias := bosh.Alias{ 43 | Domain: "some.domain.internal", 44 | HealthFilter: "healthy", 45 | InitialHealthCheck: "asynchronous", 46 | } 47 | 48 | job := bosh.Job{}.AddProvidesLinkWithAliases("mylink", []bosh.Alias{alias}) 49 | 50 | Expect(job.Provides["mylink"].Aliases).To(Equal([]bosh.Alias{alias})) 51 | }) 52 | 53 | It("can cross deployment links", func() { 54 | job := bosh.Job{}.AddCrossDeploymentConsumesLink("foo", "a-job", "a-deployment") 55 | Expect(job.Consumes["foo"]).To(Equal(bosh.ConsumesLink{From: "a-job", Deployment: "a-deployment"})) 56 | }) 57 | 58 | It("can add nullified links", func() { 59 | job := bosh.Job{}.AddNullifiedConsumesLink("not-wired") 60 | Expect(job.Consumes["not-wired"]).To(Equal("nil")) // Yes, this really should be string "nil" 61 | }) 62 | 63 | It("can add custom provider definitions", func() { 64 | job := bosh.Job{}.AddCustomProviderDefinition("some-name", "some-type", []string{"prop1"}) 65 | job = job.AddCustomProviderDefinition("some-other", "some-other-type", nil) 66 | Expect(job.CustomProviderDefinitions).To( 67 | ConsistOf( 68 | bosh.CustomProviderDefinition{Name: "some-name", Type: "some-type", Properties: []string{"prop1"}}, 69 | bosh.CustomProviderDefinition{Name: "some-other", Type: "some-other-type"}, 70 | ), 71 | ) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /serviceadapter/service_release_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package serviceadapter_test 17 | 18 | import ( 19 | "encoding/json" 20 | 21 | . "github.com/onsi/ginkgo/v2" 22 | . "github.com/onsi/gomega" 23 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 24 | ) 25 | 26 | var _ = Describe("ServiceRelease", func() { 27 | Describe("(De)serialising from JSON", func() { 28 | serviceReleaseJson := []byte(`{ 29 | "name": "kafka", 30 | "version": "dev.1", 31 | "jobs": ["kafka_node", "zookeeper", "whatever you need"] 32 | }`) 33 | 34 | expectedServiceRelease := serviceadapter.ServiceRelease{ 35 | Name: "kafka", 36 | Version: "dev.1", 37 | Jobs: []string{"kafka_node", "zookeeper", "whatever you need"}, 38 | } 39 | 40 | It("deserialises JSON into a ServiceRelease object", func() { 41 | var serviceRelease serviceadapter.ServiceRelease 42 | Expect(json.Unmarshal(serviceReleaseJson, &serviceRelease)).To(Succeed()) 43 | Expect(serviceRelease).To(Equal(expectedServiceRelease)) 44 | }) 45 | 46 | It("serialises a ServiceRelease object to JSON", func() { 47 | Expect(toJson(expectedServiceRelease)).To(MatchJSON(serviceReleaseJson)) 48 | }) 49 | }) 50 | 51 | Describe("Validating", func() { 52 | It("returns no error when there is at least one valid release", func() { 53 | serviceReleases := serviceadapter.ServiceReleases{ 54 | {Name: "foo", Version: "bar", Jobs: []string{"baz"}}, 55 | } 56 | Expect(serviceReleases.Validate()).To(Succeed()) 57 | }) 58 | 59 | It("returns an error if there are no service releases", func() { 60 | serviceReleases := serviceadapter.ServiceReleases{} 61 | Expect(serviceReleases.Validate()).NotTo(Succeed()) 62 | }) 63 | 64 | It("returns an error if a release is missing a field", func() { 65 | serviceReleases := serviceadapter.ServiceReleases{ 66 | {Name: "foo", Version: "bar", Jobs: []string{"baz"}}, 67 | {Name: "qux", Jobs: []string{"quux"}}, 68 | } 69 | Expect(serviceReleases.Validate()).NotTo(Succeed()) 70 | }) 71 | 72 | It("returns an error if a release provides no jobs", func() { 73 | serviceReleases := serviceadapter.ServiceReleases{ 74 | {Name: "foo", Version: "bar", Jobs: []string{"baz"}}, 75 | {Name: "qux", Version: "quux", Jobs: []string{}}, 76 | } 77 | Expect(serviceReleases.Validate()).NotTo(Succeed()) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /serviceadapter/service_deployment_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package serviceadapter_test 17 | 18 | import ( 19 | "encoding/json" 20 | 21 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | ) 26 | 27 | var validDeployment serviceadapter.ServiceDeployment 28 | 29 | var _ = Describe("ServiceDeployment", func() { 30 | BeforeEach(func() { 31 | validDeployment = serviceadapter.ServiceDeployment{ 32 | DeploymentName: "service-instance-deployment", 33 | Releases: serviceadapter.ServiceReleases{ 34 | { 35 | Name: "release-name", 36 | Version: "release-version", 37 | Jobs: []string{"job_one", "job_two"}, 38 | }, 39 | }, 40 | Stemcells: []serviceadapter.Stemcell{{ 41 | Name: "beos-fips-stemcell", 42 | OS: "BeOS", 43 | Version: "2", 44 | }}, 45 | } 46 | }) 47 | 48 | Describe("(De)serialising JSON", func() { 49 | var expectedServiceDeployment serviceadapter.ServiceDeployment 50 | 51 | serviceDeploymentJSON := []byte(`{ 52 | "deployment_name": "service-instance-deployment", 53 | "releases": [{ 54 | "name": "release-name", 55 | "version": "release-version", 56 | "jobs": [ 57 | "job_one", 58 | "job_two" 59 | ] 60 | }], 61 | "stemcells": [{ 62 | "stemcell_name": "beos-fips-stemcell", 63 | "stemcell_os": "BeOS", 64 | "stemcell_version": "2" 65 | }] 66 | }`) 67 | 68 | JustBeforeEach(func() { 69 | expectedServiceDeployment = validDeployment 70 | }) 71 | 72 | It("deserialises a ServiceDeployment object from JSON", func() { 73 | var serviceDeployment serviceadapter.ServiceDeployment 74 | Expect(json.Unmarshal(serviceDeploymentJSON, &serviceDeployment)).To(Succeed()) 75 | Expect(serviceDeployment).To(Equal(validDeployment)) 76 | }) 77 | 78 | It("serialises a ServiceDeployment object to JSON", func() { 79 | Expect(toJson(expectedServiceDeployment)).To(MatchJSON(serviceDeploymentJSON)) 80 | }) 81 | }) 82 | 83 | Describe("validation", func() { 84 | It("returns no error when all fields non-empty", func() { 85 | Expect(validDeployment.Validate()).To(Succeed()) 86 | }) 87 | 88 | It("returns an error when a field is empty", func() { 89 | invalidServiceDeployment := serviceadapter.ServiceDeployment{ 90 | DeploymentName: "service-instance-deployment", 91 | Stemcells: []serviceadapter.Stemcell{{ 92 | OS: "BeOS", 93 | Version: "2", 94 | }}, 95 | } 96 | Expect(invalidServiceDeployment.Validate()).NotTo(Succeed()) 97 | }) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /bosh/fixtures/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: deployment-name 3 | 4 | addons: 5 | - name: some-addon 6 | jobs: 7 | - name: the-italian-job 8 | release: "2003" 9 | include: 10 | stemcell: 11 | - os: ubuntu-trusty 12 | deployments: 13 | - dep1 14 | - dep2 15 | jobs: 16 | - name: the-italian-job-old 17 | release: "1969" 18 | instance_groups: 19 | - an-errand 20 | networks: 21 | - some-network 22 | teams: 23 | - a-team 24 | exclude: 25 | stemcell: 26 | - os: ubuntu-jammy 27 | deployments: 28 | - dep3 29 | jobs: 30 | - name: the-italian-job 31 | release: "1969" 32 | instance_groups: 33 | - an-errand 34 | networks: 35 | - some-network 36 | teams: 37 | - a-team 38 | 39 | releases: 40 | - name: a-release 41 | version: latest 42 | 43 | stemcells: 44 | - alias: greatest 45 | os: Windows 46 | version: "3.1" 47 | 48 | instance_groups: 49 | - name: jerb 50 | instances: 1 51 | jobs: 52 | - name: broker 53 | release: a-release 54 | provides: 55 | some_link: {as: link-name} 56 | consumes: 57 | another_link: {from: jerb-link} 58 | nullified_link: nil 59 | custom_provider_definitions: 60 | - name: some-custom-link 61 | type: some-link-type 62 | properties: 63 | - prop1 64 | - url 65 | properties: 66 | some_property: some_value 67 | vm_type: massive 68 | vm_extensions: [extended] 69 | persistent_disk_type: big 70 | azs: [az1, az2] 71 | stemcell: greatest 72 | networks: 73 | - name: a-network 74 | static_ips: [10.0.0.0] 75 | default: [dns] 76 | migrated_from: 77 | - name: old-instance-group-name 78 | env: 79 | bosh: 80 | password: passwerd 81 | keep_root_password: true 82 | remove_dev_tools: false 83 | remove_static_libraries: false 84 | swap_size: 0 85 | something_else: foo 86 | update: 87 | canaries: 1 88 | canary_watch_time: 30000-180000 89 | update_watch_time: 30000-180000 90 | max_in_flight: 10 91 | serial: false 92 | initial_deploy_az_update_strategy: parallel 93 | - name: an-errand 94 | lifecycle: errand 95 | instances: 1 96 | jobs: [{name: a-release, release: a-release}] 97 | vm_type: small 98 | stemcell: greatest 99 | networks: [{name: a-network}] 100 | 101 | properties: 102 | foo: bar 103 | 104 | variables: 105 | - name: admin_password 106 | type: password 107 | - name: default_ca 108 | type: certificate 109 | update_mode: converge 110 | options: 111 | is_ca: true 112 | alternative_names: [some-other-ca] 113 | consumes: 114 | alternative_name: 115 | from: my-custom-app-server-address 116 | common_name: 117 | from: my-custom-app-server-address 118 | properties: 119 | wildcard: true 120 | update: 121 | canaries: 1 122 | canary_watch_time: 30000-180000 123 | update_watch_time: 30000-180000 124 | max_in_flight: 4 125 | serial: false 126 | vm_strategy: create-and-swap 127 | initial_deploy_az_update_strategy: serial 128 | tags: 129 | quadrata: parrot 130 | secondTag: tagValue 131 | features: 132 | randomize_az_placement: true 133 | use_short_dns_addresses: false 134 | another_feature: ok 135 | -------------------------------------------------------------------------------- /bosh/job.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package bosh 17 | 18 | type Job struct { 19 | Name string `yaml:"name"` 20 | Release string `yaml:"release"` 21 | Provides map[string]ProvidesLink `yaml:"provides,omitempty"` 22 | Consumes map[string]interface{} `yaml:"consumes,omitempty"` 23 | CustomProviderDefinitions []CustomProviderDefinition `yaml:"custom_provider_definitions,omitempty"` 24 | Properties map[string]interface{} `yaml:"properties,omitempty"` 25 | } 26 | 27 | type CustomProviderDefinition struct { 28 | Name string `yaml:"name"` 29 | Type string `yaml:"type"` 30 | Properties []string `yaml:"properties,omitempty"` 31 | } 32 | 33 | type ProvidesLink struct { 34 | As string `yaml:"as,omitempty"` 35 | Shared bool `yaml:"shared,omitempty"` 36 | Aliases []Alias `yaml:"aliases,omitempty"` 37 | } 38 | 39 | type Alias struct { 40 | Domain string `yaml:"domain"` 41 | HealthFilter string `yaml:"health_filter,omitempty"` 42 | InitialHealthCheck string `yaml:"initial_health_check,omitempty"` 43 | PlaceHolderType string `yaml:"placeholder_type,omitempty"` 44 | } 45 | 46 | type ConsumesLink struct { 47 | From string `yaml:"from,omitempty"` 48 | Deployment string `yaml:"deployment,omitempty"` 49 | Network string `yaml:"network,omitempty"` 50 | } 51 | 52 | func (j Job) AddCustomProviderDefinition(name, providerType string, properties []string) Job { 53 | if j.CustomProviderDefinitions == nil { 54 | j.CustomProviderDefinitions = []CustomProviderDefinition{} 55 | } 56 | j.CustomProviderDefinitions = append( 57 | j.CustomProviderDefinitions, 58 | CustomProviderDefinition{Name: name, Type: providerType, Properties: properties}, 59 | ) 60 | return j 61 | } 62 | 63 | func (j Job) AddSharedProvidesLink(name string) Job { 64 | return j.addProvidesLink(name, ProvidesLink{Shared: true}) 65 | } 66 | 67 | func (j Job) AddProvidesLinkWithAliases(name string, aliases []Alias) Job { 68 | return j.addProvidesLink(name, ProvidesLink{Aliases: aliases}) 69 | } 70 | 71 | func (j Job) AddConsumesLink(name, fromJob string) Job { 72 | return j.addConsumesLink(name, ConsumesLink{From: fromJob}) 73 | } 74 | 75 | func (j Job) AddCrossDeploymentConsumesLink(name, fromJob, deployment string) Job { 76 | return j.addConsumesLink(name, ConsumesLink{From: fromJob, Deployment: deployment}) 77 | } 78 | 79 | func (j Job) AddNullifiedConsumesLink(name string) Job { 80 | return j.addConsumesLink(name, "nil") 81 | } 82 | 83 | func (j Job) addConsumesLink(name string, value interface{}) Job { 84 | if j.Consumes == nil { 85 | j.Consumes = map[string]interface{}{} 86 | } 87 | j.Consumes[name] = value 88 | return j 89 | } 90 | 91 | func (j Job) addProvidesLink(name string, providesLink ProvidesLink) Job { 92 | if j.Provides == nil { 93 | j.Provides = map[string]ProvidesLink{} 94 | } 95 | j.Provides[name] = providesLink 96 | return j 97 | } 98 | -------------------------------------------------------------------------------- /serviceadapter/delete_binding.go: -------------------------------------------------------------------------------- 1 | package serviceadapter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/pkg/errors" 9 | "gopkg.in/yaml.v2" 10 | 11 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 12 | ) 13 | 14 | type DeleteBindingAction struct { 15 | unbinder Binder 16 | } 17 | 18 | func NewDeleteBindingAction(unbinder Binder) *DeleteBindingAction { 19 | return &DeleteBindingAction{ 20 | unbinder: unbinder, 21 | } 22 | } 23 | 24 | func (d *DeleteBindingAction) IsImplemented() bool { 25 | return d.unbinder != nil 26 | } 27 | 28 | func (d *DeleteBindingAction) ParseArgs(reader io.Reader, args []string) (InputParams, error) { 29 | var inputParams InputParams 30 | 31 | if len(args) > 0 { 32 | if len(args) < 4 { 33 | return inputParams, NewMissingArgsError(" ") 34 | } 35 | 36 | inputParams = InputParams{ 37 | DeleteBinding: DeleteBindingJSONParams{ 38 | BindingId: args[0], 39 | BoshVms: args[1], 40 | Manifest: args[2], 41 | RequestParameters: args[3], 42 | }, 43 | } 44 | return inputParams, nil 45 | } 46 | 47 | data, err := io.ReadAll(reader) 48 | if err != nil { 49 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error reading input params JSON, error: %s", err)} 50 | } 51 | 52 | if len(data) > 0 { 53 | err = json.Unmarshal(data, &inputParams) 54 | if err != nil { 55 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error unmarshalling input params JSON, error: %s", err)} 56 | } 57 | 58 | return inputParams, err 59 | } 60 | 61 | return inputParams, CLIHandlerError{ErrorExitCode, "expecting parameters to be passed via stdin"} 62 | } 63 | 64 | func (d *DeleteBindingAction) Execute(inputParams InputParams, outputWriter io.Writer) error { 65 | var boshVMs map[string][]string 66 | if err := json.Unmarshal([]byte(inputParams.DeleteBinding.BoshVms), &boshVMs); err != nil { 67 | return errors.Wrap(err, "unmarshalling BOSH VMs") 68 | } 69 | 70 | var manifest bosh.BoshManifest 71 | if err := yaml.Unmarshal([]byte(inputParams.DeleteBinding.Manifest), &manifest); err != nil { 72 | return errors.Wrap(err, "unmarshalling manifest YAML") 73 | } 74 | 75 | var reqParams map[string]interface{} 76 | if err := json.Unmarshal([]byte(inputParams.DeleteBinding.RequestParameters), &reqParams); err != nil { 77 | return errors.Wrap(err, "unmarshalling request binding parameters") 78 | } 79 | 80 | var secrets ManifestSecrets 81 | if inputParams.DeleteBinding.Secrets != "" { 82 | if err := json.Unmarshal([]byte(inputParams.DeleteBinding.Secrets), &secrets); err != nil { 83 | return errors.Wrap(err, "unmarshalling secrets") 84 | } 85 | } 86 | 87 | var dnsAddresses DNSAddresses 88 | if inputParams.DeleteBinding.DNSAddresses != "" { 89 | if err := json.Unmarshal([]byte(inputParams.DeleteBinding.DNSAddresses), &dnsAddresses); err != nil { 90 | return errors.Wrap(err, "unmarshalling DNS addresses") 91 | } 92 | } 93 | 94 | params := DeleteBindingParams{ 95 | BindingID: inputParams.DeleteBinding.BindingId, 96 | DeploymentTopology: boshVMs, 97 | Manifest: manifest, 98 | RequestParams: reqParams, 99 | Secrets: secrets, 100 | DNSAddresses: dnsAddresses, 101 | } 102 | err := d.unbinder.DeleteBinding(params) 103 | if err != nil { 104 | fmt.Fprint(outputWriter, err.Error()) 105 | switch err.(type) { 106 | case BindingNotFoundError: 107 | return CLIHandlerError{BindingNotFoundErrorExitCode, err.Error()} 108 | default: 109 | return CLIHandlerError{ErrorExitCode, err.Error()} 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /serviceadapter/create_binding.go: -------------------------------------------------------------------------------- 1 | package serviceadapter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/pkg/errors" 9 | "gopkg.in/yaml.v2" 10 | 11 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 12 | ) 13 | 14 | type CreateBindingAction struct { 15 | bindingCreator Binder 16 | } 17 | 18 | func NewCreateBindingAction(binder Binder) *CreateBindingAction { 19 | action := CreateBindingAction{ 20 | bindingCreator: binder, 21 | } 22 | return &action 23 | } 24 | 25 | func (a *CreateBindingAction) IsImplemented() bool { 26 | return a.bindingCreator != nil 27 | } 28 | 29 | func (a *CreateBindingAction) ParseArgs(reader io.Reader, args []string) (InputParams, error) { 30 | var inputParams InputParams 31 | 32 | if len(args) > 0 { 33 | if len(args) < 4 { 34 | return inputParams, NewMissingArgsError(" ") 35 | } 36 | 37 | inputParams = InputParams{ 38 | CreateBinding: CreateBindingJSONParams{ 39 | BindingId: args[0], 40 | BoshVms: args[1], 41 | Manifest: args[2], 42 | RequestParameters: args[3], 43 | }, 44 | } 45 | return inputParams, nil 46 | } 47 | 48 | data, err := io.ReadAll(reader) 49 | if err != nil { 50 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error reading input params JSON, error: %s", err)} 51 | } 52 | 53 | if len(data) > 0 { 54 | err = json.Unmarshal(data, &inputParams) 55 | if err != nil { 56 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error unmarshalling input params JSON, error: %s", err)} 57 | } 58 | 59 | return inputParams, err 60 | } 61 | 62 | return inputParams, CLIHandlerError{ErrorExitCode, "expecting parameters to be passed via stdin"} 63 | } 64 | 65 | func (a *CreateBindingAction) Execute(inputParams InputParams, outputWriter io.Writer) error { 66 | var boshVMs map[string][]string 67 | if err := json.Unmarshal([]byte(inputParams.CreateBinding.BoshVms), &boshVMs); err != nil { 68 | return errors.Wrap(err, "unmarshalling BOSH VMs") 69 | } 70 | 71 | var manifest bosh.BoshManifest 72 | if err := yaml.Unmarshal([]byte(inputParams.CreateBinding.Manifest), &manifest); err != nil { 73 | return errors.Wrap(err, "unmarshalling manifest YAML") 74 | } 75 | 76 | var reqParams map[string]interface{} 77 | if err := json.Unmarshal([]byte(inputParams.CreateBinding.RequestParameters), &reqParams); err != nil { 78 | return errors.Wrap(err, "unmarshalling request binding parameters") 79 | } 80 | 81 | var secrets ManifestSecrets 82 | if inputParams.CreateBinding.Secrets != "" { 83 | if err := json.Unmarshal([]byte(inputParams.CreateBinding.Secrets), &secrets); err != nil { 84 | return errors.Wrap(err, "unmarshalling secrets") 85 | } 86 | } 87 | 88 | var dnsAddresses DNSAddresses 89 | if inputParams.CreateBinding.DNSAddresses != "" { 90 | if err := json.Unmarshal([]byte(inputParams.CreateBinding.DNSAddresses), &dnsAddresses); err != nil { 91 | return errors.Wrap(err, "unmarshalling DNS addresses") 92 | } 93 | } 94 | 95 | params := CreateBindingParams{ 96 | BindingID: inputParams.CreateBinding.BindingId, 97 | DeploymentTopology: boshVMs, 98 | Manifest: manifest, 99 | RequestParams: reqParams, 100 | Secrets: secrets, 101 | DNSAddresses: dnsAddresses, 102 | } 103 | binding, err := a.bindingCreator.CreateBinding(params) 104 | if err != nil { 105 | fmt.Fprint(outputWriter, err.Error()) 106 | switch err := err.(type) { 107 | case BindingAlreadyExistsError: 108 | return CLIHandlerError{BindingAlreadyExistsErrorExitCode, err.Error()} 109 | case AppGuidNotProvidedError: 110 | return CLIHandlerError{AppGuidNotProvidedErrorExitCode, err.Error()} 111 | default: 112 | return CLIHandlerError{ErrorExitCode, err.Error()} 113 | } 114 | } 115 | 116 | if err := json.NewEncoder(outputWriter).Encode(binding); err != nil { 117 | return errors.Wrap(err, "error marshalling binding") 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /serviceadapter/instance_group_mapping.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package serviceadapter 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 23 | ) 24 | 25 | func GenerateInstanceGroupsWithNoProperties( 26 | instanceGroups []InstanceGroup, 27 | serviceReleases ServiceReleases, 28 | stemcell string, 29 | deploymentInstanceGroupsToJobs map[string][]string, 30 | ) ([]bosh.InstanceGroup, error) { 31 | if len(instanceGroups) == 0 { 32 | return nil, fmt.Errorf("no instance groups provided") 33 | } 34 | 35 | boshInstanceGroups := []bosh.InstanceGroup{} 36 | for _, instanceGroup := range instanceGroups { 37 | if _, ok := deploymentInstanceGroupsToJobs[instanceGroup.Name]; !ok { 38 | continue 39 | } 40 | 41 | networks := []bosh.Network{} 42 | 43 | for _, network := range instanceGroup.Networks { 44 | networks = append(networks, bosh.Network{Name: network}) 45 | } 46 | 47 | boshJobs, err := generateJobsForInstanceGroup(instanceGroup.Name, deploymentInstanceGroupsToJobs, serviceReleases) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | var migrations []bosh.Migration 53 | 54 | if len(instanceGroup.MigratedFrom) > 0 { 55 | for _, migration := range instanceGroup.MigratedFrom { 56 | migrations = append(migrations, bosh.Migration{Name: migration.Name}) 57 | } 58 | } 59 | 60 | boshInstanceGroup := bosh.InstanceGroup{ 61 | Name: instanceGroup.Name, 62 | Instances: instanceGroup.Instances, 63 | Stemcell: stemcell, 64 | VMType: instanceGroup.VMType, 65 | VMExtensions: instanceGroup.VMExtensions, 66 | PersistentDiskType: instanceGroup.PersistentDiskType, 67 | AZs: instanceGroup.AZs, 68 | Networks: networks, 69 | Jobs: boshJobs, 70 | Lifecycle: instanceGroup.Lifecycle, 71 | MigratedFrom: migrations, 72 | } 73 | boshInstanceGroups = append(boshInstanceGroups, boshInstanceGroup) 74 | } 75 | return boshInstanceGroups, nil 76 | } 77 | 78 | func FindReleaseForJob(jobName string, releases ServiceReleases) (ServiceRelease, error) { 79 | releasesThatMentionJob := []ServiceRelease{} 80 | for _, release := range releases { 81 | for _, job := range release.Jobs { 82 | if job == jobName { 83 | releasesThatMentionJob = append(releasesThatMentionJob, release) 84 | } 85 | } 86 | } 87 | 88 | if len(releasesThatMentionJob) == 0 { 89 | return ServiceRelease{}, fmt.Errorf("job '%s' not provided", jobName) 90 | } 91 | 92 | if len(releasesThatMentionJob) > 1 { 93 | releaseNames := []string{} 94 | for _, release := range releasesThatMentionJob { 95 | releaseNames = append(releaseNames, release.Name) 96 | } 97 | return ServiceRelease{}, fmt.Errorf("job '%s' provided %d times, by %s", jobName, len(releasesThatMentionJob), strings.Join(releaseNames, ", ")) 98 | } 99 | 100 | return releasesThatMentionJob[0], nil 101 | } 102 | 103 | func generateJobsForInstanceGroup(instanceGroupName string, deploymentInstanceGroupsToJobs map[string][]string, serviceReleases ServiceReleases) ([]bosh.Job, error) { 104 | boshJobs := []bosh.Job{} 105 | for _, job := range deploymentInstanceGroupsToJobs[instanceGroupName] { 106 | release, err := FindReleaseForJob(job, serviceReleases) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | boshJobs = append(boshJobs, bosh.Job{Name: job, Release: release.Name}) 112 | } 113 | return boshJobs, nil 114 | } 115 | -------------------------------------------------------------------------------- /serviceadapter/fakes/dashboard_url_generator.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 8 | ) 9 | 10 | type FakeDashboardUrlGenerator struct { 11 | DashboardUrlStub func(serviceadapter.DashboardUrlParams) (serviceadapter.DashboardUrl, error) 12 | dashboardUrlMutex sync.RWMutex 13 | dashboardUrlArgsForCall []struct { 14 | arg1 serviceadapter.DashboardUrlParams 15 | } 16 | dashboardUrlReturns struct { 17 | result1 serviceadapter.DashboardUrl 18 | result2 error 19 | } 20 | dashboardUrlReturnsOnCall map[int]struct { 21 | result1 serviceadapter.DashboardUrl 22 | result2 error 23 | } 24 | invocations map[string][][]interface{} 25 | invocationsMutex sync.RWMutex 26 | } 27 | 28 | func (fake *FakeDashboardUrlGenerator) DashboardUrl(arg1 serviceadapter.DashboardUrlParams) (serviceadapter.DashboardUrl, error) { 29 | fake.dashboardUrlMutex.Lock() 30 | ret, specificReturn := fake.dashboardUrlReturnsOnCall[len(fake.dashboardUrlArgsForCall)] 31 | fake.dashboardUrlArgsForCall = append(fake.dashboardUrlArgsForCall, struct { 32 | arg1 serviceadapter.DashboardUrlParams 33 | }{arg1}) 34 | fake.recordInvocation("DashboardUrl", []interface{}{arg1}) 35 | fake.dashboardUrlMutex.Unlock() 36 | if fake.DashboardUrlStub != nil { 37 | return fake.DashboardUrlStub(arg1) 38 | } 39 | if specificReturn { 40 | return ret.result1, ret.result2 41 | } 42 | fakeReturns := fake.dashboardUrlReturns 43 | return fakeReturns.result1, fakeReturns.result2 44 | } 45 | 46 | func (fake *FakeDashboardUrlGenerator) DashboardUrlCallCount() int { 47 | fake.dashboardUrlMutex.RLock() 48 | defer fake.dashboardUrlMutex.RUnlock() 49 | return len(fake.dashboardUrlArgsForCall) 50 | } 51 | 52 | func (fake *FakeDashboardUrlGenerator) DashboardUrlCalls(stub func(serviceadapter.DashboardUrlParams) (serviceadapter.DashboardUrl, error)) { 53 | fake.dashboardUrlMutex.Lock() 54 | defer fake.dashboardUrlMutex.Unlock() 55 | fake.DashboardUrlStub = stub 56 | } 57 | 58 | func (fake *FakeDashboardUrlGenerator) DashboardUrlArgsForCall(i int) serviceadapter.DashboardUrlParams { 59 | fake.dashboardUrlMutex.RLock() 60 | defer fake.dashboardUrlMutex.RUnlock() 61 | argsForCall := fake.dashboardUrlArgsForCall[i] 62 | return argsForCall.arg1 63 | } 64 | 65 | func (fake *FakeDashboardUrlGenerator) DashboardUrlReturns(result1 serviceadapter.DashboardUrl, result2 error) { 66 | fake.dashboardUrlMutex.Lock() 67 | defer fake.dashboardUrlMutex.Unlock() 68 | fake.DashboardUrlStub = nil 69 | fake.dashboardUrlReturns = struct { 70 | result1 serviceadapter.DashboardUrl 71 | result2 error 72 | }{result1, result2} 73 | } 74 | 75 | func (fake *FakeDashboardUrlGenerator) DashboardUrlReturnsOnCall(i int, result1 serviceadapter.DashboardUrl, result2 error) { 76 | fake.dashboardUrlMutex.Lock() 77 | defer fake.dashboardUrlMutex.Unlock() 78 | fake.DashboardUrlStub = nil 79 | if fake.dashboardUrlReturnsOnCall == nil { 80 | fake.dashboardUrlReturnsOnCall = make(map[int]struct { 81 | result1 serviceadapter.DashboardUrl 82 | result2 error 83 | }) 84 | } 85 | fake.dashboardUrlReturnsOnCall[i] = struct { 86 | result1 serviceadapter.DashboardUrl 87 | result2 error 88 | }{result1, result2} 89 | } 90 | 91 | func (fake *FakeDashboardUrlGenerator) Invocations() map[string][][]interface{} { 92 | fake.invocationsMutex.RLock() 93 | defer fake.invocationsMutex.RUnlock() 94 | fake.dashboardUrlMutex.RLock() 95 | defer fake.dashboardUrlMutex.RUnlock() 96 | copiedInvocations := map[string][][]interface{}{} 97 | for key, value := range fake.invocations { 98 | copiedInvocations[key] = value 99 | } 100 | return copiedInvocations 101 | } 102 | 103 | func (fake *FakeDashboardUrlGenerator) recordInvocation(key string, args []interface{}) { 104 | fake.invocationsMutex.Lock() 105 | defer fake.invocationsMutex.Unlock() 106 | if fake.invocations == nil { 107 | fake.invocations = map[string][][]interface{}{} 108 | } 109 | if fake.invocations[key] == nil { 110 | fake.invocations[key] = [][]interface{}{} 111 | } 112 | fake.invocations[key] = append(fake.invocations[key], args) 113 | } 114 | 115 | var _ serviceadapter.DashboardUrlGenerator = new(FakeDashboardUrlGenerator) 116 | -------------------------------------------------------------------------------- /serviceadapter/fakes/schema_generator.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 8 | ) 9 | 10 | type FakeSchemaGenerator struct { 11 | GeneratePlanSchemaStub func(serviceadapter.GeneratePlanSchemaParams) (serviceadapter.PlanSchema, error) 12 | generatePlanSchemaMutex sync.RWMutex 13 | generatePlanSchemaArgsForCall []struct { 14 | arg1 serviceadapter.GeneratePlanSchemaParams 15 | } 16 | generatePlanSchemaReturns struct { 17 | result1 serviceadapter.PlanSchema 18 | result2 error 19 | } 20 | generatePlanSchemaReturnsOnCall map[int]struct { 21 | result1 serviceadapter.PlanSchema 22 | result2 error 23 | } 24 | invocations map[string][][]interface{} 25 | invocationsMutex sync.RWMutex 26 | } 27 | 28 | func (fake *FakeSchemaGenerator) GeneratePlanSchema(arg1 serviceadapter.GeneratePlanSchemaParams) (serviceadapter.PlanSchema, error) { 29 | fake.generatePlanSchemaMutex.Lock() 30 | ret, specificReturn := fake.generatePlanSchemaReturnsOnCall[len(fake.generatePlanSchemaArgsForCall)] 31 | fake.generatePlanSchemaArgsForCall = append(fake.generatePlanSchemaArgsForCall, struct { 32 | arg1 serviceadapter.GeneratePlanSchemaParams 33 | }{arg1}) 34 | fake.recordInvocation("GeneratePlanSchema", []interface{}{arg1}) 35 | fake.generatePlanSchemaMutex.Unlock() 36 | if fake.GeneratePlanSchemaStub != nil { 37 | return fake.GeneratePlanSchemaStub(arg1) 38 | } 39 | if specificReturn { 40 | return ret.result1, ret.result2 41 | } 42 | fakeReturns := fake.generatePlanSchemaReturns 43 | return fakeReturns.result1, fakeReturns.result2 44 | } 45 | 46 | func (fake *FakeSchemaGenerator) GeneratePlanSchemaCallCount() int { 47 | fake.generatePlanSchemaMutex.RLock() 48 | defer fake.generatePlanSchemaMutex.RUnlock() 49 | return len(fake.generatePlanSchemaArgsForCall) 50 | } 51 | 52 | func (fake *FakeSchemaGenerator) GeneratePlanSchemaCalls(stub func(serviceadapter.GeneratePlanSchemaParams) (serviceadapter.PlanSchema, error)) { 53 | fake.generatePlanSchemaMutex.Lock() 54 | defer fake.generatePlanSchemaMutex.Unlock() 55 | fake.GeneratePlanSchemaStub = stub 56 | } 57 | 58 | func (fake *FakeSchemaGenerator) GeneratePlanSchemaArgsForCall(i int) serviceadapter.GeneratePlanSchemaParams { 59 | fake.generatePlanSchemaMutex.RLock() 60 | defer fake.generatePlanSchemaMutex.RUnlock() 61 | argsForCall := fake.generatePlanSchemaArgsForCall[i] 62 | return argsForCall.arg1 63 | } 64 | 65 | func (fake *FakeSchemaGenerator) GeneratePlanSchemaReturns(result1 serviceadapter.PlanSchema, result2 error) { 66 | fake.generatePlanSchemaMutex.Lock() 67 | defer fake.generatePlanSchemaMutex.Unlock() 68 | fake.GeneratePlanSchemaStub = nil 69 | fake.generatePlanSchemaReturns = struct { 70 | result1 serviceadapter.PlanSchema 71 | result2 error 72 | }{result1, result2} 73 | } 74 | 75 | func (fake *FakeSchemaGenerator) GeneratePlanSchemaReturnsOnCall(i int, result1 serviceadapter.PlanSchema, result2 error) { 76 | fake.generatePlanSchemaMutex.Lock() 77 | defer fake.generatePlanSchemaMutex.Unlock() 78 | fake.GeneratePlanSchemaStub = nil 79 | if fake.generatePlanSchemaReturnsOnCall == nil { 80 | fake.generatePlanSchemaReturnsOnCall = make(map[int]struct { 81 | result1 serviceadapter.PlanSchema 82 | result2 error 83 | }) 84 | } 85 | fake.generatePlanSchemaReturnsOnCall[i] = struct { 86 | result1 serviceadapter.PlanSchema 87 | result2 error 88 | }{result1, result2} 89 | } 90 | 91 | func (fake *FakeSchemaGenerator) Invocations() map[string][][]interface{} { 92 | fake.invocationsMutex.RLock() 93 | defer fake.invocationsMutex.RUnlock() 94 | fake.generatePlanSchemaMutex.RLock() 95 | defer fake.generatePlanSchemaMutex.RUnlock() 96 | copiedInvocations := map[string][][]interface{}{} 97 | for key, value := range fake.invocations { 98 | copiedInvocations[key] = value 99 | } 100 | return copiedInvocations 101 | } 102 | 103 | func (fake *FakeSchemaGenerator) recordInvocation(key string, args []interface{}) { 104 | fake.invocationsMutex.Lock() 105 | defer fake.invocationsMutex.Unlock() 106 | if fake.invocations == nil { 107 | fake.invocations = map[string][][]interface{}{} 108 | } 109 | if fake.invocations[key] == nil { 110 | fake.invocations[key] = [][]interface{}{} 111 | } 112 | fake.invocations[key] = append(fake.invocations[key], args) 113 | } 114 | 115 | var _ serviceadapter.SchemaGenerator = new(FakeSchemaGenerator) 116 | -------------------------------------------------------------------------------- /serviceadapter/fakes/manifest_generator.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 8 | ) 9 | 10 | type FakeManifestGenerator struct { 11 | GenerateManifestStub func(serviceadapter.GenerateManifestParams) (serviceadapter.GenerateManifestOutput, error) 12 | generateManifestMutex sync.RWMutex 13 | generateManifestArgsForCall []struct { 14 | arg1 serviceadapter.GenerateManifestParams 15 | } 16 | generateManifestReturns struct { 17 | result1 serviceadapter.GenerateManifestOutput 18 | result2 error 19 | } 20 | generateManifestReturnsOnCall map[int]struct { 21 | result1 serviceadapter.GenerateManifestOutput 22 | result2 error 23 | } 24 | invocations map[string][][]interface{} 25 | invocationsMutex sync.RWMutex 26 | } 27 | 28 | func (fake *FakeManifestGenerator) GenerateManifest(arg1 serviceadapter.GenerateManifestParams) (serviceadapter.GenerateManifestOutput, error) { 29 | fake.generateManifestMutex.Lock() 30 | ret, specificReturn := fake.generateManifestReturnsOnCall[len(fake.generateManifestArgsForCall)] 31 | fake.generateManifestArgsForCall = append(fake.generateManifestArgsForCall, struct { 32 | arg1 serviceadapter.GenerateManifestParams 33 | }{arg1}) 34 | fake.recordInvocation("GenerateManifest", []interface{}{arg1}) 35 | fake.generateManifestMutex.Unlock() 36 | if fake.GenerateManifestStub != nil { 37 | return fake.GenerateManifestStub(arg1) 38 | } 39 | if specificReturn { 40 | return ret.result1, ret.result2 41 | } 42 | fakeReturns := fake.generateManifestReturns 43 | return fakeReturns.result1, fakeReturns.result2 44 | } 45 | 46 | func (fake *FakeManifestGenerator) GenerateManifestCallCount() int { 47 | fake.generateManifestMutex.RLock() 48 | defer fake.generateManifestMutex.RUnlock() 49 | return len(fake.generateManifestArgsForCall) 50 | } 51 | 52 | func (fake *FakeManifestGenerator) GenerateManifestCalls(stub func(serviceadapter.GenerateManifestParams) (serviceadapter.GenerateManifestOutput, error)) { 53 | fake.generateManifestMutex.Lock() 54 | defer fake.generateManifestMutex.Unlock() 55 | fake.GenerateManifestStub = stub 56 | } 57 | 58 | func (fake *FakeManifestGenerator) GenerateManifestArgsForCall(i int) serviceadapter.GenerateManifestParams { 59 | fake.generateManifestMutex.RLock() 60 | defer fake.generateManifestMutex.RUnlock() 61 | argsForCall := fake.generateManifestArgsForCall[i] 62 | return argsForCall.arg1 63 | } 64 | 65 | func (fake *FakeManifestGenerator) GenerateManifestReturns(result1 serviceadapter.GenerateManifestOutput, result2 error) { 66 | fake.generateManifestMutex.Lock() 67 | defer fake.generateManifestMutex.Unlock() 68 | fake.GenerateManifestStub = nil 69 | fake.generateManifestReturns = struct { 70 | result1 serviceadapter.GenerateManifestOutput 71 | result2 error 72 | }{result1, result2} 73 | } 74 | 75 | func (fake *FakeManifestGenerator) GenerateManifestReturnsOnCall(i int, result1 serviceadapter.GenerateManifestOutput, result2 error) { 76 | fake.generateManifestMutex.Lock() 77 | defer fake.generateManifestMutex.Unlock() 78 | fake.GenerateManifestStub = nil 79 | if fake.generateManifestReturnsOnCall == nil { 80 | fake.generateManifestReturnsOnCall = make(map[int]struct { 81 | result1 serviceadapter.GenerateManifestOutput 82 | result2 error 83 | }) 84 | } 85 | fake.generateManifestReturnsOnCall[i] = struct { 86 | result1 serviceadapter.GenerateManifestOutput 87 | result2 error 88 | }{result1, result2} 89 | } 90 | 91 | func (fake *FakeManifestGenerator) Invocations() map[string][][]interface{} { 92 | fake.invocationsMutex.RLock() 93 | defer fake.invocationsMutex.RUnlock() 94 | fake.generateManifestMutex.RLock() 95 | defer fake.generateManifestMutex.RUnlock() 96 | copiedInvocations := map[string][][]interface{}{} 97 | for key, value := range fake.invocations { 98 | copiedInvocations[key] = value 99 | } 100 | return copiedInvocations 101 | } 102 | 103 | func (fake *FakeManifestGenerator) recordInvocation(key string, args []interface{}) { 104 | fake.invocationsMutex.Lock() 105 | defer fake.invocationsMutex.Unlock() 106 | if fake.invocations == nil { 107 | fake.invocations = map[string][][]interface{}{} 108 | } 109 | if fake.invocations[key] == nil { 110 | fake.invocations[key] = [][]interface{}{} 111 | } 112 | fake.invocations[key] = append(fake.invocations[key], args) 113 | } 114 | 115 | var _ serviceadapter.ManifestGenerator = new(FakeManifestGenerator) 116 | -------------------------------------------------------------------------------- /integration_tests/testharness/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package main 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 24 | "github.com/pivotal-cf/on-demand-services-sdk/integration_tests/testharness/testvariables" 25 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 26 | ) 27 | 28 | const OperationShouldFail = "true" 29 | 30 | func main() { 31 | if os.Getenv(testvariables.DoNotImplementInterfacesKey) == "true" { 32 | serviceadapter.HandleCLI(os.Args, serviceadapter.CommandLineHandler{}) 33 | return 34 | } 35 | 36 | handler := serviceadapter.CommandLineHandler{ 37 | ManifestGenerator: &manifestGenerator{}, 38 | Binder: &binder{}, 39 | DashboardURLGenerator: &dashboard{}, 40 | SchemaGenerator: &schemaGenerator{}, 41 | } 42 | 43 | serviceadapter.HandleCLI(os.Args, handler) 44 | } 45 | 46 | type manifestGenerator struct{} 47 | 48 | func (m *manifestGenerator) GenerateManifest(params serviceadapter.GenerateManifestParams) (serviceadapter.GenerateManifestOutput, error) { 49 | if os.Getenv(testvariables.OperationFailsKey) == OperationShouldFail { 50 | fmt.Fprintf(os.Stderr, "not valid") 51 | return serviceadapter.GenerateManifestOutput{}, errors.New("some message to the user") 52 | } 53 | 54 | manifest := bosh.BoshManifest{ 55 | Name: "deployment-name", 56 | Releases: []bosh.Release{ 57 | { 58 | Name: "a-release", 59 | Version: "latest", 60 | }, 61 | }, 62 | Stemcells: []bosh.Stemcell{ 63 | { 64 | Alias: "greatest", 65 | OS: "Windows", 66 | Version: "3.1", 67 | }, 68 | }, 69 | InstanceGroups: []bosh.InstanceGroup{ 70 | { 71 | Name: "Test", 72 | Properties: map[string]interface{}{ 73 | "parseSymbols": "yes%[===", 74 | }, 75 | }, 76 | }, 77 | } 78 | return serviceadapter.GenerateManifestOutput{ 79 | Manifest: manifest, 80 | }, nil 81 | } 82 | 83 | type binder struct{} 84 | 85 | func (b *binder) CreateBinding(params serviceadapter.CreateBindingParams) (serviceadapter.Binding, error) { 86 | errs := func(err error) (serviceadapter.Binding, error) { 87 | return serviceadapter.Binding{}, err 88 | } 89 | 90 | switch os.Getenv(testvariables.OperationFailsKey) { 91 | case testvariables.ErrAppGuidNotProvided: 92 | return errs(serviceadapter.NewAppGuidNotProvidedError(nil)) 93 | case testvariables.ErrBindingAlreadyExists: 94 | return errs(serviceadapter.NewBindingAlreadyExistsError(nil)) 95 | case OperationShouldFail: 96 | return errs(errors.New("An internal error occurred.")) 97 | } 98 | 99 | return testvariables.SuccessfulBinding, nil 100 | } 101 | 102 | func (b *binder) DeleteBinding(params serviceadapter.DeleteBindingParams) error { 103 | switch os.Getenv(testvariables.OperationFailsKey) { 104 | case testvariables.ErrBindingNotFound: 105 | return serviceadapter.NewBindingNotFoundError(errors.New("not found")) 106 | case OperationShouldFail: 107 | return errors.New("An error occurred") 108 | } 109 | 110 | return nil 111 | } 112 | 113 | type dashboard struct{} 114 | 115 | func (d *dashboard) DashboardUrl(params serviceadapter.DashboardUrlParams) (serviceadapter.DashboardUrl, error) { 116 | if os.Getenv(testvariables.OperationFailsKey) == OperationShouldFail { 117 | return serviceadapter.DashboardUrl{}, errors.New("An error occurred") 118 | } 119 | 120 | return serviceadapter.DashboardUrl{DashboardUrl: "http://dashboard.com"}, nil 121 | } 122 | 123 | type schemaGenerator struct{} 124 | 125 | func (s *schemaGenerator) GeneratePlanSchema(params serviceadapter.GeneratePlanSchemaParams) (serviceadapter.PlanSchema, error) { 126 | errs := func(err error) (serviceadapter.PlanSchema, error) { 127 | return serviceadapter.PlanSchema{}, err 128 | } 129 | 130 | if os.Getenv(testvariables.OperationFailsKey) == OperationShouldFail { 131 | return errs(errors.New("An error occurred")) 132 | } 133 | 134 | schemas := serviceadapter.JSONSchemas{ 135 | Parameters: map[string]interface{}{ 136 | "$schema": "http://json-schema.org/draft-04/schema#", 137 | "type": "object", 138 | "properties": map[string]interface{}{ 139 | "billing-account": map[string]interface{}{ 140 | "description": "Billing account number used to charge use of shared fake server.", 141 | "type": "string", 142 | }, 143 | }, 144 | }, 145 | } 146 | return serviceadapter.PlanSchema{ 147 | ServiceInstance: serviceadapter.ServiceInstanceSchema{ 148 | Create: schemas, 149 | Update: schemas, 150 | }, 151 | ServiceBinding: serviceadapter.ServiceBindingSchema{ 152 | Create: schemas, 153 | }, 154 | }, nil 155 | } 156 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in the 6 | On-Demand Service Broker project and our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at oss-coc@vmware.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /serviceadapter/instance_group_mapping_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package serviceadapter_test 17 | 18 | import ( 19 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 20 | . "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 21 | 22 | . "github.com/onsi/ginkgo/v2" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | var _ = Describe("Instance Groups Mapping", func() { 27 | var ( 28 | stemcell = "windows-ME" 29 | deploymentGroupsAndJobs = map[string][]string{ 30 | "real-instance-group": {"important-job", "extra-job"}, 31 | "another-instance-group": {"underrated-job"}, 32 | } 33 | 34 | instanceGroups []InstanceGroup 35 | serviceReleases ServiceReleases 36 | 37 | manifestInstanceGroups []bosh.InstanceGroup 38 | generateErr error 39 | ) 40 | 41 | BeforeEach(func() { 42 | instanceGroups = []InstanceGroup{ 43 | { 44 | Name: "real-instance-group", 45 | VMType: "a-vm", 46 | VMExtensions: []string{"what an extension"}, 47 | PersistentDiskType: "such-persistence", 48 | Instances: 7, 49 | Networks: []string{"an-etwork", "another-etwork"}, 50 | AZs: []string{"an-az", "jay-z"}, 51 | MigratedFrom: []Migration{{Name: "old-instance-group"}}, 52 | }, 53 | { 54 | Name: "another-instance-group", 55 | VMType: "another-vm", 56 | PersistentDiskType: "such-persistence", 57 | Instances: 7, 58 | Networks: []string{"another-etwork"}, 59 | AZs: []string{"another-az"}, 60 | }, 61 | } 62 | 63 | serviceReleases = ServiceReleases{ 64 | {Name: "real-release", Version: "4", Jobs: []string{"important-job"}}, 65 | {Name: "good-release", Version: "doesn't matter", Jobs: []string{"extra-job"}}, 66 | {Name: "service-backups", Version: "doesn't matter", Jobs: []string{"underrated-job"}}, 67 | } 68 | }) 69 | 70 | JustBeforeEach(func() { 71 | manifestInstanceGroups, generateErr = GenerateInstanceGroupsWithNoProperties(instanceGroups, serviceReleases, stemcell, deploymentGroupsAndJobs) 72 | }) 73 | 74 | Context("when each instance group and job is provided", func() { 75 | It("generates deployment instance groups", func() { 76 | Expect(manifestInstanceGroups).To(ConsistOf(bosh.InstanceGroup{ 77 | Name: "real-instance-group", 78 | Instances: 7, 79 | VMType: "a-vm", 80 | VMExtensions: []string{"what an extension"}, 81 | PersistentDiskType: "such-persistence", 82 | Networks: []bosh.Network{{Name: "an-etwork"}, {Name: "another-etwork"}}, 83 | AZs: []string{"an-az", "jay-z"}, 84 | Stemcell: stemcell, 85 | Jobs: []bosh.Job{ 86 | {Name: "important-job", Release: "real-release"}, 87 | {Name: "extra-job", Release: "good-release"}, 88 | }, 89 | MigratedFrom: []bosh.Migration{ 90 | {Name: "old-instance-group"}, 91 | }, 92 | }, 93 | bosh.InstanceGroup{ 94 | Name: "another-instance-group", 95 | Instances: 7, 96 | VMType: "another-vm", 97 | PersistentDiskType: "such-persistence", 98 | Networks: []bosh.Network{{Name: "another-etwork"}}, 99 | AZs: []string{"another-az"}, 100 | Stemcell: stemcell, 101 | Jobs: []bosh.Job{ 102 | {Name: "underrated-job", Release: "service-backups"}, 103 | }, 104 | }, 105 | )) 106 | }) 107 | 108 | It("returns no error", func() { 109 | Expect(generateErr).NotTo(HaveOccurred()) 110 | }) 111 | }) 112 | 113 | Context("when no instance groups are provided", func() { 114 | BeforeEach(func() { 115 | instanceGroups = nil 116 | }) 117 | 118 | It("returns an error", func() { 119 | Expect(generateErr).To(MatchError(MatchRegexp(`^no instance groups provided$`))) 120 | }) 121 | }) 122 | 123 | Context("when providing an instance group that's not expected", func() { 124 | BeforeEach(func() { 125 | instanceGroups = append(instanceGroups, InstanceGroup{Name: "i am not wanted"}) 126 | }) 127 | 128 | It("returns no error", func() { 129 | Expect(generateErr).NotTo(HaveOccurred()) 130 | }) 131 | 132 | It("does not include the unexpected instance group", func() { 133 | for _, manifestInstanceGroup := range manifestInstanceGroups { 134 | Expect(manifestInstanceGroup.Name).NotTo(Equal("i am not wanted")) 135 | } 136 | }) 137 | }) 138 | 139 | Context("when a job is expected but not provided", func() { 140 | BeforeEach(func() { 141 | serviceReleases[1].Jobs = nil 142 | }) 143 | 144 | It("returns an error", func() { 145 | Expect(generateErr).To(MatchError("job 'extra-job' not provided")) 146 | }) 147 | }) 148 | 149 | Context("when an expected job is provided twice", func() { 150 | BeforeEach(func() { 151 | serviceReleases = append(serviceReleases, ServiceRelease{Name: "doppelganger", Jobs: []string{"underrated-job"}}) 152 | }) 153 | 154 | It("returns an error", func() { 155 | Expect(generateErr).To(MatchError(ContainSubstring("job 'underrated-job' provided 2 times"))) 156 | }) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /serviceadapter/command_line_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package serviceadapter 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | "os" 22 | "path/filepath" 23 | "sort" 24 | "strings" 25 | ) 26 | 27 | // CommandLineHandler contains all of the implementers required for the service adapter interface 28 | type CommandLineHandler struct { 29 | ManifestGenerator ManifestGenerator 30 | Binder Binder 31 | DashboardURLGenerator DashboardUrlGenerator 32 | SchemaGenerator SchemaGenerator 33 | } 34 | 35 | type CLIHandlerError struct { 36 | ExitCode int 37 | Message string 38 | } 39 | 40 | func (e CLIHandlerError) Error() string { 41 | return e.Message 42 | } 43 | 44 | // Deprecated: Use HandleCLI method of a CommandLineHandler 45 | // 46 | // HandleCommandLineInvocation constructs a CommandLineHandler based on minimal 47 | // service adapter interface handlers and runs HandleCLI based on the 48 | // arguments provided 49 | func HandleCommandLineInvocation(args []string, manifestGenerator ManifestGenerator, binder Binder, dashboardUrlGenerator DashboardUrlGenerator) { 50 | handler := CommandLineHandler{ 51 | ManifestGenerator: manifestGenerator, 52 | Binder: binder, 53 | DashboardURLGenerator: dashboardUrlGenerator, 54 | } 55 | HandleCLI(args, handler) 56 | } 57 | 58 | // HandleCLI calls the correct Service Adapter handler method based on command 59 | // line arguments. The first argument at the command line should be one of: 60 | // generate-manifest, create-binding, delete-binding, dashboard-url. 61 | func HandleCLI(args []string, handler CommandLineHandler) { 62 | err := handler.Handle(args, os.Stdout, os.Stderr, os.Stdin) 63 | switch e := err.(type) { 64 | case nil: 65 | case CLIHandlerError: 66 | failWithCode(e.ExitCode, "%s", err.Error()) 67 | default: 68 | failWithCode(ErrorExitCode, "%s", err.Error()) 69 | } 70 | } 71 | 72 | // Handle executes required action and returns an error. Writes responses to the writer provided 73 | func (h CommandLineHandler) Handle(args []string, outputWriter, errorWriter io.Writer, inputParamsReader io.Reader) error { 74 | actions := map[string]Action{ 75 | "generate-manifest": NewGenerateManifestAction(h.ManifestGenerator), 76 | "create-binding": NewCreateBindingAction(h.Binder), 77 | "delete-binding": NewDeleteBindingAction(h.Binder), 78 | "dashboard-url": NewDashboardUrlAction(h.DashboardURLGenerator), 79 | "generate-plan-schemas": NewGeneratePlanSchemasAction(h.SchemaGenerator, errorWriter), 80 | } 81 | supportedCommands := h.generateSupportedCommandsMessage(actions) 82 | 83 | if len(args) < 2 { 84 | return CLIHandlerError{ 85 | ErrorExitCode, 86 | fmt.Sprintf("the following commands are supported: %s", supportedCommands), 87 | } 88 | } 89 | 90 | action, arguments := args[1], args[2:] 91 | fmt.Fprintf(errorWriter, "[odb-sdk] handling %s\n", action) 92 | 93 | var inputParams InputParams 94 | 95 | var err error 96 | ac, ok := actions[action] 97 | if !ok { 98 | failWithCode(ErrorExitCode, "unknown subcommand: %s. The following commands are supported: %s", args[1], supportedCommands) 99 | return nil 100 | } 101 | 102 | if !ac.IsImplemented() { 103 | return CLIHandlerError{NotImplementedExitCode, fmt.Sprintf("%s not implemented", action)} 104 | } 105 | 106 | if inputParams, err = ac.ParseArgs(inputParamsReader, arguments); err != nil { 107 | switch e := err.(type) { 108 | case MissingArgsError: 109 | return missingArgsError(args, e.Error()) 110 | default: 111 | return e 112 | } 113 | } 114 | return ac.Execute(inputParams, outputWriter) 115 | } 116 | 117 | func failWithMissingArgsError(args []string, argumentNames string) { 118 | failWithCode( 119 | ErrorExitCode, 120 | "Missing arguments for %s. Usage: %s %s %s", 121 | args[1], 122 | filepath.Base(args[0]), 123 | args[1], 124 | argumentNames, 125 | ) 126 | } 127 | 128 | func incorrectArgsError(cmd string) error { 129 | return CLIHandlerError{ 130 | ErrorExitCode, 131 | fmt.Sprintf("Incorrect arguments for %s", cmd), 132 | } 133 | } 134 | 135 | func missingArgsError(args []string, argumentNames string) error { 136 | return CLIHandlerError{ 137 | ExitCode: ErrorExitCode, 138 | Message: fmt.Sprintf( 139 | "Missing arguments for %s. Usage: %s %s %s", 140 | args[1], 141 | filepath.Base(args[0]), 142 | args[1], 143 | argumentNames, 144 | ), 145 | } 146 | } 147 | 148 | func (h CommandLineHandler) generateSupportedCommandsMessage(actions map[string]Action) string { 149 | commands := []string{} 150 | for key, action := range actions { 151 | if action.IsImplemented() { 152 | commands = append(commands, key) 153 | } 154 | } 155 | 156 | sort.Strings(commands) 157 | return strings.Join(commands, ", ") 158 | } 159 | 160 | func (h CommandLineHandler) must(err error, msg string) { 161 | if err != nil { 162 | fail("error %s: %s\n", msg, err) 163 | } 164 | } 165 | 166 | func (h CommandLineHandler) mustNot(err error, msg string) { 167 | h.must(err, msg) 168 | } 169 | 170 | func fail(format string, params ...interface{}) { 171 | failWithCode(ErrorExitCode, format, params...) 172 | } 173 | 174 | func failWithCode(code int, format string, params ...interface{}) { 175 | message := fmt.Sprintf(format, params...) 176 | fmt.Fprintf(os.Stderr, "[odb-sdk] %s\n", message) 177 | os.Exit(code) 178 | } 179 | 180 | func failWithCodeAndNotifyUser(code int, format string) { 181 | fmt.Fprint(os.Stdout, format) 182 | os.Exit(code) 183 | } 184 | -------------------------------------------------------------------------------- /serviceadapter/dashboard_url_test.go: -------------------------------------------------------------------------------- 1 | package serviceadapter_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gbytes" 10 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 11 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 12 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter/fakes" 13 | ) 14 | 15 | var _ = Describe("DashboardUrl", func() { 16 | var ( 17 | fakeDashboardUrlGenerator *fakes.FakeDashboardUrlGenerator 18 | instanceId string 19 | plan serviceadapter.Plan 20 | manifest bosh.BoshManifest 21 | 22 | expectedInputParams serviceadapter.InputParams 23 | action *serviceadapter.DashboardUrlAction 24 | outputBuffer *gbytes.Buffer 25 | ) 26 | 27 | BeforeEach(func() { 28 | fakeDashboardUrlGenerator = new(fakes.FakeDashboardUrlGenerator) 29 | instanceId = "my-instance-identifier" 30 | plan = defaultPlan() 31 | manifest = defaultManifest() 32 | outputBuffer = gbytes.NewBuffer() 33 | 34 | expectedInputParams = serviceadapter.InputParams{ 35 | DashboardUrl: serviceadapter.DashboardUrlJSONParams{ 36 | InstanceId: instanceId, 37 | Plan: toJson(plan), 38 | Manifest: toYaml(manifest), 39 | }, 40 | } 41 | 42 | action = serviceadapter.NewDashboardUrlAction(fakeDashboardUrlGenerator) 43 | }) 44 | 45 | Describe("IsImplemented", func() { 46 | It("returns true if implemented", func() { 47 | Expect(action.IsImplemented()).To(BeTrue()) 48 | }) 49 | 50 | It("returns false if not implemented", func() { 51 | c := serviceadapter.NewDashboardUrlAction(nil) 52 | Expect(c.IsImplemented()).To(BeFalse()) 53 | }) 54 | }) 55 | 56 | Describe("ParseArgs", func() { 57 | When("giving arguments in stdin", func() { 58 | It("can parse arguments from stdin", func() { 59 | input := bytes.NewBuffer([]byte(toJson(expectedInputParams))) 60 | actualInputParams, err := action.ParseArgs(input, []string{}) 61 | 62 | Expect(err).NotTo(HaveOccurred()) 63 | Expect(actualInputParams).To(Equal(expectedInputParams)) 64 | }) 65 | 66 | It("returns an error when cannot read from input buffer", func() { 67 | fakeReader := new(FakeReader) 68 | _, err := action.ParseArgs(fakeReader, []string{}) 69 | 70 | Expect(err).To(BeACLIError(1, "error reading input params JSON")) 71 | }) 72 | 73 | It("returns an error when cannot unmarshal from input buffer", func() { 74 | input := bytes.NewBuffer([]byte("not-valid-json")) 75 | _, err := action.ParseArgs(input, []string{}) 76 | 77 | Expect(err).To(BeACLIError(1, "error unmarshalling input params JSON")) 78 | }) 79 | 80 | It("returns an error when input buffer is empty", func() { 81 | input := bytes.NewBuffer([]byte{}) 82 | _, err := action.ParseArgs(input, []string{}) 83 | 84 | Expect(err).To(BeACLIError(1, "expecting parameters to be passed via stdin")) 85 | }) 86 | }) 87 | 88 | When("given positional arguments", func() { 89 | It("can parse positional arguments", func() { 90 | positionalArgs := []string{ 91 | expectedInputParams.DashboardUrl.InstanceId, 92 | expectedInputParams.DashboardUrl.Plan, 93 | expectedInputParams.DashboardUrl.Manifest, 94 | } 95 | 96 | actualInputParams, err := action.ParseArgs(nil, positionalArgs) 97 | Expect(err).NotTo(HaveOccurred()) 98 | Expect(actualInputParams).To(Equal(expectedInputParams)) 99 | }) 100 | 101 | It("returns an error when required arguments are not passed in", func() { 102 | _, err := action.ParseArgs(nil, []string{"foo"}) 103 | Expect(err).To(HaveOccurred()) 104 | Expect(err).To(BeAssignableToTypeOf(serviceadapter.MissingArgsError{})) 105 | Expect(err).To(MatchError(ContainSubstring(" "))) 106 | }) 107 | }) 108 | }) 109 | 110 | Describe("Execute", func() { 111 | It("calls the supplied handler passing args through", func() { 112 | fakeDashboardUrlGenerator.DashboardUrlReturns(serviceadapter.DashboardUrl{DashboardUrl: "gopher://foo"}, nil) 113 | 114 | err := action.Execute(expectedInputParams, outputBuffer) 115 | 116 | Expect(err).NotTo(HaveOccurred()) 117 | 118 | Expect(fakeDashboardUrlGenerator.DashboardUrlCallCount()).To(Equal(1)) 119 | actualParams := fakeDashboardUrlGenerator.DashboardUrlArgsForCall(0) 120 | 121 | Expect(actualParams.InstanceID).To(Equal(instanceId)) 122 | Expect(actualParams.Plan).To(Equal(plan)) 123 | Expect(actualParams.Manifest).To(Equal(manifest)) 124 | 125 | Expect(outputBuffer).To(gbytes.Say(`{"dashboard_url":"gopher://foo"}`)) 126 | }) 127 | 128 | Context("error handling", func() { 129 | It("returns an error when plan cannot be unmarshalled", func() { 130 | expectedInputParams.DashboardUrl.Plan = "not-json" 131 | err := action.Execute(expectedInputParams, outputBuffer) 132 | Expect(err).To(MatchError(ContainSubstring("unmarshalling service plan"))) 133 | }) 134 | 135 | It("returns an error when manifest cannot be unmarshalled", func() { 136 | expectedInputParams.DashboardUrl.Manifest = "not-yaml" 137 | err := action.Execute(expectedInputParams, outputBuffer) 138 | Expect(err).To(MatchError(ContainSubstring("unmarshalling manifest YAML"))) 139 | }) 140 | 141 | It("returns an error when plan is invalid", func() { 142 | expectedInputParams.DashboardUrl.Plan = "{}" 143 | err := action.Execute(expectedInputParams, outputBuffer) 144 | Expect(err).To(MatchError(ContainSubstring("validating service plan"))) 145 | }) 146 | 147 | It("returns an error when dashboardUrlGenerator returns an error", func() { 148 | fakeDashboardUrlGenerator.DashboardUrlReturns(serviceadapter.DashboardUrl{}, errors.New("something went wrong")) 149 | err := action.Execute(expectedInputParams, outputBuffer) 150 | Expect(err).To(BeACLIError(1, "something went wrong")) 151 | }) 152 | 153 | It("returns an error when dashboardUrlGenerator returns an unmarshalable struct", func() { 154 | fakeWriter := new(FakeWriter) 155 | fakeDashboardUrlGenerator.DashboardUrlReturns(serviceadapter.DashboardUrl{}, nil) 156 | 157 | err := action.Execute(expectedInputParams, fakeWriter) 158 | Expect(err).To(MatchError(ContainSubstring("marshalling dashboardUrl"))) 159 | }) 160 | }) 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /serviceadapter/generate_plan_schemas_test.go: -------------------------------------------------------------------------------- 1 | package serviceadapter_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 12 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter/fakes" 13 | ) 14 | 15 | var _ = Describe("GeneratePlanSchemas", func() { 16 | var ( 17 | fakeSchemaGenerator *fakes.FakeSchemaGenerator 18 | plan serviceadapter.Plan 19 | expectedInputParams serviceadapter.InputParams 20 | action *serviceadapter.GeneratePlanSchemasAction 21 | outputBuffer *gbytes.Buffer 22 | errorBuffer *gbytes.Buffer 23 | ) 24 | 25 | BeforeEach(func() { 26 | fakeSchemaGenerator = new(fakes.FakeSchemaGenerator) 27 | plan = defaultPlan() 28 | outputBuffer = gbytes.NewBuffer() 29 | errorBuffer = gbytes.NewBuffer() 30 | 31 | expectedInputParams = serviceadapter.InputParams{ 32 | GeneratePlanSchemas: serviceadapter.GeneratePlanSchemasJSONParams{ 33 | Plan: toJson(plan), 34 | }, 35 | } 36 | 37 | action = serviceadapter.NewGeneratePlanSchemasAction(fakeSchemaGenerator, errorBuffer) 38 | }) 39 | 40 | Describe("IsImplemented", func() { 41 | It("returns true if implemented", func() { 42 | g := serviceadapter.NewGeneratePlanSchemasAction(fakeSchemaGenerator, errorBuffer) 43 | Expect(g.IsImplemented()).To(BeTrue()) 44 | }) 45 | 46 | It("returns false if not implemented", func() { 47 | g := serviceadapter.NewGeneratePlanSchemasAction(nil, errorBuffer) 48 | Expect(g.IsImplemented()).To(BeFalse()) 49 | }) 50 | }) 51 | 52 | Describe("ParseArgs", func() { 53 | When("giving arguments in stdin", func() { 54 | It("can parse arguments from stdin", func() { 55 | input := bytes.NewBuffer([]byte(toJson(expectedInputParams))) 56 | actualInputParams, err := action.ParseArgs(input, []string{}) 57 | 58 | Expect(err).NotTo(HaveOccurred()) 59 | Expect(actualInputParams).To(Equal(expectedInputParams)) 60 | }) 61 | 62 | It("returns an error when cannot read from input buffer", func() { 63 | fakeReader := new(FakeReader) 64 | _, err := action.ParseArgs(fakeReader, []string{}) 65 | Expect(err).To(BeACLIError(1, "error reading input params JSON")) 66 | }) 67 | 68 | It("returns an error when cannot unmarshal from input buffer", func() { 69 | input := bytes.NewBuffer([]byte("not-valid-json")) 70 | _, err := action.ParseArgs(input, []string{}) 71 | Expect(err).To(BeACLIError(1, "error unmarshalling input params JSON")) 72 | }) 73 | 74 | It("returns an error when input buffer is empty", func() { 75 | input := bytes.NewBuffer([]byte{}) 76 | _, err := action.ParseArgs(input, []string{}) 77 | Expect(err).To(BeACLIError(1, "expecting parameters to be passed via stdin")) 78 | }) 79 | }) 80 | 81 | When("given positional arguments", func() { 82 | It("can parse positional arguments", func() { 83 | positionalArgs := []string{ 84 | "-plan-json", 85 | expectedInputParams.GeneratePlanSchemas.Plan, 86 | } 87 | 88 | actualInputParams, err := action.ParseArgs(nil, positionalArgs) 89 | Expect(err).NotTo(HaveOccurred()) 90 | expectedInputParams.TextOutput = true 91 | Expect(actualInputParams).To(Equal(expectedInputParams)) 92 | }) 93 | 94 | It("returns an error when required arguments are not passed in", func() { 95 | _, err := action.ParseArgs(nil, []string{"-plan-json", ""}) 96 | Expect(err).To(HaveOccurred()) 97 | Expect(err).To(BeAssignableToTypeOf(serviceadapter.MissingArgsError{})) 98 | Expect(err).To(MatchError(ContainSubstring(""))) 99 | }) 100 | 101 | It("returns an error when unrecognised arguments are passed in", func() { 102 | _, err := action.ParseArgs(nil, []string{"-what"}) 103 | Expect(err).To(HaveOccurred()) 104 | Expect(err).To(MatchError(ContainSubstring("flag provided but not defined: -what"))) 105 | }) 106 | }) 107 | }) 108 | 109 | Describe("Execute", func() { 110 | It("calls the supplied handler passing args through", func() { 111 | planSchema := serviceadapter.PlanSchema{ 112 | ServiceInstance: serviceadapter.ServiceInstanceSchema{ 113 | Create: serviceadapter.JSONSchemas{ 114 | Parameters: map[string]interface{}{ 115 | "foo": "string", 116 | }, 117 | }, 118 | }, 119 | } 120 | fakeSchemaGenerator.GeneratePlanSchemaReturns(planSchema, nil) 121 | 122 | err := action.Execute(expectedInputParams, outputBuffer) 123 | Expect(err).NotTo(HaveOccurred()) 124 | 125 | Expect(fakeSchemaGenerator.GeneratePlanSchemaCallCount()).To(Equal(1)) 126 | actualParams := fakeSchemaGenerator.GeneratePlanSchemaArgsForCall(0) 127 | 128 | Expect(actualParams.Plan).To(Equal(plan)) 129 | var planSchemasOutput serviceadapter.PlanSchema 130 | Expect(json.Unmarshal(outputBuffer.Contents(), &planSchemasOutput)).To(Succeed()) 131 | Expect(planSchemasOutput).To(Equal(planSchema)) 132 | }) 133 | 134 | Context("error handling", func() { 135 | It("returns an error when plan cannot be unmarshalled", func() { 136 | expectedInputParams.GeneratePlanSchemas.Plan = "not-json" 137 | err := action.Execute(expectedInputParams, outputBuffer) 138 | Expect(err).To(MatchError(ContainSubstring("unmarshalling plan JSON"))) 139 | }) 140 | 141 | It("returns an error when plan is invalid", func() { 142 | expectedInputParams.GeneratePlanSchemas.Plan = "{}" 143 | err := action.Execute(expectedInputParams, outputBuffer) 144 | Expect(err).To(MatchError(ContainSubstring("validating plan JSON"))) 145 | }) 146 | 147 | It("returns an error when schemaGenerator returns an error", func() { 148 | fakeSchemaGenerator.GeneratePlanSchemaReturns(serviceadapter.PlanSchema{}, errors.New("something went wrong")) 149 | err := action.Execute(expectedInputParams, outputBuffer) 150 | Expect(err).To(BeACLIError(1, "something went wrong")) 151 | }) 152 | 153 | It("returns an error when the returned object cannot be unmarshalled", func() { 154 | fakeWriter := new(FakeWriter) 155 | fakeSchemaGenerator.GeneratePlanSchemaReturns(serviceadapter.PlanSchema{}, nil) 156 | 157 | err := action.Execute(expectedInputParams, fakeWriter) 158 | Expect(err).To(MatchError(ContainSubstring("marshalling plan schema"))) 159 | }) 160 | }) 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /serviceadapter/fakes/binder.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 8 | ) 9 | 10 | type FakeBinder struct { 11 | CreateBindingStub func(serviceadapter.CreateBindingParams) (serviceadapter.Binding, error) 12 | createBindingMutex sync.RWMutex 13 | createBindingArgsForCall []struct { 14 | arg1 serviceadapter.CreateBindingParams 15 | } 16 | createBindingReturns struct { 17 | result1 serviceadapter.Binding 18 | result2 error 19 | } 20 | createBindingReturnsOnCall map[int]struct { 21 | result1 serviceadapter.Binding 22 | result2 error 23 | } 24 | DeleteBindingStub func(serviceadapter.DeleteBindingParams) error 25 | deleteBindingMutex sync.RWMutex 26 | deleteBindingArgsForCall []struct { 27 | arg1 serviceadapter.DeleteBindingParams 28 | } 29 | deleteBindingReturns struct { 30 | result1 error 31 | } 32 | deleteBindingReturnsOnCall map[int]struct { 33 | result1 error 34 | } 35 | invocations map[string][][]interface{} 36 | invocationsMutex sync.RWMutex 37 | } 38 | 39 | func (fake *FakeBinder) CreateBinding(arg1 serviceadapter.CreateBindingParams) (serviceadapter.Binding, error) { 40 | fake.createBindingMutex.Lock() 41 | ret, specificReturn := fake.createBindingReturnsOnCall[len(fake.createBindingArgsForCall)] 42 | fake.createBindingArgsForCall = append(fake.createBindingArgsForCall, struct { 43 | arg1 serviceadapter.CreateBindingParams 44 | }{arg1}) 45 | fake.recordInvocation("CreateBinding", []interface{}{arg1}) 46 | fake.createBindingMutex.Unlock() 47 | if fake.CreateBindingStub != nil { 48 | return fake.CreateBindingStub(arg1) 49 | } 50 | if specificReturn { 51 | return ret.result1, ret.result2 52 | } 53 | fakeReturns := fake.createBindingReturns 54 | return fakeReturns.result1, fakeReturns.result2 55 | } 56 | 57 | func (fake *FakeBinder) CreateBindingCallCount() int { 58 | fake.createBindingMutex.RLock() 59 | defer fake.createBindingMutex.RUnlock() 60 | return len(fake.createBindingArgsForCall) 61 | } 62 | 63 | func (fake *FakeBinder) CreateBindingCalls(stub func(serviceadapter.CreateBindingParams) (serviceadapter.Binding, error)) { 64 | fake.createBindingMutex.Lock() 65 | defer fake.createBindingMutex.Unlock() 66 | fake.CreateBindingStub = stub 67 | } 68 | 69 | func (fake *FakeBinder) CreateBindingArgsForCall(i int) serviceadapter.CreateBindingParams { 70 | fake.createBindingMutex.RLock() 71 | defer fake.createBindingMutex.RUnlock() 72 | argsForCall := fake.createBindingArgsForCall[i] 73 | return argsForCall.arg1 74 | } 75 | 76 | func (fake *FakeBinder) CreateBindingReturns(result1 serviceadapter.Binding, result2 error) { 77 | fake.createBindingMutex.Lock() 78 | defer fake.createBindingMutex.Unlock() 79 | fake.CreateBindingStub = nil 80 | fake.createBindingReturns = struct { 81 | result1 serviceadapter.Binding 82 | result2 error 83 | }{result1, result2} 84 | } 85 | 86 | func (fake *FakeBinder) CreateBindingReturnsOnCall(i int, result1 serviceadapter.Binding, result2 error) { 87 | fake.createBindingMutex.Lock() 88 | defer fake.createBindingMutex.Unlock() 89 | fake.CreateBindingStub = nil 90 | if fake.createBindingReturnsOnCall == nil { 91 | fake.createBindingReturnsOnCall = make(map[int]struct { 92 | result1 serviceadapter.Binding 93 | result2 error 94 | }) 95 | } 96 | fake.createBindingReturnsOnCall[i] = struct { 97 | result1 serviceadapter.Binding 98 | result2 error 99 | }{result1, result2} 100 | } 101 | 102 | func (fake *FakeBinder) DeleteBinding(arg1 serviceadapter.DeleteBindingParams) error { 103 | fake.deleteBindingMutex.Lock() 104 | ret, specificReturn := fake.deleteBindingReturnsOnCall[len(fake.deleteBindingArgsForCall)] 105 | fake.deleteBindingArgsForCall = append(fake.deleteBindingArgsForCall, struct { 106 | arg1 serviceadapter.DeleteBindingParams 107 | }{arg1}) 108 | fake.recordInvocation("DeleteBinding", []interface{}{arg1}) 109 | fake.deleteBindingMutex.Unlock() 110 | if fake.DeleteBindingStub != nil { 111 | return fake.DeleteBindingStub(arg1) 112 | } 113 | if specificReturn { 114 | return ret.result1 115 | } 116 | fakeReturns := fake.deleteBindingReturns 117 | return fakeReturns.result1 118 | } 119 | 120 | func (fake *FakeBinder) DeleteBindingCallCount() int { 121 | fake.deleteBindingMutex.RLock() 122 | defer fake.deleteBindingMutex.RUnlock() 123 | return len(fake.deleteBindingArgsForCall) 124 | } 125 | 126 | func (fake *FakeBinder) DeleteBindingCalls(stub func(serviceadapter.DeleteBindingParams) error) { 127 | fake.deleteBindingMutex.Lock() 128 | defer fake.deleteBindingMutex.Unlock() 129 | fake.DeleteBindingStub = stub 130 | } 131 | 132 | func (fake *FakeBinder) DeleteBindingArgsForCall(i int) serviceadapter.DeleteBindingParams { 133 | fake.deleteBindingMutex.RLock() 134 | defer fake.deleteBindingMutex.RUnlock() 135 | argsForCall := fake.deleteBindingArgsForCall[i] 136 | return argsForCall.arg1 137 | } 138 | 139 | func (fake *FakeBinder) DeleteBindingReturns(result1 error) { 140 | fake.deleteBindingMutex.Lock() 141 | defer fake.deleteBindingMutex.Unlock() 142 | fake.DeleteBindingStub = nil 143 | fake.deleteBindingReturns = struct { 144 | result1 error 145 | }{result1} 146 | } 147 | 148 | func (fake *FakeBinder) DeleteBindingReturnsOnCall(i int, result1 error) { 149 | fake.deleteBindingMutex.Lock() 150 | defer fake.deleteBindingMutex.Unlock() 151 | fake.DeleteBindingStub = nil 152 | if fake.deleteBindingReturnsOnCall == nil { 153 | fake.deleteBindingReturnsOnCall = make(map[int]struct { 154 | result1 error 155 | }) 156 | } 157 | fake.deleteBindingReturnsOnCall[i] = struct { 158 | result1 error 159 | }{result1} 160 | } 161 | 162 | func (fake *FakeBinder) Invocations() map[string][][]interface{} { 163 | fake.invocationsMutex.RLock() 164 | defer fake.invocationsMutex.RUnlock() 165 | fake.createBindingMutex.RLock() 166 | defer fake.createBindingMutex.RUnlock() 167 | fake.deleteBindingMutex.RLock() 168 | defer fake.deleteBindingMutex.RUnlock() 169 | copiedInvocations := map[string][][]interface{}{} 170 | for key, value := range fake.invocations { 171 | copiedInvocations[key] = value 172 | } 173 | return copiedInvocations 174 | } 175 | 176 | func (fake *FakeBinder) recordInvocation(key string, args []interface{}) { 177 | fake.invocationsMutex.Lock() 178 | defer fake.invocationsMutex.Unlock() 179 | if fake.invocations == nil { 180 | fake.invocations = map[string][][]interface{}{} 181 | } 182 | if fake.invocations[key] == nil { 183 | fake.invocations[key] = [][]interface{}{} 184 | } 185 | fake.invocations[key] = append(fake.invocations[key], args) 186 | } 187 | 188 | var _ serviceadapter.Binder = new(FakeBinder) 189 | -------------------------------------------------------------------------------- /serviceadapter/generate_manifest.go: -------------------------------------------------------------------------------- 1 | package serviceadapter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/pkg/errors" 9 | "gopkg.in/yaml.v2" 10 | 11 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 12 | ) 13 | 14 | type GenerateManifestAction struct { 15 | manifestGenerator ManifestGenerator 16 | } 17 | 18 | func NewGenerateManifestAction(manifestGenerator ManifestGenerator) *GenerateManifestAction { 19 | return &GenerateManifestAction{ 20 | manifestGenerator: manifestGenerator, 21 | } 22 | } 23 | 24 | func (g *GenerateManifestAction) IsImplemented() bool { 25 | return g.manifestGenerator != nil 26 | } 27 | 28 | func (g *GenerateManifestAction) ParseArgs(reader io.Reader, args []string) (InputParams, error) { 29 | var inputParams InputParams 30 | 31 | if len(args) > 0 { // Legacy positional arguments 32 | if len(args) < 5 { 33 | return inputParams, NewMissingArgsError(" ") 34 | } 35 | 36 | inputParams = InputParams{ 37 | GenerateManifest: GenerateManifestJSONParams{ 38 | ServiceDeployment: args[0], 39 | Plan: args[1], 40 | RequestParameters: args[2], 41 | PreviousManifest: args[3], 42 | PreviousPlan: args[4], 43 | }, 44 | TextOutput: true, 45 | } 46 | return inputParams, nil 47 | } 48 | 49 | data, err := io.ReadAll(reader) 50 | if err != nil { 51 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error reading input params JSON, error: %s", err)} 52 | } 53 | if len(data) <= 0 { 54 | return inputParams, CLIHandlerError{ErrorExitCode, "expecting parameters to be passed via stdin"} 55 | } 56 | 57 | err = json.Unmarshal(data, &inputParams) 58 | if err != nil { 59 | return inputParams, CLIHandlerError{ErrorExitCode, fmt.Sprintf("error unmarshalling input params JSON, error: %s", err)} 60 | } 61 | 62 | return inputParams, nil 63 | } 64 | 65 | type ServiceInstanceUAAClient struct { 66 | Authorities string `json:"authorities"` 67 | AuthorizedGrantTypes string `json:"authorized_grant_types"` 68 | ClientID string `json:"client_id"` 69 | ClientSecret string `json:"client_secret"` 70 | Name string `json:"name"` 71 | ResourceIDs string `json:"resource_ids"` 72 | Scopes string `json:"scopes"` 73 | } 74 | 75 | func (g *GenerateManifestAction) Execute(inputParams InputParams, outputWriter io.Writer) (err error) { 76 | var serviceDeployment ServiceDeployment 77 | generateManifestParams := inputParams.GenerateManifest 78 | 79 | if err = json.Unmarshal([]byte(generateManifestParams.ServiceDeployment), &serviceDeployment); err != nil { 80 | return errors.Wrap(err, "unmarshalling service deployment") 81 | } 82 | if err = serviceDeployment.Validate(); err != nil { 83 | return errors.Wrap(err, "validating service deployment") 84 | } 85 | 86 | var plan Plan 87 | if err = json.Unmarshal([]byte(generateManifestParams.Plan), &plan); err != nil { 88 | return errors.Wrap(err, "unmarshalling service plan") 89 | } 90 | if err = plan.Validate(); err != nil { 91 | return errors.Wrap(err, "validating service plan") 92 | } 93 | 94 | var requestParams map[string]interface{} 95 | if err = json.Unmarshal([]byte(generateManifestParams.RequestParameters), &requestParams); err != nil { 96 | return errors.Wrap(err, "unmarshalling requestParams") 97 | } 98 | 99 | var previousManifest *bosh.BoshManifest 100 | if err = yaml.Unmarshal([]byte(generateManifestParams.PreviousManifest), &previousManifest); err != nil { 101 | return errors.Wrap(err, "unmarshalling previous manifest") 102 | } 103 | 104 | var previousPlan *Plan 105 | if err = json.Unmarshal([]byte(generateManifestParams.PreviousPlan), &previousPlan); err != nil { 106 | return errors.Wrap(err, "unmarshalling previous service plan") 107 | } 108 | if previousPlan != nil { 109 | if err = previousPlan.Validate(); err != nil { 110 | return errors.Wrap(err, "validating previous service plan") 111 | } 112 | } 113 | 114 | previousSecrets := ManifestSecrets{} 115 | if generateManifestParams.PreviousSecrets != "" { 116 | if err = json.Unmarshal([]byte(generateManifestParams.PreviousSecrets), &previousSecrets); err != nil { 117 | return errors.Wrap(err, "unmarshalling previous secrets") 118 | } 119 | } 120 | 121 | var previousConfigs BOSHConfigs 122 | if generateManifestParams.PreviousConfigs != "" { 123 | if err = json.Unmarshal([]byte(generateManifestParams.PreviousConfigs), &previousConfigs); err != nil { 124 | return errors.Wrap(err, "unmarshalling previous configs") 125 | } 126 | } 127 | 128 | var serviceInstanceClient *ServiceInstanceUAAClient 129 | if generateManifestParams.ServiceInstanceUAAClient != "" { 130 | if err = json.Unmarshal([]byte(generateManifestParams.ServiceInstanceUAAClient), &serviceInstanceClient); err != nil { 131 | return errors.Wrap(err, "unmarshalling service instance client") 132 | } 133 | } 134 | 135 | generateManifestOutput, err := g.manifestGenerator.GenerateManifest(GenerateManifestParams{ 136 | ServiceDeployment: serviceDeployment, 137 | Plan: plan, 138 | RequestParams: requestParams, 139 | PreviousManifest: previousManifest, 140 | PreviousPlan: previousPlan, 141 | PreviousSecrets: previousSecrets, 142 | PreviousConfigs: previousConfigs, 143 | ServiceInstanceUAAClient: serviceInstanceClient, 144 | }) 145 | if err != nil { 146 | fmt.Fprint(outputWriter, err.Error()) 147 | return CLIHandlerError{ErrorExitCode, err.Error()} 148 | } 149 | 150 | var output []byte 151 | if inputParams.TextOutput { 152 | defer handleErr(&err) 153 | manifestBytes, err := yaml.Marshal(generateManifestOutput.Manifest) 154 | if err != nil { 155 | return errors.Wrap(err, "error marshalling bosh manifest") 156 | } 157 | output = manifestBytes 158 | } else { 159 | defer handleErr(&err) 160 | manifestBytes, err := yaml.Marshal(generateManifestOutput.Manifest) 161 | if err != nil { 162 | return errors.Wrap(err, "error marshalling manifest yaml output") 163 | } 164 | marshalledOutput := MarshalledGenerateManifest{ 165 | Manifest: string(manifestBytes), 166 | ODBManagedSecrets: generateManifestOutput.ODBManagedSecrets, 167 | Configs: generateManifestOutput.Configs, 168 | Labels: generateManifestOutput.Labels, 169 | } 170 | output, err = json.Marshal(marshalledOutput) 171 | if err != nil { 172 | return errors.Wrap(err, "error marshalling generate-manifest json output") 173 | } 174 | } 175 | 176 | fmt.Fprint(outputWriter, string(output)) 177 | return nil 178 | } 179 | 180 | func handleErr(err *error) { 181 | if v := recover(); v != nil { 182 | *err = errors.New("error marshalling bosh manifest") 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /serviceadapter/serviceadapter_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package serviceadapter_test 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "strings" 24 | "testing" 25 | 26 | . "github.com/onsi/ginkgo/v2" 27 | . "github.com/onsi/gomega" 28 | "github.com/onsi/gomega/types" 29 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 30 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 31 | 32 | "gopkg.in/yaml.v2" 33 | ) 34 | 35 | func TestServiceadapter(t *testing.T) { 36 | RegisterFailHandler(Fail) 37 | RunSpecs(t, "Service adapter Suite") 38 | } 39 | 40 | func toJson(obj interface{}) string { 41 | str, err := json.Marshal(obj) 42 | Expect(err).NotTo(HaveOccurred()) 43 | return string(str) 44 | } 45 | 46 | func toYaml(obj interface{}) string { 47 | str, err := yaml.Marshal(obj) 48 | Expect(err).NotTo(HaveOccurred()) 49 | return string(str) 50 | } 51 | 52 | func defaultServiceDeployment() serviceadapter.ServiceDeployment { 53 | return serviceadapter.ServiceDeployment{ 54 | DeploymentName: "service-instance-deployment", 55 | Releases: serviceadapter.ServiceReleases{ 56 | { 57 | Name: "release-name", 58 | Version: "release-version", 59 | Jobs: []string{"job_one", "job_two"}, 60 | }, 61 | }, 62 | Stemcells: []serviceadapter.Stemcell{{ 63 | OS: "BeOS", 64 | Version: "2", 65 | }}, 66 | } 67 | } 68 | 69 | func defaultRequestParams() serviceadapter.RequestParameters { 70 | return serviceadapter.RequestParameters{"key": "foo", "bar": "baz"} 71 | } 72 | 73 | func defaultSecretParams() serviceadapter.ManifestSecrets { 74 | return serviceadapter.ManifestSecrets{"((/a/secret/path))": "some r34||y s3cr3t v41", "((another))": "one"} 75 | } 76 | 77 | func defaultDNSParams() serviceadapter.DNSAddresses { 78 | return serviceadapter.DNSAddresses{"foo": "a.b.c", "bar": "d.e.f"} 79 | } 80 | 81 | func defaultPlan() serviceadapter.Plan { 82 | return serviceadapter.Plan{ 83 | InstanceGroups: []serviceadapter.InstanceGroup{{ 84 | Name: "another-example-server", 85 | VMType: "small", 86 | PersistentDiskType: "ten", 87 | Networks: []string{"example-network"}, 88 | AZs: []string{"example-az"}, 89 | Instances: 1, 90 | Lifecycle: "errand", 91 | }}, 92 | Properties: serviceadapter.Properties{"example": "property"}, 93 | } 94 | } 95 | 96 | func defaultPreviousPlan() serviceadapter.Plan { 97 | return serviceadapter.Plan{ 98 | InstanceGroups: []serviceadapter.InstanceGroup{{ 99 | Name: "an-example-server", 100 | VMType: "medium", 101 | PersistentDiskType: "ten", 102 | Networks: []string{"example-network"}, 103 | AZs: []string{"example-az"}, 104 | Instances: 1, 105 | Lifecycle: "errand", 106 | }}, 107 | Properties: serviceadapter.Properties{"example": "property"}, 108 | } 109 | } 110 | 111 | func defaultManifest() bosh.BoshManifest { 112 | return bosh.BoshManifest{ 113 | Name: "another-deployment-name", 114 | Releases: []bosh.Release{ 115 | { 116 | Name: "a-release", 117 | Version: "latest", 118 | }, 119 | }, 120 | InstanceGroups: []bosh.InstanceGroup{}, 121 | Stemcells: []bosh.Stemcell{ 122 | { 123 | Alias: "greatest", 124 | OS: "Windows", 125 | Version: "3.1", 126 | }, 127 | }, 128 | } 129 | } 130 | 131 | func defaultPreviousManifest() bosh.BoshManifest { 132 | return bosh.BoshManifest{ 133 | Name: "another-deployment-name", 134 | Releases: []bosh.Release{ 135 | { 136 | Name: "a-release", 137 | Version: "latest", 138 | }, 139 | }, 140 | InstanceGroups: []bosh.InstanceGroup{}, 141 | Stemcells: []bosh.Stemcell{ 142 | { 143 | Alias: "greatest", 144 | OS: "Windows", 145 | Version: "3.1", 146 | }, 147 | }, 148 | } 149 | } 150 | 151 | func defaultPreviousBoshConfigs() serviceadapter.BOSHConfigs { 152 | return serviceadapter.BOSHConfigs{ 153 | "cloud-config": "fake-cloud-config", 154 | "cpi-config": "fake-cpi-config", 155 | "runtime-config": "fake-runtime-config", 156 | } 157 | } 158 | 159 | type CLIErrorMatcher struct { 160 | exitCode int 161 | errorSubstring string 162 | } 163 | 164 | func BeACLIError(exitCode int, errorSubstring string) types.GomegaMatcher { 165 | return &CLIErrorMatcher{ 166 | exitCode: exitCode, 167 | errorSubstring: errorSubstring, 168 | } 169 | } 170 | 171 | func (c CLIErrorMatcher) Match(actual interface{}) (bool, error) { 172 | if actual == nil { 173 | return false, errors.New("Expected error, none occured") 174 | } 175 | 176 | theError, ok := actual.(serviceadapter.CLIHandlerError) 177 | if !ok { 178 | return false, fmt.Errorf("Expected error to be of type serviceadapter.CLIHandlerError, instead got '%v'", actual) 179 | } 180 | 181 | if theError.ExitCode != c.exitCode { 182 | return false, nil 183 | } 184 | if !strings.Contains(theError.Error(), c.errorSubstring) { 185 | return false, nil 186 | } 187 | return true, nil 188 | } 189 | 190 | func (c CLIErrorMatcher) FailureMessage(actual interface{}) string { 191 | theError, _ := actual.(serviceadapter.CLIHandlerError) 192 | if theError.ExitCode != c.exitCode { 193 | return fmt.Sprintf("Expected Exit Code\n\t%d\nto equal\n\t%d", theError.ExitCode, c.exitCode) 194 | } 195 | return fmt.Sprintf("Expected error message\n\t\"%s\"\nto contain\n\t\"%s\"", theError.Error(), c.errorSubstring) 196 | } 197 | 198 | func (c CLIErrorMatcher) NegatedFailureMessage(actual interface{}) string { 199 | theError, _ := actual.(serviceadapter.CLIHandlerError) 200 | if theError.ExitCode == c.exitCode { 201 | return fmt.Sprintf("Expected Exit Code\n\t%d\nto not equal\n\t%d", theError.ExitCode, c.exitCode) 202 | } 203 | return fmt.Sprintf("Expected error message\n\t\"%s\"\nto not contain\n\t\"%s\"", theError.Error(), c.errorSubstring) 204 | } 205 | 206 | type FakeWriter struct{} 207 | 208 | func (f *FakeWriter) Write(b []byte) (int, error) { 209 | return 0, errors.New("boom!") 210 | } 211 | 212 | func NewFakeReader() io.Reader { 213 | return &FakeReader{} 214 | } 215 | 216 | type FakeReader struct{} 217 | 218 | func (f *FakeReader) Read(b []byte) (int, error) { 219 | return 1, fmt.Errorf("fool!") 220 | } 221 | -------------------------------------------------------------------------------- /serviceadapter/delete_binding_test.go: -------------------------------------------------------------------------------- 1 | package serviceadapter_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gbytes" 10 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 11 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 12 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter/fakes" 13 | ) 14 | 15 | var _ = Describe("DeleteBinding", func() { 16 | var ( 17 | fakeBinder *fakes.FakeBinder 18 | bindingId string 19 | boshVMs bosh.BoshVMs 20 | requestParams serviceadapter.RequestParameters 21 | secrets serviceadapter.ManifestSecrets 22 | dnsAddresses serviceadapter.DNSAddresses 23 | manifest bosh.BoshManifest 24 | 25 | expectedInputParams serviceadapter.InputParams 26 | action *serviceadapter.DeleteBindingAction 27 | outputBuffer *gbytes.Buffer 28 | ) 29 | 30 | BeforeEach(func() { 31 | fakeBinder = new(fakes.FakeBinder) 32 | bindingId = "binding-id" 33 | boshVMs = bosh.BoshVMs{} 34 | requestParams = defaultRequestParams() 35 | secrets = defaultSecretParams() 36 | dnsAddresses = defaultDNSParams() 37 | manifest = defaultManifest() 38 | outputBuffer = gbytes.NewBuffer() 39 | 40 | expectedInputParams = serviceadapter.InputParams{ 41 | DeleteBinding: serviceadapter.DeleteBindingJSONParams{ 42 | BindingId: bindingId, 43 | BoshVms: toJson(boshVMs), 44 | Manifest: toYaml(manifest), 45 | RequestParameters: toJson(requestParams), 46 | }, 47 | } 48 | 49 | action = serviceadapter.NewDeleteBindingAction(fakeBinder) 50 | }) 51 | 52 | Describe("IsImplemented", func() { 53 | It("returns true if implemented", func() { 54 | Expect(action.IsImplemented()).To(BeTrue()) 55 | }) 56 | 57 | It("returns false if not implemented", func() { 58 | c := serviceadapter.NewDeleteBindingAction(nil) 59 | Expect(c.IsImplemented()).To(BeFalse()) 60 | }) 61 | }) 62 | 63 | Describe("ParseArgs", func() { 64 | When("giving arguments in stdin", func() { 65 | It("can parse arguments from stdin", func() { 66 | expectedInputParams.DeleteBinding.Secrets = toJson(secrets) 67 | expectedInputParams.DeleteBinding.DNSAddresses = toJson(dnsAddresses) 68 | input := bytes.NewBuffer([]byte(toJson(expectedInputParams))) 69 | actualInputParams, err := action.ParseArgs(input, []string{}) 70 | 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(actualInputParams).To(Equal(expectedInputParams)) 73 | }) 74 | 75 | It("returns an error when cannot read from input buffer", func() { 76 | fakeReader := new(FakeReader) 77 | _, err := action.ParseArgs(fakeReader, []string{}) 78 | Expect(err).To(BeACLIError(1, "error reading input params JSON")) 79 | }) 80 | 81 | It("returns an error when cannot unmarshal from input buffer", func() { 82 | input := bytes.NewBuffer([]byte("not-valid-json")) 83 | _, err := action.ParseArgs(input, []string{}) 84 | Expect(err).To(BeACLIError(1, "error unmarshalling input params JSON")) 85 | }) 86 | 87 | It("returns an error when input buffer is empty", func() { 88 | input := bytes.NewBuffer([]byte{}) 89 | _, err := action.ParseArgs(input, []string{}) 90 | Expect(err).To(BeACLIError(1, "expecting parameters to be passed via stdin")) 91 | }) 92 | }) 93 | 94 | When("given positional arguments", func() { 95 | It("can parse positional arguments", func() { 96 | positionalArgs := []string{ 97 | expectedInputParams.DeleteBinding.BindingId, 98 | expectedInputParams.DeleteBinding.BoshVms, 99 | expectedInputParams.DeleteBinding.Manifest, 100 | expectedInputParams.DeleteBinding.RequestParameters, 101 | } 102 | 103 | actualInputParams, err := action.ParseArgs(nil, positionalArgs) 104 | Expect(err).NotTo(HaveOccurred()) 105 | Expect(actualInputParams).To(Equal(expectedInputParams)) 106 | }) 107 | 108 | It("returns an error when required arguments are not passed in", func() { 109 | _, err := action.ParseArgs(nil, []string{"foo"}) 110 | Expect(err).To(HaveOccurred()) 111 | Expect(err).To(BeAssignableToTypeOf(serviceadapter.MissingArgsError{})) 112 | Expect(err).To(MatchError(ContainSubstring(" "))) 113 | }) 114 | }) 115 | }) 116 | 117 | Describe("Execute", func() { 118 | It("calls the supplied handler passing args through", func() { 119 | fakeBinder.DeleteBindingReturns(nil) 120 | 121 | expectedInputParams.DeleteBinding.Secrets = toJson(secrets) 122 | expectedInputParams.DeleteBinding.DNSAddresses = toJson(dnsAddresses) 123 | err := action.Execute(expectedInputParams, outputBuffer) 124 | 125 | Expect(err).NotTo(HaveOccurred()) 126 | 127 | Expect(fakeBinder.DeleteBindingCallCount()).To(Equal(1)) 128 | params := fakeBinder.DeleteBindingArgsForCall(0) 129 | 130 | Expect(params.BindingID).To(Equal(bindingId)) 131 | Expect(params.DeploymentTopology).To(Equal(boshVMs)) 132 | Expect(params.Manifest).To(Equal(manifest)) 133 | Expect(params.RequestParams).To(Equal(requestParams)) 134 | Expect(params.Secrets).To(Equal(secrets)) 135 | Expect(params.DNSAddresses).To(Equal(dnsAddresses)) 136 | }) 137 | 138 | Context("error handling", func() { 139 | It("returns an error when bosh VMs cannot be unmarshalled", func() { 140 | expectedInputParams.DeleteBinding.BoshVms = "not-json" 141 | err := action.Execute(expectedInputParams, outputBuffer) 142 | Expect(err).To(MatchError(ContainSubstring("unmarshalling BOSH VMs"))) 143 | }) 144 | 145 | It("returns an error when manifest cannot be unmarshalled", func() { 146 | expectedInputParams.DeleteBinding.Manifest = "not-yaml" 147 | err := action.Execute(expectedInputParams, outputBuffer) 148 | Expect(err).To(MatchError(ContainSubstring("unmarshalling manifest YAML"))) 149 | }) 150 | 151 | It("returns an error when request params cannot be unmarshalled", func() { 152 | expectedInputParams.DeleteBinding.RequestParameters = "not-json" 153 | err := action.Execute(expectedInputParams, outputBuffer) 154 | Expect(err).To(MatchError(ContainSubstring("unmarshalling request binding parameters"))) 155 | }) 156 | 157 | It("returns an error when secrets cannot be unmarshalled", func() { 158 | expectedInputParams.DeleteBinding.Secrets = "not-json" 159 | err := action.Execute(expectedInputParams, outputBuffer) 160 | Expect(err).To(MatchError(ContainSubstring("unmarshalling secrets"))) 161 | }) 162 | 163 | It("returns an error when DNS addresses cannot be unmarshalled", func() { 164 | expectedInputParams.DeleteBinding.DNSAddresses = "not-json" 165 | err := action.Execute(expectedInputParams, outputBuffer) 166 | Expect(err).To(MatchError(ContainSubstring("unmarshalling DNS addresses"))) 167 | }) 168 | 169 | It("returns an error when binder returns an error", func() { 170 | fakeBinder.DeleteBindingReturns(errors.New("something went wrong")) 171 | err := action.Execute(expectedInputParams, outputBuffer) 172 | Expect(err).To(BeACLIError(1, "something went wrong")) 173 | }) 174 | 175 | It("returns a BindingNotFoundError when binding not found", func() { 176 | fakeBinder.DeleteBindingReturns(serviceadapter.NewBindingNotFoundError(errors.New("something went wrong"))) 177 | err := action.Execute(expectedInputParams, outputBuffer) 178 | Expect(err).To(BeACLIError(serviceadapter.BindingNotFoundErrorExitCode, "something went wrong")) 179 | }) 180 | }) 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /bosh/bosh_manifest.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package bosh 17 | 18 | import ( 19 | "fmt" 20 | "regexp" 21 | ) 22 | 23 | type BoshManifest struct { 24 | Addons []Addon `yaml:"addons,omitempty" json:"addons"` 25 | Name string `yaml:"name" json:"name"` 26 | Releases []Release `yaml:"releases" json:"releases"` 27 | Stemcells []Stemcell `yaml:"stemcells" json:"stemcells"` 28 | InstanceGroups []InstanceGroup `yaml:"instance_groups" json:"instance_groups"` 29 | Update *Update `yaml:"update" json:"update"` 30 | // DEPRECATED: BOSH deprecated deployment level "properties". Use Job properties instead. 31 | Properties map[string]interface{} `yaml:"properties,omitempty" json:"properties,omitempty"` 32 | Variables []Variable `yaml:"variables,omitempty" json:"variables,omitempty"` 33 | Tags map[string]interface{} `yaml:"tags,omitempty" json:"tags,omitempty"` 34 | Features BoshFeatures `yaml:"features,omitempty" json:"features,omitempty"` 35 | } 36 | 37 | type BoshFeatures struct { 38 | UseDNSAddresses *bool `yaml:"use_dns_addresses,omitempty"` 39 | RandomizeAZPlacement *bool `yaml:"randomize_az_placement,omitempty"` 40 | UseShortDNSAddresses *bool `yaml:"use_short_dns_addresses,omitempty"` 41 | ExtraFeatures map[string]interface{} `yaml:"extra_features,inline"` 42 | } 43 | 44 | func BoolPointer(val bool) *bool { 45 | return &val 46 | } 47 | 48 | type PlacementRuleStemcell struct { 49 | OS string `yaml:"os"` 50 | } 51 | 52 | type PlacementRule struct { 53 | Stemcell []PlacementRuleStemcell `yaml:"stemcell,omitempty"` 54 | Deployments []string `yaml:"deployments,omitempty"` 55 | Jobs []Job `yaml:"jobs,omitempty"` 56 | InstanceGroups []string `yaml:"instance_groups,omitempty"` 57 | Networks []string `yaml:"networks,omitempty"` 58 | Teams []string `yaml:"teams,omitempty"` 59 | } 60 | 61 | type Addon struct { 62 | Name string `yaml:"name"` 63 | Jobs []Job `yaml:"jobs"` 64 | Include PlacementRule `yaml:"include,omitempty"` 65 | Exclude PlacementRule `yaml:"exclude,omitempty"` 66 | } 67 | 68 | // Variable represents a variable in the `variables` block of a BOSH manifest 69 | type Variable struct { 70 | Name string `yaml:"name"` 71 | Type string `yaml:"type"` 72 | UpdateMode string `yaml:"update_mode,omitempty"` 73 | Options map[string]interface{} `yaml:"options,omitempty"` 74 | 75 | // Variables of type `certificate` can optionally be configured with a 76 | // `consumes` block, so generated certificates can be created with automatic 77 | // BOSH DNS records in their Common Name and/or Subject Alternative Names. 78 | // 79 | // Should be used in conjunction to the `custom_provider_definitions` block in 80 | // a Job. 81 | // 82 | // Requires BOSH v267+ 83 | Consumes *VariableConsumes `yaml:"consumes,omitempty"` 84 | } 85 | 86 | type VariableConsumes struct { 87 | AlternativeName VariableConsumesLink `yaml:"alternative_name,omitempty"` 88 | CommonName VariableConsumesLink `yaml:"common_name,omitempty"` 89 | } 90 | 91 | type VariableConsumesLink struct { 92 | From string `yaml:"from"` 93 | Properties map[string]interface{} `yaml:"properties,omitempty"` 94 | } 95 | 96 | type Release struct { 97 | Name string `yaml:"name"` 98 | Version string `yaml:"version"` 99 | } 100 | 101 | type Stemcell struct { 102 | Alias string `yaml:"alias"` 103 | OS string `yaml:"os"` 104 | Version string `yaml:"version"` 105 | Name string `yaml:"name,omitempty"` 106 | } 107 | 108 | type InstanceGroup struct { 109 | Name string `yaml:"name,omitempty"` 110 | Lifecycle string `yaml:"lifecycle,omitempty"` 111 | Instances int `yaml:"instances"` 112 | Jobs []Job `yaml:"jobs,omitempty"` 113 | VMType string `yaml:"vm_type"` 114 | VMExtensions []string `yaml:"vm_extensions,omitempty"` 115 | Stemcell string `yaml:"stemcell"` 116 | PersistentDiskType string `yaml:"persistent_disk_type,omitempty"` 117 | AZs []string `yaml:"azs,omitempty"` 118 | Networks []Network `yaml:"networks"` 119 | // DEPRECATED: BOSH deprecated instance_group level "properties". Use Job properties instead. 120 | Properties map[string]interface{} `yaml:"properties,omitempty"` 121 | MigratedFrom []Migration `yaml:"migrated_from,omitempty"` 122 | Env map[string]interface{} `yaml:"env,omitempty"` 123 | Update *Update `yaml:"update,omitempty"` 124 | } 125 | 126 | type Migration struct { 127 | Name string `yaml:"name"` 128 | } 129 | 130 | type Network struct { 131 | Name string `yaml:"name"` 132 | StaticIPs []string `yaml:"static_ips,omitempty"` 133 | Default []string `yaml:"default,omitempty"` 134 | } 135 | 136 | // MaxInFlightValue holds a value of one of these types: 137 | // 138 | // int, for YAML numbers 139 | // string, for YAML string literals representing a percentage 140 | type MaxInFlightValue interface{} 141 | 142 | type UpdateStrategy string 143 | 144 | const ( 145 | SerialUpdate UpdateStrategy = "serial" 146 | ParallelUpdate UpdateStrategy = "parallel" 147 | ) 148 | 149 | type Update struct { 150 | Canaries int `yaml:"canaries"` 151 | CanaryWatchTime string `yaml:"canary_watch_time"` 152 | UpdateWatchTime string `yaml:"update_watch_time"` 153 | MaxInFlight MaxInFlightValue `yaml:"max_in_flight"` 154 | Serial *bool `yaml:"serial,omitempty"` 155 | VmStrategy string `yaml:"vm_strategy,omitempty"` 156 | // See bosh.SerialUpdate and bosh.ParallelUpdate 157 | InitialDeployAZUpdateStrategy UpdateStrategy `yaml:"initial_deploy_az_update_strategy,omitempty"` 158 | } 159 | 160 | type updateAlias Update 161 | 162 | func (u *Update) MarshalYAML() (interface{}, error) { 163 | if u != nil { 164 | if err := ValidateMaxInFlight(u.MaxInFlight); err != nil { 165 | return []byte{}, err 166 | } 167 | } 168 | 169 | return (*updateAlias)(u), nil 170 | } 171 | 172 | func (u *Update) UnmarshalYAML(unmarshal func(interface{}) error) error { 173 | err := unmarshal((*updateAlias)(u)) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | if u != nil { 179 | return ValidateMaxInFlight(u.MaxInFlight) 180 | } 181 | 182 | return nil 183 | } 184 | 185 | func ValidateMaxInFlight(m MaxInFlightValue) error { 186 | switch v := m.(type) { 187 | case string: 188 | matched, err := regexp.Match(`\d+%`, []byte(v)) 189 | if !matched || err != nil { 190 | return fmt.Errorf("MaxInFlight must be either an integer or a percentage. Got %v", v) 191 | } 192 | case int: 193 | default: 194 | return fmt.Errorf("MaxInFlight must be either an integer or a percentage. Got %v", v) 195 | } 196 | 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | code.cloudfoundry.org/brokerapi/v13 v13.0.15 h1:esa7Lqc4NGwhikp79mDkiwuW4x0mUqHwcvR941554Mo= 2 | code.cloudfoundry.org/brokerapi/v13 v13.0.15/go.mod h1:1YVZiOwRtpkf0JR0TagEQN4mHUCoCWhWzU6PX+PM0dA= 3 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 4 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= 8 | github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 9 | github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= 10 | github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= 11 | github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= 12 | github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= 13 | github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= 14 | github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 15 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 16 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 18 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 19 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 20 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 21 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 22 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 23 | github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk= 24 | github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4= 25 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 26 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 27 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 28 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 29 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 30 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 31 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= 32 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= 33 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 34 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 35 | github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= 36 | github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 37 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 38 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 39 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 40 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 41 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 42 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 43 | github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= 44 | github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= 45 | github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1 h1:D4O2wLxB384TS3ohBJMfolnxb4qGmoZ1PnWNtit8LYo= 46 | github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1/go.mod h1:RuJdxo0oI6dClIaMzdl3hewq3a065RH65dofJP03h8I= 47 | github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= 48 | github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= 49 | github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8= 50 | github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= 51 | github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= 52 | github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= 53 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 54 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 58 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 59 | github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= 60 | github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= 61 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 62 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 63 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 64 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 65 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 66 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 67 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 68 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 69 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 70 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 71 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 72 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 73 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 74 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 75 | golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= 76 | golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 77 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 78 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 79 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 80 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 81 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 82 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 83 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 84 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 85 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= 86 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 87 | google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= 88 | google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 89 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 90 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 91 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 92 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 93 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 94 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 95 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | -------------------------------------------------------------------------------- /serviceadapter/create_binding_test.go: -------------------------------------------------------------------------------- 1 | package serviceadapter_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 12 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 13 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter/fakes" 14 | ) 15 | 16 | var _ = Describe("CreateBinding", func() { 17 | var ( 18 | fakeBinder *fakes.FakeBinder 19 | bindingId string 20 | boshVMs bosh.BoshVMs 21 | requestParams serviceadapter.RequestParameters 22 | manifest bosh.BoshManifest 23 | dnsAddresses serviceadapter.DNSAddresses 24 | 25 | expectedInputParams serviceadapter.InputParams 26 | action *serviceadapter.CreateBindingAction 27 | outputBuffer *gbytes.Buffer 28 | ) 29 | 30 | BeforeEach(func() { 31 | fakeBinder = new(fakes.FakeBinder) 32 | bindingId = "binding-id" 33 | boshVMs = bosh.BoshVMs{} 34 | requestParams = defaultRequestParams() 35 | manifest = defaultManifest() 36 | outputBuffer = gbytes.NewBuffer() 37 | 38 | expectedInputParams = serviceadapter.InputParams{ 39 | CreateBinding: serviceadapter.CreateBindingJSONParams{ 40 | BindingId: bindingId, 41 | BoshVms: toJson(boshVMs), 42 | Manifest: toYaml(manifest), 43 | RequestParameters: toJson(requestParams), 44 | }, 45 | } 46 | 47 | action = serviceadapter.NewCreateBindingAction(fakeBinder) 48 | }) 49 | 50 | Describe("IsImplemented", func() { 51 | It("returns true if implemented", func() { 52 | Expect(action.IsImplemented()).To(BeTrue()) 53 | }) 54 | 55 | It("returns false if not implemented", func() { 56 | c := serviceadapter.NewCreateBindingAction(nil) 57 | Expect(c.IsImplemented()).To(BeFalse()) 58 | }) 59 | }) 60 | 61 | Describe("ParseArgs", func() { 62 | When("giving arguments in stdin", func() { 63 | It("can parse arguments from stdin", func() { 64 | input := bytes.NewBuffer([]byte(toJson(expectedInputParams))) 65 | actualInputParams, err := action.ParseArgs(input, []string{}) 66 | 67 | Expect(err).NotTo(HaveOccurred()) 68 | Expect(actualInputParams).To(Equal(expectedInputParams)) 69 | }) 70 | 71 | It("returns an error when cannot read from input buffer", func() { 72 | fakeReader := new(FakeReader) 73 | _, err := action.ParseArgs(fakeReader, []string{}) 74 | Expect(err).To(BeACLIError(1, "error reading input params JSON")) 75 | }) 76 | 77 | It("returns an error when cannot unmarshal from input buffer", func() { 78 | input := bytes.NewBuffer([]byte("not-valid-json")) 79 | _, err := action.ParseArgs(input, []string{}) 80 | Expect(err).To(BeACLIError(1, "error unmarshalling input params JSON")) 81 | }) 82 | 83 | It("returns an error when input buffer is empty", func() { 84 | input := bytes.NewBuffer([]byte{}) 85 | _, err := action.ParseArgs(input, []string{}) 86 | Expect(err).To(BeACLIError(1, "expecting parameters to be passed via stdin")) 87 | }) 88 | 89 | It("can parse manifest secrets", func() { 90 | expectedInputParamsWithSecrets := expectedInputParams 91 | expectedInputParamsWithSecrets.CreateBinding.Secrets = `{ "/foo": "{ "status": "foo" }" }` 92 | input := bytes.NewBuffer([]byte(toJson(expectedInputParamsWithSecrets))) 93 | actualInputParams, err := action.ParseArgs(input, []string{}) 94 | 95 | Expect(err).NotTo(HaveOccurred()) 96 | Expect(actualInputParams).To(Equal(expectedInputParamsWithSecrets)) 97 | }) 98 | }) 99 | 100 | When("given positional arguments", func() { 101 | It("can parse positional arguments", func() { 102 | positionalArgs := []string{ 103 | expectedInputParams.CreateBinding.BindingId, 104 | expectedInputParams.CreateBinding.BoshVms, 105 | expectedInputParams.CreateBinding.Manifest, 106 | expectedInputParams.CreateBinding.RequestParameters, 107 | } 108 | 109 | actualInputParams, err := action.ParseArgs(nil, positionalArgs) 110 | Expect(err).NotTo(HaveOccurred()) 111 | Expect(actualInputParams).To(Equal(expectedInputParams)) 112 | }) 113 | 114 | It("returns an error when required arguments are not passed in", func() { 115 | _, err := action.ParseArgs(nil, []string{"foo"}) 116 | Expect(err).To(HaveOccurred()) 117 | Expect(err).To(BeAssignableToTypeOf(serviceadapter.MissingArgsError{})) 118 | Expect(err).To(MatchError(ContainSubstring(" "))) 119 | }) 120 | }) 121 | }) 122 | 123 | Describe("Execute", func() { 124 | It("calls the supplied handler passing args through", func() { 125 | binding := serviceadapter.Binding{ 126 | Credentials: map[string]interface{}{ 127 | "password": "letmein", 128 | }, 129 | } 130 | fakeBinder.CreateBindingReturns(binding, nil) 131 | 132 | dnsAddresses = serviceadapter.DNSAddresses{ 133 | "config-1": "some-dns.bosh", 134 | } 135 | expectedInputParams.CreateBinding.DNSAddresses = toJson(dnsAddresses) 136 | expectedInputParams.CreateBinding.Secrets = `{ "/foo": "{ \"status\": \"bar\" }" }` 137 | err := action.Execute(expectedInputParams, outputBuffer) 138 | 139 | Expect(err).NotTo(HaveOccurred()) 140 | 141 | Expect(fakeBinder.CreateBindingCallCount()).To(Equal(1)) 142 | params := fakeBinder.CreateBindingArgsForCall(0) 143 | 144 | Expect(params.BindingID).To(Equal(bindingId)) 145 | Expect(params.DeploymentTopology).To(Equal(boshVMs)) 146 | Expect(params.Manifest).To(Equal(manifest)) 147 | Expect(params.RequestParams).To(Equal(requestParams)) 148 | Expect(params.DNSAddresses).To(Equal(dnsAddresses)) 149 | Expect(params.Secrets).To(Equal(serviceadapter.ManifestSecrets{ 150 | "/foo": `{ "status": "bar" }`, 151 | })) 152 | 153 | var bindingOutput serviceadapter.Binding 154 | json.Unmarshal(outputBuffer.Contents(), &bindingOutput) 155 | Expect(bindingOutput).To(Equal(binding)) 156 | }) 157 | 158 | Context("error handling", func() { 159 | It("returns an error when bosh VMs cannot be unmarshalled", func() { 160 | expectedInputParams.CreateBinding.BoshVms = "not-json" 161 | err := action.Execute(expectedInputParams, outputBuffer) 162 | Expect(err).To(MatchError(ContainSubstring("unmarshalling BOSH VMs"))) 163 | }) 164 | 165 | It("returns an error when manifest cannot be unmarshalled", func() { 166 | expectedInputParams.CreateBinding.Manifest = "not-yaml" 167 | err := action.Execute(expectedInputParams, outputBuffer) 168 | Expect(err).To(MatchError(ContainSubstring("unmarshalling manifest YAML"))) 169 | }) 170 | 171 | It("returns an error when request params cannot be unmarshalled", func() { 172 | expectedInputParams.CreateBinding.RequestParameters = "not-json" 173 | err := action.Execute(expectedInputParams, outputBuffer) 174 | Expect(err).To(MatchError(ContainSubstring("unmarshalling request binding parameters"))) 175 | }) 176 | 177 | It("returns an error when secrets cannot be unmarshalled", func() { 178 | expectedInputParams.CreateBinding.Secrets = "not-json" 179 | err := action.Execute(expectedInputParams, outputBuffer) 180 | Expect(err).To(MatchError(ContainSubstring("unmarshalling secrets"))) 181 | }) 182 | 183 | It("returns an error when DNS addresses cannot be unmarshalled", func() { 184 | expectedInputParams.CreateBinding.DNSAddresses = "not-json" 185 | err := action.Execute(expectedInputParams, outputBuffer) 186 | Expect(err).To(MatchError(ContainSubstring("unmarshalling DNS addresses"))) 187 | }) 188 | 189 | It("returns an generic error when binder returns an error", func() { 190 | fakeBinder.CreateBindingReturns(serviceadapter.Binding{}, errors.New("something went wrong")) 191 | err := action.Execute(expectedInputParams, outputBuffer) 192 | Expect(err).To(BeACLIError(1, "something went wrong")) 193 | }) 194 | 195 | It("returns a BindingAlreadyExistsError when binding already exists", func() { 196 | fakeBinder.CreateBindingReturns(serviceadapter.Binding{}, serviceadapter.NewBindingAlreadyExistsError(errors.New("something went wrong"))) 197 | err := action.Execute(expectedInputParams, outputBuffer) 198 | Expect(err).To(BeACLIError(serviceadapter.BindingAlreadyExistsErrorExitCode, "something went wrong")) 199 | }) 200 | 201 | It("returns an AppGuidNotProvidedError when app guid is not provided", func() { 202 | fakeBinder.CreateBindingReturns(serviceadapter.Binding{}, serviceadapter.NewAppGuidNotProvidedError(errors.New("something went wrong"))) 203 | err := action.Execute(expectedInputParams, outputBuffer) 204 | Expect(err).To(BeACLIError(serviceadapter.AppGuidNotProvidedErrorExitCode, "something went wrong")) 205 | }) 206 | 207 | It("returns an error when the binding cannot be marshalled", func() { 208 | fakeBinder.CreateBindingReturns(serviceadapter.Binding{ 209 | Credentials: map[string]interface{}{"a": make(chan bool)}, 210 | }, 211 | nil, 212 | ) 213 | 214 | err := action.Execute(expectedInputParams, outputBuffer) 215 | Expect(err).To(MatchError(ContainSubstring("error marshalling binding"))) 216 | }) 217 | }) 218 | }) 219 | }) 220 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /bosh/bosh_manifest_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package bosh_test 17 | 18 | import ( 19 | "errors" 20 | "io" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | "text/template" 25 | 26 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 27 | 28 | . "github.com/onsi/ginkgo/v2" 29 | . "github.com/onsi/gomega" 30 | "github.com/onsi/gomega/gbytes" 31 | "gopkg.in/yaml.v2" 32 | ) 33 | 34 | var _ = Describe("(de)serialising BOSH manifests", func() { 35 | boolPointer := func(b bool) *bool { 36 | return &b 37 | } 38 | 39 | sampleManifest := bosh.BoshManifest{ 40 | Name: "deployment-name", 41 | Releases: []bosh.Release{ 42 | { 43 | Name: "a-release", 44 | Version: "latest", 45 | }, 46 | }, 47 | Addons: []bosh.Addon{ 48 | { 49 | Name: "some-addon", 50 | Jobs: []bosh.Job{ 51 | { 52 | Name: "the-italian-job", 53 | Release: "2003", 54 | }, 55 | }, 56 | Include: bosh.PlacementRule{ 57 | Stemcell: []bosh.PlacementRuleStemcell{ 58 | { 59 | OS: "ubuntu-trusty", 60 | }, 61 | }, 62 | Deployments: []string{ 63 | "dep1", 64 | "dep2", 65 | }, 66 | Jobs: []bosh.Job{ 67 | { 68 | Name: "the-italian-job-old", 69 | Release: "1969", 70 | }, 71 | }, 72 | InstanceGroups: []string{ 73 | "an-errand", 74 | }, 75 | Networks: []string{ 76 | "some-network", 77 | }, 78 | Teams: []string{ 79 | "a-team", 80 | }, 81 | }, 82 | Exclude: bosh.PlacementRule{ 83 | Stemcell: []bosh.PlacementRuleStemcell{ 84 | { 85 | OS: "ubuntu-jammy", 86 | }, 87 | }, 88 | Deployments: []string{ 89 | "dep3", 90 | }, 91 | Jobs: []bosh.Job{ 92 | { 93 | Name: "the-italian-job", 94 | Release: "1969", 95 | }, 96 | }, 97 | InstanceGroups: []string{ 98 | "an-errand", 99 | }, 100 | Networks: []string{ 101 | "some-network", 102 | }, 103 | Teams: []string{ 104 | "a-team", 105 | }, 106 | }, 107 | }, 108 | }, 109 | Stemcells: []bosh.Stemcell{ 110 | { 111 | Alias: "greatest", 112 | OS: "Windows", 113 | Version: "3.1", 114 | }, 115 | }, 116 | InstanceGroups: []bosh.InstanceGroup{ 117 | { 118 | Name: "jerb", 119 | Instances: 1, 120 | Jobs: []bosh.Job{ 121 | { 122 | Name: "broker", 123 | Release: "a-release", 124 | Provides: map[string]bosh.ProvidesLink{ 125 | "some_link": {As: "link-name"}, 126 | }, 127 | Consumes: map[string]interface{}{ 128 | "another_link": bosh.ConsumesLink{From: "jerb-link"}, 129 | "nullified_link": "nil", 130 | }, 131 | CustomProviderDefinitions: []bosh.CustomProviderDefinition{ 132 | {Name: "some-custom-link", Type: "some-link-type", Properties: []string{"prop1", "url"}}, 133 | }, 134 | Properties: map[string]interface{}{ 135 | "some_property": "some_value", 136 | }, 137 | }, 138 | }, 139 | VMType: "massive", 140 | VMExtensions: []string{"extended"}, 141 | PersistentDiskType: "big", 142 | AZs: []string{"az1", "az2"}, 143 | Stemcell: "greatest", 144 | Networks: []bosh.Network{ 145 | { 146 | Name: "a-network", 147 | StaticIPs: []string{"10.0.0.0"}, 148 | Default: []string{"dns"}, 149 | }, 150 | }, 151 | MigratedFrom: []bosh.Migration{ 152 | { 153 | Name: "old-instance-group-name", 154 | }, 155 | }, 156 | Env: map[string]interface{}{ 157 | "bosh": map[string]interface{}{ 158 | "password": "passwerd", 159 | "keep_root_password": true, 160 | "remove_dev_tools": false, 161 | "remove_static_libraries": false, 162 | "swap_size": 0, 163 | }, 164 | "something_else": "foo", 165 | }, 166 | Update: &bosh.Update{ 167 | Canaries: 1, 168 | CanaryWatchTime: "30000-180000", 169 | UpdateWatchTime: "30000-180000", 170 | MaxInFlight: 10, 171 | Serial: boolPointer(false), 172 | InitialDeployAZUpdateStrategy: bosh.ParallelUpdate, 173 | }, 174 | }, 175 | { 176 | Name: "an-errand", 177 | Lifecycle: "errand", 178 | Instances: 1, 179 | Jobs: []bosh.Job{ 180 | { 181 | Name: "a-release", 182 | Release: "a-release", 183 | }, 184 | }, 185 | VMType: "small", 186 | Stemcell: "greatest", 187 | Networks: []bosh.Network{ 188 | { 189 | Name: "a-network", 190 | }, 191 | }, 192 | }, 193 | }, 194 | Properties: map[string]interface{}{ 195 | "foo": "bar", 196 | }, 197 | Update: &bosh.Update{ 198 | Canaries: 1, 199 | CanaryWatchTime: "30000-180000", 200 | UpdateWatchTime: "30000-180000", 201 | MaxInFlight: 4, 202 | Serial: boolPointer(false), 203 | VmStrategy: "create-and-swap", 204 | InitialDeployAZUpdateStrategy: bosh.SerialUpdate, 205 | }, 206 | Variables: []bosh.Variable{ 207 | { 208 | Name: "admin_password", 209 | Type: "password", 210 | }, 211 | { 212 | Name: "default_ca", 213 | Type: "certificate", 214 | UpdateMode: "converge", 215 | Options: map[string]interface{}{ 216 | "is_ca": true, 217 | "alternative_names": []string{"some-other-ca"}, 218 | }, 219 | Consumes: &bosh.VariableConsumes{ 220 | AlternativeName: bosh.VariableConsumesLink{ 221 | From: "my-custom-app-server-address", 222 | }, 223 | CommonName: bosh.VariableConsumesLink{ 224 | From: "my-custom-app-server-address", 225 | Properties: map[string]interface{}{ 226 | "wildcard": true, 227 | }, 228 | }, 229 | }, 230 | }, 231 | }, 232 | Tags: map[string]interface{}{ 233 | "quadrata": "parrot", 234 | "secondTag": "tagValue", 235 | }, 236 | Features: bosh.BoshFeatures{ 237 | RandomizeAZPlacement: bosh.BoolPointer(true), 238 | UseShortDNSAddresses: bosh.BoolPointer(false), 239 | ExtraFeatures: map[string]interface{}{ 240 | "another_feature": "ok", 241 | }, 242 | }, 243 | } 244 | 245 | It("serialises bosh manifests", func() { 246 | cwd, err := os.Getwd() 247 | Expect(err).NotTo(HaveOccurred()) 248 | manifestBytes, err := os.ReadFile(filepath.Join(cwd, "fixtures", "manifest.yml")) 249 | Expect(err).NotTo(HaveOccurred()) 250 | 251 | serialisedManifest, err := yaml.Marshal(sampleManifest) 252 | Expect(err).NotTo(HaveOccurred()) 253 | Expect(serialisedManifest).To(MatchYAML(manifestBytes)) 254 | }) 255 | 256 | It("deserialises bosh manifest features into struct", func() { 257 | cwd, err := os.Getwd() 258 | Expect(err).NotTo(HaveOccurred()) 259 | manifestBytes, err := os.ReadFile(filepath.Join(cwd, "fixtures", "manifest.yml")) 260 | Expect(err).NotTo(HaveOccurred()) 261 | 262 | manifest := bosh.BoshManifest{} 263 | err = yaml.Unmarshal(manifestBytes, &manifest) 264 | Expect(err).NotTo(HaveOccurred()) 265 | 266 | Expect(manifest.Features).To(Equal(sampleManifest.Features)) 267 | }) 268 | 269 | It("omits optional keys", func() { 270 | emptyManifest := bosh.BoshManifest{ 271 | Releases: []bosh.Release{ 272 | {}, 273 | }, 274 | Stemcells: []bosh.Stemcell{ 275 | {}, 276 | }, 277 | InstanceGroups: []bosh.InstanceGroup{ 278 | { 279 | Networks: []bosh.Network{ 280 | {}, 281 | }, 282 | }, 283 | }, 284 | Update: &bosh.Update{ 285 | Canaries: 1, 286 | CanaryWatchTime: "30000-180000", 287 | UpdateWatchTime: "30000-180000", 288 | MaxInFlight: 4, 289 | }, 290 | Variables: []bosh.Variable{}, 291 | Tags: map[string]interface{}{}, 292 | } 293 | 294 | content, err := yaml.Marshal(emptyManifest) 295 | Expect(err).NotTo(HaveOccurred()) 296 | 297 | Expect(content).NotTo(ContainSubstring("static_ips:")) 298 | Expect(content).NotTo(ContainSubstring("lifecycle:")) 299 | Expect(content).NotTo(ContainSubstring("azs:")) 300 | Expect(content).NotTo(ContainSubstring("vm_extensions:")) 301 | Expect(content).NotTo(ContainSubstring("persistent_disk_type:")) 302 | Expect(content).NotTo(ContainSubstring("jobs:")) 303 | Expect(content).NotTo(ContainSubstring("provides:")) 304 | Expect(content).NotTo(ContainSubstring("consumes:")) 305 | Expect(content).NotTo(ContainSubstring("properties:")) 306 | Expect(content).NotTo(ContainSubstring("serial:")) 307 | Expect(content).NotTo(ContainSubstring("variables:")) 308 | Expect(content).NotTo(ContainSubstring("migrated_from:")) 309 | Expect(content).NotTo(ContainSubstring("tags:")) 310 | Expect(content).NotTo(ContainSubstring("features:")) 311 | Expect(content).NotTo(ContainSubstring("vm_strategy:")) 312 | Expect(content).NotTo(ContainSubstring("custom_provider_definitions:")) 313 | Expect(strings.Count(string(content), "update:")).To(Equal(1)) 314 | }) 315 | 316 | It("omits optional keys from Variables", func() { 317 | emptyManifest := bosh.BoshManifest{ 318 | Variables: []bosh.Variable{ 319 | { 320 | Name: "admin_password", 321 | Type: "password", 322 | }, 323 | }, 324 | } 325 | 326 | content, err := yaml.Marshal(emptyManifest) 327 | Expect(err).NotTo(HaveOccurred()) 328 | Expect(content).NotTo(ContainSubstring("options:")) 329 | Expect(content).NotTo(ContainSubstring("consumes:")) 330 | }) 331 | 332 | It("includes set properties and omits unset properties in Features", func() { 333 | emptyishManifest := bosh.BoshManifest{ 334 | Features: bosh.BoshFeatures{ 335 | UseDNSAddresses: bosh.BoolPointer(true), 336 | UseShortDNSAddresses: bosh.BoolPointer(false), 337 | // RandomizeAZPlacement is deliberately omitted 338 | }, 339 | } 340 | 341 | content, err := yaml.Marshal(emptyishManifest) 342 | Expect(err).NotTo(HaveOccurred()) 343 | Expect(string(content)).To(ContainSubstring("use_dns_addresses:")) 344 | Expect(string(content)).To(ContainSubstring("use_short_dns_addresses:")) 345 | Expect(string(content)).NotTo(ContainSubstring("randomize_az_placement:")) 346 | }) 347 | 348 | DescribeTable( 349 | "marshalling when max in flight set to", 350 | func(maxInFlight bosh.MaxInFlightValue, expectedErr error, expectedContent string) { 351 | manifest := bosh.BoshManifest{ 352 | Update: &bosh.Update{ 353 | MaxInFlight: maxInFlight, 354 | }, 355 | } 356 | content, err := yaml.Marshal(&manifest) 357 | 358 | if expectedErr != nil { 359 | Expect(err).To(MatchError(expectedErr)) 360 | } else { 361 | Expect(err).NotTo(HaveOccurred()) 362 | Expect(string(content)).To(ContainSubstring(expectedContent)) 363 | } 364 | }, 365 | Entry("a percentage", "25%", nil, "max_in_flight: 25%"), 366 | Entry("an integer", 4, nil, "max_in_flight: 4"), 367 | Entry("a float", 0.2, errors.New("MaxInFlight must be either an integer or a percentage. Got 0.2"), ""), 368 | Entry("nil", nil, errors.New("MaxInFlight must be either an integer or a percentage. Got "), ""), 369 | Entry("a bool", true, errors.New("MaxInFlight must be either an integer or a percentage. Got true"), ""), 370 | Entry("a non percentage string", "some instances", errors.New("MaxInFlight must be either an integer or a percentage. Got some instances"), ""), 371 | Entry("a numeric string", "24", errors.New("MaxInFlight must be either an integer or a percentage. Got 24"), ""), 372 | ) 373 | 374 | DescribeTable( 375 | "unmarshalling when max in flight set to", 376 | func(maxInFlight bosh.MaxInFlightValue, expectedErr error) { 377 | cwd, err := os.Getwd() 378 | Expect(err).NotTo(HaveOccurred()) 379 | tmpl, err := template.ParseFiles(filepath.Join(cwd, "fixtures", "manifest_template.yml")) 380 | Expect(err).NotTo(HaveOccurred()) 381 | 382 | output := gbytes.NewBuffer() 383 | err = tmpl.Execute(output, map[string]any{ 384 | "MaxInFlight": maxInFlight, 385 | }) 386 | Expect(err).NotTo(HaveOccurred()) 387 | 388 | var manifest bosh.BoshManifest 389 | err = yaml.Unmarshal(output.Contents(), &manifest) 390 | 391 | if expectedErr != nil { 392 | Expect(err).To(MatchError(expectedErr)) 393 | } else { 394 | Expect(err).NotTo(HaveOccurred()) 395 | Expect(manifest.Update.MaxInFlight).To(Equal(maxInFlight)) 396 | } 397 | }, 398 | Entry("a percentage", "25%", nil), 399 | Entry("an integer", 4, nil), 400 | Entry("a float", 0.2, errors.New("MaxInFlight must be either an integer or a percentage. Got 0.2")), 401 | Entry("null", "null", errors.New("MaxInFlight must be either an integer or a percentage. Got ")), 402 | Entry("a bool", true, errors.New("MaxInFlight must be either an integer or a percentage. Got true")), 403 | Entry("a non percentage string", "some instances", errors.New("MaxInFlight must be either an integer or a percentage. Got some instances")), 404 | ) 405 | 406 | When("a stemcell name has been configured", func() { 407 | It("correctly handles a manifest including a stemcell name property", func() { 408 | tmpl, err := template.ParseFiles(filepath.Join("fixtures", "manifest_template.yml")) 409 | Expect(err).NotTo(HaveOccurred()) 410 | 411 | params := map[string]any{ 412 | "MaxInFlight": 1, 413 | "StemcellName": "ubuntu-fancy-name-with-fips", 414 | } 415 | 416 | output := gbytes.NewBuffer() 417 | err = tmpl.Execute(io.MultiWriter(GinkgoWriter, output), params) 418 | Expect(err).NotTo(HaveOccurred()) 419 | 420 | var manifest bosh.BoshManifest 421 | err = yaml.Unmarshal(output.Contents(), &manifest) 422 | Expect(err).NotTo(HaveOccurred()) 423 | 424 | Expect(manifest.Stemcells[0].Name).To(Equal("ubuntu-fancy-name-with-fips")) 425 | }) 426 | }) 427 | }) 428 | -------------------------------------------------------------------------------- /serviceadapter/domain.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package serviceadapter 17 | 18 | import ( 19 | "bytes" 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "io" 24 | 25 | "code.cloudfoundry.org/brokerapi/v13/domain" 26 | "github.com/go-playground/validator/v10" 27 | 28 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 29 | ) 30 | 31 | type GenerateManifestParams struct { 32 | ServiceDeployment ServiceDeployment 33 | Plan Plan 34 | RequestParams RequestParameters 35 | PreviousManifest *bosh.BoshManifest 36 | PreviousPlan *Plan 37 | PreviousSecrets ManifestSecrets 38 | PreviousConfigs BOSHConfigs 39 | ServiceInstanceUAAClient *ServiceInstanceUAAClient 40 | } 41 | 42 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o fakes/manifest_generator.go . ManifestGenerator 43 | 44 | type ManifestGenerator interface { 45 | GenerateManifest(params GenerateManifestParams) (GenerateManifestOutput, error) 46 | } 47 | 48 | type CreateBindingParams struct { 49 | BindingID string 50 | DeploymentTopology bosh.BoshVMs 51 | Manifest bosh.BoshManifest 52 | RequestParams RequestParameters 53 | Secrets ManifestSecrets 54 | DNSAddresses DNSAddresses 55 | } 56 | 57 | type DeleteBindingParams struct { 58 | BindingID string 59 | DeploymentTopology bosh.BoshVMs 60 | Manifest bosh.BoshManifest 61 | RequestParams RequestParameters 62 | Secrets ManifestSecrets 63 | DNSAddresses DNSAddresses 64 | } 65 | 66 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o fakes/binder.go . Binder 67 | 68 | type Binder interface { 69 | CreateBinding(params CreateBindingParams) (Binding, error) 70 | DeleteBinding(params DeleteBindingParams) error 71 | } 72 | 73 | type DashboardUrlParams struct { 74 | InstanceID string 75 | Plan Plan 76 | Manifest bosh.BoshManifest 77 | } 78 | 79 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o fakes/dashboard_url_generator.go . DashboardUrlGenerator 80 | 81 | type DashboardUrlGenerator interface { 82 | DashboardUrl(params DashboardUrlParams) (DashboardUrl, error) 83 | } 84 | 85 | type GeneratePlanSchemaParams struct { 86 | Plan Plan 87 | } 88 | 89 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o fakes/schema_generator.go . SchemaGenerator 90 | 91 | type SchemaGenerator interface { 92 | GeneratePlanSchema(params GeneratePlanSchemaParams) (PlanSchema, error) 93 | } 94 | 95 | type ServiceInstanceSchema struct { 96 | Create JSONSchemas `json:"create"` 97 | Update JSONSchemas `json:"update"` 98 | } 99 | 100 | type JSONSchemas struct { 101 | Parameters map[string]interface{} `json:"parameters"` 102 | } 103 | 104 | type ServiceBindingSchema struct { 105 | Create JSONSchemas `json:"create"` 106 | } 107 | 108 | type PlanSchema struct { 109 | ServiceInstance ServiceInstanceSchema `json:"service_instance"` 110 | ServiceBinding ServiceBindingSchema `json:"service_binding"` 111 | } 112 | 113 | type ( 114 | ManifestSecrets map[string]string 115 | DNSAddresses map[string]string 116 | ) 117 | 118 | type DashboardUrl struct { 119 | DashboardUrl string `json:"dashboard_url"` 120 | } 121 | 122 | type GenerateManifestJSONParams struct { 123 | ServiceDeployment string `json:"service_deployment"` 124 | Plan string `json:"plan"` 125 | PreviousPlan string `json:"previous_plan"` 126 | PreviousManifest string `json:"previous_manifest"` 127 | RequestParameters string `json:"request_parameters"` 128 | PreviousSecrets string `json:"previous_secrets"` 129 | PreviousConfigs string `json:"previous_configs"` 130 | ServiceInstanceUAAClient string `json:"uaa_client"` 131 | } 132 | 133 | type DashboardUrlJSONParams struct { 134 | InstanceId string `json:"instance_id"` 135 | Plan string `json:"plan"` 136 | Manifest string `json:"manifest"` 137 | } 138 | 139 | type CreateBindingJSONParams struct { 140 | BindingId string `json:"binding_id"` 141 | BoshVms string `json:"bosh_vms"` 142 | Manifest string `json:"manifest"` 143 | RequestParameters string `json:"request_parameters"` 144 | Secrets string `json:"secrets"` 145 | DNSAddresses string `json:"dns_addresses"` 146 | } 147 | 148 | type DeleteBindingJSONParams struct { 149 | BindingId string `json:"binding_id"` 150 | BoshVms string `json:"bosh_vms"` 151 | Manifest string `json:"manifest"` 152 | RequestParameters string `json:"request_parameters"` 153 | Secrets string `json:"secrets"` 154 | DNSAddresses string `json:"dns_addresses"` 155 | } 156 | 157 | type GeneratePlanSchemasJSONParams struct { 158 | Plan string `json:"plan"` 159 | } 160 | 161 | type InputParams struct { 162 | GenerateManifest GenerateManifestJSONParams `json:"generate_manifest,omitempty"` 163 | DashboardUrl DashboardUrlJSONParams `json:"dashboard_url,omitempty"` 164 | CreateBinding CreateBindingJSONParams `json:"create_binding,omitempty"` 165 | DeleteBinding DeleteBindingJSONParams `json:"delete_binding,omitempty"` 166 | GeneratePlanSchemas GeneratePlanSchemasJSONParams `json:"generate_plan_schemas,omitempty"` 167 | TextOutput bool `json:"-"` 168 | } 169 | 170 | type ( 171 | ODBManagedSecrets map[string]interface{} 172 | BOSHConfigs map[string]string 173 | ) 174 | 175 | type GenerateManifestOutput struct { 176 | Manifest bosh.BoshManifest `json:"manifest"` 177 | ODBManagedSecrets ODBManagedSecrets `json:"secrets"` 178 | Configs BOSHConfigs `json:"configs"` 179 | Labels map[string]any `json:"labels,omitempty"` 180 | } 181 | 182 | type MarshalledGenerateManifest struct { 183 | Manifest string `json:"manifest"` 184 | ODBManagedSecrets ODBManagedSecrets `json:"secrets"` 185 | Configs BOSHConfigs `json:"configs"` 186 | Labels map[string]any `json:"labels,omitempty"` 187 | } 188 | 189 | const ( 190 | ErrorExitCode = 1 191 | NotImplementedExitCode = 10 192 | BindingNotFoundErrorExitCode = 41 193 | AppGuidNotProvidedErrorExitCode = 42 194 | BindingAlreadyExistsErrorExitCode = 49 195 | 196 | ODBSecretPrefix = "odb_secret" 197 | ) 198 | 199 | type BindingAlreadyExistsError struct { 200 | error 201 | } 202 | 203 | type AppGuidNotProvidedError struct { 204 | error 205 | } 206 | 207 | type BindingNotFoundError struct { 208 | error 209 | } 210 | 211 | type Action interface { 212 | IsImplemented() bool 213 | ParseArgs(io.Reader, []string) (InputParams, error) 214 | Execute(InputParams, io.Writer) error 215 | } 216 | 217 | func NewBindingAlreadyExistsError(err error) BindingAlreadyExistsError { 218 | return BindingAlreadyExistsError{error: fmt.Errorf("binding already exists: %s", err)} 219 | } 220 | 221 | func NewAppGuidNotProvidedError(err error) AppGuidNotProvidedError { 222 | return AppGuidNotProvidedError{error: fmt.Errorf("app GUID not provided: %s", err)} 223 | } 224 | 225 | func NewBindingNotFoundError(err error) BindingNotFoundError { 226 | return BindingNotFoundError{error: fmt.Errorf("binding not found: %s", err)} 227 | } 228 | 229 | type RequestParameters map[string]interface{} 230 | 231 | func (s RequestParameters) ArbitraryParams() map[string]interface{} { 232 | if s["parameters"] == nil { 233 | return map[string]interface{}{} 234 | } 235 | return s["parameters"].(map[string]interface{}) 236 | } 237 | 238 | func (s RequestParameters) ArbitraryContext() map[string]interface{} { 239 | if s["context"] == nil { 240 | return map[string]interface{}{} 241 | } 242 | return s["context"].(map[string]interface{}) 243 | } 244 | 245 | func (s RequestParameters) Platform() string { 246 | context := s.ArbitraryContext() 247 | platform := context["platform"] 248 | platformStr, _ := platform.(string) 249 | return platformStr 250 | } 251 | 252 | func (s RequestParameters) BindResource() domain.BindResource { 253 | marshalledParams, _ := json.Marshal(s["bind_resource"]) 254 | res := domain.BindResource{} 255 | json.Unmarshal(marshalledParams, &res) 256 | return res 257 | } 258 | 259 | var validate *validator.Validate 260 | 261 | func init() { 262 | validate = validator.New() 263 | } 264 | 265 | type ServiceRelease struct { 266 | Name string `json:"name" validate:"required"` 267 | Version string `json:"version" validate:"required"` 268 | Jobs []string `json:"jobs" validate:"required,min=1"` 269 | } 270 | 271 | type ServiceReleases []ServiceRelease 272 | 273 | type ServiceDeployment struct { 274 | DeploymentName string `json:"deployment_name" validate:"required"` 275 | Releases ServiceReleases `json:"releases" validate:"required"` 276 | Stemcells []Stemcell `json:"stemcells" validate:"required"` 277 | } 278 | 279 | func (r ServiceReleases) Validate() error { 280 | if len(r) < 1 { 281 | return errors.New("no releases specified") 282 | } 283 | 284 | for _, serviceRelease := range r { 285 | if err := validate.Struct(serviceRelease); err != nil { 286 | return err 287 | } 288 | } 289 | 290 | return nil 291 | } 292 | 293 | type Stemcell struct { 294 | Name string `json:"stemcell_name,omitempty"` 295 | OS string `json:"stemcell_os" validate:"required"` 296 | Version string `json:"stemcell_version" validate:"required"` 297 | } 298 | 299 | func (s ServiceDeployment) Validate() error { 300 | return validate.Struct(s) 301 | } 302 | 303 | type Properties map[string]interface{} 304 | 305 | type Plan struct { 306 | Properties Properties `json:"properties"` 307 | LifecycleErrands LifecycleErrands `json:"lifecycle_errands,omitempty" yaml:"lifecycle_errands"` 308 | InstanceGroups []InstanceGroup `json:"instance_groups" validate:"required,dive" yaml:"instance_groups"` 309 | Update *Update `json:"update,omitempty"` 310 | } 311 | 312 | func (p Plan) Validate() error { 313 | return validate.Struct(p) 314 | } 315 | 316 | type VMExtensions []string 317 | 318 | func (e *VMExtensions) UnmarshalYAML(unmarshal func(interface{}) error) error { 319 | var ext []string 320 | 321 | if err := unmarshal(&ext); err != nil { 322 | return err 323 | } 324 | 325 | for _, s := range ext { 326 | if s != "" { 327 | *e = append(*e, s) 328 | } 329 | } 330 | 331 | return nil 332 | } 333 | 334 | type LifecycleErrands struct { 335 | PostDeploy []Errand `json:"post_deploy,omitempty" yaml:"post_deploy,omitempty"` 336 | PreDelete []Errand `json:"pre_delete,omitempty" yaml:"pre_delete,omitempty"` 337 | } 338 | 339 | type Errand struct { 340 | Name string `json:"name,omitempty"` 341 | Instances []string `json:"instances,omitempty"` 342 | } 343 | 344 | type InstanceGroup struct { 345 | Name string `json:"name" validate:"required"` 346 | VMType string `yaml:"vm_type" json:"vm_type" validate:"required"` 347 | VMExtensions VMExtensions `yaml:"vm_extensions,omitempty" json:"vm_extensions,omitempty"` 348 | PersistentDiskType string `yaml:"persistent_disk_type,omitempty" json:"persistent_disk_type,omitempty"` 349 | Instances int `json:"instances" validate:"min=1"` 350 | Networks []string `json:"networks" validate:"required"` 351 | AZs []string `json:"azs" validate:"required,min=1"` 352 | Lifecycle string `yaml:"lifecycle,omitempty" json:"lifecycle,omitempty"` 353 | MigratedFrom []Migration `yaml:"migrated_from,omitempty" json:"migrated_from,omitempty"` 354 | } 355 | 356 | func (i *InstanceGroup) UnmarshalYAML(unmarshal func(interface{}) error) error { 357 | type aux InstanceGroup 358 | 359 | var tmp aux 360 | if err := unmarshal(&tmp); err != nil { 361 | return err 362 | } 363 | 364 | if tmp.VMExtensions == nil { 365 | tmp.VMExtensions = VMExtensions{} 366 | } 367 | 368 | *i = InstanceGroup(tmp) 369 | 370 | return nil 371 | } 372 | 373 | type Migration struct { 374 | Name string `yaml:"name" json:"name"` 375 | } 376 | 377 | type Update struct { 378 | Canaries int `json:"canaries" yaml:"canaries"` 379 | CanaryWatchTime string `json:"canary_watch_time" yaml:"canary_watch_time"` 380 | UpdateWatchTime string `json:"update_watch_time" yaml:"update_watch_time"` 381 | MaxInFlight bosh.MaxInFlightValue `json:"max_in_flight," yaml:"max_in_flight"` 382 | Serial *bool `json:"serial,omitempty" yaml:"serial,omitempty"` 383 | InitialDeployAZUpdateStrategy bosh.UpdateStrategy `json:"initial_deploy_az_update_strategy,omitempty" yaml:"initial_deploy_az_update_strategy,omitempty"` 384 | } 385 | 386 | type updateAlias Update 387 | 388 | func (u *Update) UnmarshalJSON(data []byte) error { 389 | decoder := json.NewDecoder(bytes.NewReader(data)) 390 | decoder.UseNumber() 391 | err := decoder.Decode((*updateAlias)(u)) 392 | if err != nil { 393 | return err 394 | } 395 | 396 | v, ok := u.MaxInFlight.(json.Number) 397 | if ok { 398 | int64v, err := v.Int64() 399 | if err == nil { 400 | u.MaxInFlight = int(int64v) 401 | } 402 | } 403 | 404 | return bosh.ValidateMaxInFlight(u.MaxInFlight) 405 | } 406 | 407 | func (u *Update) MarshalJSON() ([]byte, error) { 408 | if u.MaxInFlight == nil { 409 | u.MaxInFlight = 0 410 | } 411 | 412 | err := bosh.ValidateMaxInFlight(u.MaxInFlight) 413 | if err != nil { 414 | return []byte{}, err 415 | } 416 | 417 | return json.Marshal((*updateAlias)(u)) 418 | } 419 | 420 | func (u *Update) UnmarshalYAML(unmarshal func(interface{}) error) error { 421 | err := unmarshal((*updateAlias)(u)) 422 | if err != nil { 423 | return err 424 | } 425 | 426 | return bosh.ValidateMaxInFlight(u.MaxInFlight) 427 | } 428 | 429 | func (u *Update) MarshalYAML() (interface{}, error) { 430 | err := bosh.ValidateMaxInFlight(u.MaxInFlight) 431 | if err != nil { 432 | return []byte{}, err 433 | } 434 | return (*updateAlias)(u), nil 435 | } 436 | 437 | type Binding struct { 438 | Credentials map[string]interface{} `json:"credentials"` 439 | SyslogDrainURL string `json:"syslog_drain_url,omitempty"` 440 | RouteServiceURL string `json:"route_service_url,omitempty"` 441 | BackupAgentURL string `json:"backup_agent_url,omitempty"` 442 | } 443 | 444 | type MissingArgsError struct { 445 | error 446 | } 447 | 448 | func NewMissingArgsError(e string) MissingArgsError { 449 | return MissingArgsError{errors.New(e)} 450 | } 451 | -------------------------------------------------------------------------------- /serviceadapter/generate_manifest_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. 2 | 3 | // This program and the accompanying materials are made available under 4 | // the terms of the under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package serviceadapter_test 17 | 18 | import ( 19 | "bytes" 20 | "encoding/json" 21 | "errors" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | "github.com/onsi/gomega/gbytes" 26 | 27 | "github.com/pivotal-cf/on-demand-services-sdk/bosh" 28 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter" 29 | "github.com/pivotal-cf/on-demand-services-sdk/serviceadapter/fakes" 30 | ) 31 | 32 | var _ = Describe("GenerateManifest", func() { 33 | var ( 34 | fakeManifestGenerator *fakes.FakeManifestGenerator 35 | serviceDeployment serviceadapter.ServiceDeployment 36 | requestParams serviceadapter.RequestParameters 37 | plan serviceadapter.Plan 38 | previousPlan serviceadapter.Plan 39 | previousManifest bosh.BoshManifest 40 | secretsMap serviceadapter.ManifestSecrets 41 | previousBoshConfigs serviceadapter.BOSHConfigs 42 | expectedInputParams serviceadapter.InputParams 43 | action *serviceadapter.GenerateManifestAction 44 | outputBuffer *gbytes.Buffer 45 | ) 46 | 47 | BeforeEach(func() { 48 | fakeManifestGenerator = new(fakes.FakeManifestGenerator) 49 | serviceDeployment = defaultServiceDeployment() 50 | requestParams = defaultRequestParams() 51 | plan = defaultPlan() 52 | previousPlan = defaultPreviousPlan() 53 | previousManifest = defaultManifest() 54 | secretsMap = serviceadapter.ManifestSecrets{ 55 | "foo": "b4r", 56 | } 57 | previousBoshConfigs = defaultPreviousBoshConfigs() 58 | outputBuffer = gbytes.NewBuffer() 59 | 60 | expectedInputParams = serviceadapter.InputParams{ 61 | GenerateManifest: serviceadapter.GenerateManifestJSONParams{ 62 | ServiceDeployment: toJson(serviceDeployment), 63 | Plan: toJson(plan), 64 | PreviousPlan: toJson(previousPlan), 65 | RequestParameters: toJson(requestParams), 66 | PreviousManifest: toYaml(previousManifest), 67 | }, 68 | } 69 | 70 | action = serviceadapter.NewGenerateManifestAction(fakeManifestGenerator) 71 | }) 72 | 73 | Describe("IsImplemented", func() { 74 | It("returns true if implemented", func() { 75 | g := serviceadapter.NewGenerateManifestAction(fakeManifestGenerator) 76 | Expect(g.IsImplemented()).To(BeTrue()) 77 | }) 78 | 79 | It("returns false if not implemented", func() { 80 | g := serviceadapter.NewGenerateManifestAction(nil) 81 | Expect(g.IsImplemented()).To(BeFalse()) 82 | }) 83 | }) 84 | 85 | Describe("ParseArgs", func() { 86 | When("giving arguments in stdin", func() { 87 | It("can parse arguments from stdin", func() { 88 | expectedInputParams.GenerateManifest.PreviousSecrets = toJson(secretsMap) 89 | expectedInputParams.GenerateManifest.PreviousConfigs = toJson(previousBoshConfigs) 90 | expectedInputParams.GenerateManifest.ServiceInstanceUAAClient = toJson(map[string]string{ 91 | "some": "bar", 92 | "client": "secret", 93 | }) 94 | 95 | input := bytes.NewBuffer([]byte(toJson(expectedInputParams))) 96 | actualInputParams, err := action.ParseArgs(input, []string{}) 97 | 98 | Expect(err).NotTo(HaveOccurred()) 99 | Expect(actualInputParams).To(Equal(expectedInputParams)) 100 | }) 101 | 102 | It("returns an error when cannot read from input buffer", func() { 103 | fakeReader := new(FakeReader) 104 | _, err := action.ParseArgs(fakeReader, []string{}) 105 | Expect(err).To(BeACLIError(1, "error reading input params JSON")) 106 | }) 107 | 108 | It("returns an error when cannot unmarshal from input buffer", func() { 109 | input := bytes.NewBuffer([]byte("not-valid-json")) 110 | _, err := action.ParseArgs(input, []string{}) 111 | Expect(err).To(BeACLIError(1, "error unmarshalling input params JSON")) 112 | }) 113 | 114 | It("returns an error when input buffer is empty", func() { 115 | input := bytes.NewBuffer([]byte{}) 116 | _, err := action.ParseArgs(input, []string{}) 117 | Expect(err).To(BeACLIError(1, "expecting parameters to be passed via stdin")) 118 | }) 119 | }) 120 | 121 | When("given positional arguments", func() { 122 | var emptyBuffer *bytes.Buffer 123 | 124 | BeforeEach(func() { 125 | emptyBuffer = bytes.NewBuffer(nil) 126 | }) 127 | 128 | It("can parse positional arguments", func() { 129 | positionalArgs := []string{ 130 | expectedInputParams.GenerateManifest.ServiceDeployment, 131 | expectedInputParams.GenerateManifest.Plan, 132 | expectedInputParams.GenerateManifest.RequestParameters, 133 | expectedInputParams.GenerateManifest.PreviousManifest, 134 | expectedInputParams.GenerateManifest.PreviousPlan, 135 | } 136 | 137 | actualInputParams, err := action.ParseArgs(emptyBuffer, positionalArgs) 138 | Expect(err).NotTo(HaveOccurred()) 139 | expectedInputParams.TextOutput = true 140 | Expect(actualInputParams).To(Equal(expectedInputParams)) 141 | }) 142 | 143 | It("returns an error when required arguments are not passed in", func() { 144 | _, err := action.ParseArgs(emptyBuffer, []string{"foo"}) 145 | Expect(err).To(HaveOccurred()) 146 | Expect(err).To(BeAssignableToTypeOf(serviceadapter.MissingArgsError{})) 147 | Expect(err).To(MatchError(ContainSubstring(" "))) 148 | }) 149 | }) 150 | }) 151 | 152 | Describe("Execute", func() { 153 | It("calls the supplied handler passing args through", func() { 154 | manifest := bosh.BoshManifest{Name: "bill"} 155 | fakeManifestGenerator.GenerateManifestReturns(serviceadapter.GenerateManifestOutput{ 156 | Manifest: manifest, 157 | ODBManagedSecrets: serviceadapter.ODBManagedSecrets{}, 158 | Configs: serviceadapter.BOSHConfigs{}, 159 | }, nil) 160 | 161 | expectedInputParams.GenerateManifest.PreviousSecrets = toJson(secretsMap) 162 | expectedInputParams.GenerateManifest.PreviousConfigs = toJson(previousBoshConfigs) 163 | client := serviceadapter.ServiceInstanceUAAClient{ 164 | Authorities: "foo", 165 | AuthorizedGrantTypes: "bar", 166 | ClientID: "baz", 167 | ClientSecret: "secret", 168 | Name: "a-name", 169 | ResourceIDs: "resource-id1,resourceid2", 170 | Scopes: "master-of-universe,admin", 171 | } 172 | expectedInputParams.GenerateManifest.ServiceInstanceUAAClient = toJson(client) 173 | 174 | err := action.Execute(expectedInputParams, outputBuffer) 175 | 176 | Expect(err).NotTo(HaveOccurred()) 177 | 178 | Expect(fakeManifestGenerator.GenerateManifestCallCount()).To(Equal(1)) 179 | actualParams := fakeManifestGenerator.GenerateManifestArgsForCall(0) 180 | 181 | Expect(actualParams.ServiceDeployment).To(Equal(serviceDeployment)) 182 | Expect(actualParams.Plan).To(Equal(plan)) 183 | Expect(actualParams.RequestParams).To(Equal(requestParams)) 184 | Expect(actualParams.PreviousManifest).To(Equal(&previousManifest)) 185 | Expect(actualParams.PreviousPlan).To(Equal(&previousPlan)) 186 | Expect(actualParams.PreviousSecrets).To(Equal(secretsMap)) 187 | Expect(actualParams.PreviousConfigs).To(Equal(previousBoshConfigs)) 188 | Expect(*actualParams.ServiceInstanceUAAClient).To(Equal(client)) 189 | 190 | var output serviceadapter.MarshalledGenerateManifest 191 | Expect(json.Unmarshal(outputBuffer.Contents(), &output)).To(Succeed()) 192 | Expect(output.Manifest).To(Equal(toYaml(manifest))) 193 | }) 194 | 195 | It("pass nil when service client is not set", func() { 196 | manifest := bosh.BoshManifest{Name: "bill"} 197 | fakeManifestGenerator.GenerateManifestReturns(serviceadapter.GenerateManifestOutput{ 198 | Manifest: manifest, 199 | ODBManagedSecrets: serviceadapter.ODBManagedSecrets{}, 200 | Configs: serviceadapter.BOSHConfigs{}, 201 | }, nil) 202 | 203 | err := action.Execute(expectedInputParams, outputBuffer) 204 | 205 | Expect(err).NotTo(HaveOccurred()) 206 | 207 | Expect(fakeManifestGenerator.GenerateManifestCallCount()).To(Equal(1)) 208 | actualParams := fakeManifestGenerator.GenerateManifestArgsForCall(0) 209 | 210 | Expect(actualParams.ServiceInstanceUAAClient).To(BeNil()) 211 | }) 212 | 213 | It("returns the bosh configs", func() { 214 | manifest := bosh.BoshManifest{Name: "bill"} 215 | expectedBoshConfigs := serviceadapter.BOSHConfigs{ 216 | "foo": "{}", 217 | } 218 | fakeManifestGenerator.GenerateManifestReturns(serviceadapter.GenerateManifestOutput{ 219 | Manifest: manifest, 220 | Configs: expectedBoshConfigs, 221 | }, nil) 222 | 223 | err := action.Execute(expectedInputParams, outputBuffer) 224 | Expect(err).NotTo(HaveOccurred()) 225 | 226 | Expect(fakeManifestGenerator.GenerateManifestCallCount()).To(Equal(1)) 227 | 228 | var output serviceadapter.MarshalledGenerateManifest 229 | Expect(json.Unmarshal(outputBuffer.Contents(), &output)).To(Succeed()) 230 | Expect(output.Configs).To(Equal(expectedBoshConfigs)) 231 | }) 232 | 233 | It("returns the labels", func() { 234 | manifest := bosh.BoshManifest{Name: "bill"} 235 | expectedLabels := map[string]any{ 236 | "foo": "bar", 237 | } 238 | fakeManifestGenerator.GenerateManifestReturns(serviceadapter.GenerateManifestOutput{ 239 | Manifest: manifest, 240 | Labels: expectedLabels, 241 | }, nil) 242 | 243 | err := action.Execute(expectedInputParams, outputBuffer) 244 | Expect(err).NotTo(HaveOccurred()) 245 | 246 | Expect(fakeManifestGenerator.GenerateManifestCallCount()).To(Equal(1)) 247 | 248 | var output serviceadapter.MarshalledGenerateManifest 249 | Expect(json.Unmarshal(outputBuffer.Contents(), &output)).To(Succeed()) 250 | Expect(output.Labels).To(Equal(expectedLabels)) 251 | }) 252 | 253 | When("not outputting json", func() { 254 | It("outputs the manifest as text", func() { 255 | manifest := bosh.BoshManifest{Name: "bill"} 256 | fakeManifestGenerator.GenerateManifestReturns(serviceadapter.GenerateManifestOutput{Manifest: manifest}, nil) 257 | 258 | expectedInputParams.TextOutput = true 259 | err := action.Execute(expectedInputParams, outputBuffer) 260 | 261 | Expect(err).NotTo(HaveOccurred()) 262 | Expect(string(outputBuffer.Contents())).To(Equal(toYaml(manifest))) 263 | }) 264 | }) 265 | 266 | Context("error handling", func() { 267 | It("returns an error when service deployment cannot be unmarshalled", func() { 268 | expectedInputParams.GenerateManifest.ServiceDeployment = "not-json" 269 | err := action.Execute(expectedInputParams, outputBuffer) 270 | Expect(err).To(MatchError(ContainSubstring("unmarshalling service deployment"))) 271 | }) 272 | 273 | It("returns an error when service deployment is invalid", func() { 274 | expectedInputParams.GenerateManifest.ServiceDeployment = "{}" 275 | err := action.Execute(expectedInputParams, outputBuffer) 276 | Expect(err).To(MatchError(ContainSubstring("validating service deployment"))) 277 | }) 278 | 279 | It("returns an error when plan cannot be unmarshalled", func() { 280 | expectedInputParams.GenerateManifest.Plan = "not-json" 281 | err := action.Execute(expectedInputParams, outputBuffer) 282 | Expect(err).To(MatchError(ContainSubstring("unmarshalling service plan"))) 283 | }) 284 | 285 | It("returns an error when plan is invalid", func() { 286 | expectedInputParams.GenerateManifest.Plan = "{}" 287 | err := action.Execute(expectedInputParams, outputBuffer) 288 | Expect(err).To(MatchError(ContainSubstring("validating service plan"))) 289 | }) 290 | 291 | It("returns an error when request params cannot be unmarshalled", func() { 292 | expectedInputParams.GenerateManifest.RequestParameters = "not-json" 293 | err := action.Execute(expectedInputParams, outputBuffer) 294 | Expect(err).To(MatchError(ContainSubstring("unmarshalling requestParams"))) 295 | }) 296 | 297 | It("returns an error when previous manifest cannot be unmarshalled", func() { 298 | expectedInputParams.GenerateManifest.PreviousManifest = "not-yaml" 299 | err := action.Execute(expectedInputParams, outputBuffer) 300 | Expect(err).To(MatchError(ContainSubstring("unmarshalling previous manifest"))) 301 | }) 302 | 303 | It("returns an error when previous plan cannot be unmarshalled", func() { 304 | expectedInputParams.GenerateManifest.PreviousPlan = "not-json" 305 | err := action.Execute(expectedInputParams, outputBuffer) 306 | Expect(err).To(MatchError(ContainSubstring("unmarshalling previous service plan"))) 307 | }) 308 | 309 | It("returns an error when previous plan is invalid", func() { 310 | expectedInputParams.GenerateManifest.PreviousPlan = "{}" 311 | err := action.Execute(expectedInputParams, outputBuffer) 312 | Expect(err).To(MatchError(ContainSubstring("validating previous service plan"))) 313 | }) 314 | 315 | It("returns an error when previous configs is invalid", func() { 316 | expectedInputParams.GenerateManifest.PreviousConfigs = "not-json" 317 | err := action.Execute(expectedInputParams, outputBuffer) 318 | Expect(err).To(MatchError(ContainSubstring("unmarshalling previous configs"))) 319 | }) 320 | 321 | It("returns an error when the service instance client is invalid", func() { 322 | expectedInputParams.GenerateManifest.ServiceInstanceUAAClient = "not-json" 323 | err := action.Execute(expectedInputParams, outputBuffer) 324 | Expect(err).To(MatchError(ContainSubstring("unmarshalling service instance client"))) 325 | }) 326 | 327 | It("returns an error when manifestGenerator returns an error", func() { 328 | fakeManifestGenerator.GenerateManifestReturns(serviceadapter.GenerateManifestOutput{}, errors.New("something went wrong")) 329 | err := action.Execute(expectedInputParams, outputBuffer) 330 | Expect(err).To(BeACLIError(1, "something went wrong")) 331 | }) 332 | 333 | It("returns an error when the manifest is invalid", func() { 334 | var invalidYAML struct { 335 | A int 336 | B int `yaml:"a"` 337 | } 338 | 339 | manifest := bosh.BoshManifest{ 340 | Tags: map[string]interface{}{"foo": invalidYAML}, 341 | } 342 | 343 | expectedInputParams.TextOutput = true 344 | fakeManifestGenerator.GenerateManifestReturns(serviceadapter.GenerateManifestOutput{Manifest: manifest}, nil) 345 | err := action.Execute(expectedInputParams, outputBuffer) 346 | Expect(err).To(MatchError(ContainSubstring("error marshalling bosh manifest"))) 347 | }) 348 | 349 | It("returns an error when the generated output cannot be marshalled", func() { 350 | manifest := bosh.BoshManifest{ 351 | Tags: map[string]interface{}{"foo": make(chan int)}, 352 | } 353 | 354 | fakeManifestGenerator.GenerateManifestReturns(serviceadapter.GenerateManifestOutput{Manifest: manifest}, nil) 355 | err := action.Execute(expectedInputParams, outputBuffer) 356 | Expect(err).To(MatchError(ContainSubstring("error marshalling bosh manifest"))) 357 | }) 358 | }) 359 | }) 360 | }) 361 | --------------------------------------------------------------------------------