├── .cfignore ├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── labeler.yml ├── pull_request_template.md ├── release.yml └── workflows │ ├── codeql.yml │ ├── conventional-commit.yml │ ├── focused-test.yml │ ├── labeler.yml │ └── run-test.yml ├── .gitignore ├── CODE-OF-CONDUCT.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── acceptance-tests ├── README.md ├── acceptance_suite_test.go ├── apps │ ├── mongodbapp │ │ ├── go.mod │ │ ├── go.sum │ │ ├── internal │ │ │ ├── app │ │ │ │ ├── app.go │ │ │ │ ├── fetch_document.go │ │ │ │ ├── list_collections.go │ │ │ │ ├── list_databases.go │ │ │ │ └── store_document.go │ │ │ └── credentials │ │ │ │ └── credentials.go │ │ └── main.go │ ├── mssqlapp │ │ ├── go.mod │ │ ├── go.sum │ │ ├── internal │ │ │ ├── app │ │ │ │ ├── app.go │ │ │ │ ├── create_schema.go │ │ │ │ ├── drop_schema.go │ │ │ │ ├── fill_db.go │ │ │ │ ├── get.go │ │ │ │ └── set.go │ │ │ └── credentials │ │ │ │ ├── config.go │ │ │ │ └── credentials.go │ │ └── main.go │ ├── postgresqlapp │ │ ├── go.mod │ │ ├── go.sum │ │ ├── internal │ │ │ ├── app │ │ │ │ ├── app.go │ │ │ │ ├── create_schema.go │ │ │ │ ├── drop_schema.go │ │ │ │ ├── get.go │ │ │ │ └── set.go │ │ │ └── credentials │ │ │ │ └── credentials.go │ │ └── main.go │ └── redisapp │ │ ├── go.mod │ │ ├── go.sum │ │ ├── internal │ │ ├── app │ │ │ ├── app.go │ │ │ ├── get.go │ │ │ └── set.go │ │ └── credentials │ │ │ └── credentials.go │ │ └── main.go ├── ginkgo ├── helpers │ ├── apps │ │ ├── app.go │ │ ├── delete.go │ │ ├── http.go │ │ ├── log.go │ │ ├── prebuild.go │ │ ├── push.go │ │ ├── restage.go │ │ ├── restart.go │ │ ├── setenv.go │ │ ├── start.go │ │ ├── testapps.go │ │ └── url.go │ ├── az │ │ └── start.go │ ├── bindings │ │ ├── bind.go │ │ ├── credential.go │ │ └── unbind.go │ ├── brokerpaks │ │ ├── brokerpaks.go │ │ ├── client.go │ │ ├── prepare │ │ │ └── prepare.go │ │ ├── versions.go │ │ └── versions │ │ │ └── versions.go │ ├── brokers │ │ ├── broker.go │ │ ├── create.go │ │ ├── default.go │ │ ├── delete.go │ │ ├── encryption.go │ │ ├── env.go │ │ ├── envrc.go │ │ ├── manifest.go │ │ └── update.go │ ├── cf │ │ ├── run.go │ │ └── start.go │ ├── environment │ │ └── metadata.go │ ├── lookupplan │ │ └── lookup_plan.go │ ├── matchers │ │ └── credhub_ref_matcher.go │ ├── mssqlserver │ │ └── mssqlserver.go │ ├── plans │ │ └── plans.go │ ├── random │ │ ├── hex.go │ │ ├── name.go │ │ ├── password.go │ │ └── random.go │ ├── servicekeys │ │ ├── create.go │ │ └── delete.go │ ├── services │ │ ├── bind.go │ │ ├── create.go │ │ ├── createservicekey.go │ │ ├── delete.go │ │ ├── guid.go │ │ ├── service_test.go │ │ ├── services.go │ │ ├── update.go │ │ └── upgrade.go │ └── testpath │ │ ├── brokerpak_file.go │ │ ├── brokerpak_root.go │ │ └── exists.go ├── mongodb_test.go ├── mssql_fog_existing_test.go ├── mssql_server_and_db_test.go ├── mssql_server_pair_and_fog_db_test.go ├── passwordrotation_test.go ├── redis_test.go └── upgrade │ ├── .gitignore │ ├── README.md │ ├── update_and_upgrade_mongodb_test.go │ ├── update_and_upgrade_mssql_db_failover_group_existing_test.go │ ├── update_and_upgrade_mssql_db_failover_group_test.go │ ├── update_and_upgrade_mssql_db_test.go │ ├── update_and_upgrade_redis_test.go │ └── upgrade_suite_test.go ├── azure-mongodb.yml ├── azure-mssql-db-failover-group.yml ├── azure-mssql-db.yml ├── azure-mssql-fog-run-failover.yml ├── azure-redis.yml ├── cf-manifest.yml ├── dependency-manifest.yml ├── docs ├── README.md ├── azure-example-configs.md ├── azure-installation.md ├── azure-mysql-statedb.md ├── billing.md ├── configuration.md └── service-offerings.md ├── go.mod ├── go.sum ├── integration-tests ├── integration_test_suite_test.go ├── mongodb_test.go ├── mssql_db_failover_group_test.go ├── mssql_db_test.go └── redis_test.go ├── manifest.yml ├── providers └── terraform-provider-csbmssqldbrunfailover │ ├── Makefile │ ├── connector │ └── connector.go │ ├── csbmssqldbrunfailover │ ├── config.go │ ├── csbmssqldbrunfailover_suite_test.go │ ├── provider.go │ ├── provider_test.go │ ├── resource_run_failover.go │ └── resource_run_failover_test.go │ ├── example.tf │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── testhelpers │ ├── failover.go │ └── random.go ├── scripts └── push-broker.sh ├── service-images └── csb.png ├── staticcheck.conf ├── terraform-tests ├── README.md ├── cosmosdb_mongo_test.go ├── helpers │ ├── resource_changes.go │ └── terraform.go ├── redis_test.go └── terraform_tests_suite_test.go ├── terraform ├── azure-mongodb │ ├── bind │ │ └── noop.tf │ └── provision │ │ ├── data.tf │ │ ├── main.tf │ │ ├── moved-azure-mongodb.tf │ │ ├── outputs.tf │ │ ├── provider.tf │ │ ├── variables.tf │ │ └── versions.tf ├── azure-mssql-db-failover │ ├── azure-provider.tf │ ├── azure-versions.tf │ ├── mssql-db-fog-data.tf │ ├── mssql-db-fog-main.tf │ ├── mssql-db-fog-outputs.tf │ ├── mssql-db-fog-variables.tf │ └── run-failover │ │ ├── run-failover-providers.tf │ │ ├── run-failover-versions.tf │ │ ├── run-failover.tf │ │ └── variables.tf ├── azure-mssql-db │ ├── bind │ │ ├── mssql-bind-data.tf │ │ ├── mssql-bind-main.tf │ │ ├── mssql-bind-outputs.tf │ │ ├── mssql-bind-providers.tf │ │ ├── mssql-bind-variables.tf │ │ └── mssql-bind-versions.tf │ └── provision │ │ ├── mssql-db-data.tf │ │ ├── mssql-db-main.tf │ │ ├── mssql-db-outputs.tf │ │ ├── mssql-db-providers.tf │ │ ├── mssql-db-variables.tf │ │ └── mssql-db-versions.tf └── azure-redis │ ├── bind │ └── noop.tf │ └── provision │ ├── data.tf │ ├── main.tf │ ├── moved-azure-redis.tf │ ├── outputs.tf │ ├── provider.tf │ ├── variables.tf │ └── versions.tf └── tools └── tools.go /.cfignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .idea 4 | acceptance-tests 5 | docs 6 | integration-tests 7 | providers 8 | scripts 9 | terraform 10 | tools 11 | *.yml 12 | *.md 13 | go.* 14 | Makefile 15 | CODEOWNERS 16 | LICENSE 17 | NOTICE 18 | staticcheck.conf 19 | terraform-tests 20 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export PAK_BUILD_CACHE_PATH=/tmp/.pak-cache 2 | export CSB_DISABLE_TF_UPGRADE_PROVIDER_RENAMES=false 3 | export TERRAFORM_UPGRADES_ENABLED=true 4 | export BROKERPAK_UPDATES_ENABLED=true 5 | export GSB_COMPATIBILITY_ENABLE_GCP_DEPRECATED_SERVICES=true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "19:30" 8 | - package-ecosystem: gomod 9 | directory: "/providers/terraform-provider-csbmssqldbrunfailover" 10 | schedule: 11 | interval: "weekly" 12 | day: "saturday" 13 | groups: 14 | azure-sdk-for-go: 15 | patterns: 16 | - "github.com/Azure/azure-sdk-for-go/*" 17 | - package-ecosystem: gomod 18 | directory: "/acceptance-tests/apps/mongodbapp" 19 | schedule: 20 | interval: "weekly" 21 | day: "saturday" 22 | labels: 23 | - "test-dependencies" 24 | - package-ecosystem: gomod 25 | directory: "/acceptance-tests/apps/postgresqlapp" 26 | schedule: 27 | interval: "weekly" 28 | day: "saturday" 29 | labels: 30 | - "test-dependencies" 31 | - package-ecosystem: gomod 32 | directory: "/acceptance-tests/apps/mssqlapp" 33 | schedule: 34 | interval: "weekly" 35 | day: "saturday" 36 | labels: 37 | - "test-dependencies" 38 | - package-ecosystem: gomod 39 | directory: "/acceptance-tests/apps/redisapp" 40 | schedule: 41 | interval: "weekly" 42 | day: "saturday" 43 | labels: 44 | - "test-dependencies" 45 | - package-ecosystem: "github-actions" 46 | directory: "/" 47 | schedule: 48 | interval: "daily" 49 | time: "00:00" 50 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'breaking-change' label to any PR where the head branch name starts with `!feat` 2 | breaking-change: 3 | - head-branch: ['^feat!'] 4 | 5 | new-feature: 6 | - head-branch: ['^feat'] 7 | 8 | bug-fix: 9 | - head-branch: ['^fix'] 10 | 11 | chore: 12 | - head-branch: ['^chore'] -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Checklist: 2 | 3 | * [ ] Have you added Release Notes in the docs repositories? 4 | * [ ] Have you followed the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary)? 5 | 6 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - test-dependencies 5 | categories: 6 | - title: Breaking Changes 7 | labels: 8 | - breaking-change 9 | - title: New Features 10 | labels: 11 | - new-feature 12 | - title: Bug fixes 13 | labels: 14 | - bug-fix 15 | - title: Dependency updates 16 | labels: 17 | - dependencies 18 | - title: Others 19 | labels: 20 | - chore 21 | - "*" -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ 'main' ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ 'main' ] 9 | schedule: 10 | - cron: '27 19 * * 3' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'go' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | with: 34 | languages: ${{ matrix.language }} 35 | # If you wish to specify custom queries, you can do so here or in a config file. 36 | # By default, queries listed here will override any specified in a config file. 37 | # Prefix the list here with "+" to use these queries and those in the config file. 38 | 39 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 40 | queries: +security-and-quality 41 | 42 | # Install golang used by this project (https://github.com/github/codeql-action/issues/1842) 43 | - name: Set up Go 44 | uses: actions/setup-go@v5 45 | with: 46 | go-version-file: "go.mod" 47 | 48 | - run: make build 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v3 52 | with: 53 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /.github/workflows/conventional-commit.yml: -------------------------------------------------------------------------------- 1 | name: Conventional Commit Check 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | conventional-commit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check 10 | uses: actions/github-script@v7 11 | with: 12 | script: | 13 | const {data: pr} = await github.rest.pulls.get({ 14 | owner: context.repo.owner, 15 | repo: context.repo.repo, 16 | pull_number: context.issue.number, 17 | }) 18 | 19 | const allowed = ["feat\\!", "feat", "fix", "chore", "docs", "build", "test", "revert"] 20 | const re = new RegExp(`^(` + allowed.join('|') + `)(\\(\\w+\\))?: `) 21 | const title = pr['title'] 22 | 23 | if (!re.test(title)) { 24 | throw new Error(`PR title "${title}" does not match conventional commits filter: ${re}`) 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/focused-test.yml: -------------------------------------------------------------------------------- 1 | name: Focused Test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | focused-test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/setup-go@v5 10 | with: 11 | go-version: '1.24.3' 12 | - uses: actions/checkout@v4 13 | - run: go tool ginkgo unfocus && test -z "$(git status -s)" 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | labeler: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/labeler@v5 -------------------------------------------------------------------------------- /.github/workflows/run-test.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 | name: Go test 17 | outputs: 18 | pr_number: ${{ github.event.number }} 19 | steps: 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: '1.24.3' 23 | - uses: actions/checkout@v4 24 | - run: make test 25 | call-dependabot-pr-workflow: 26 | needs: test 27 | if: ${{ success() && github.actor == 'dependabot[bot]' }} 28 | uses: cloudfoundry/cloud-service-broker/.github/workflows/dependabot-test.yml@main 29 | with: 30 | pr_number: ${{ github.event.number }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.brokerpak 2 | providers/terraform-provider-csbsqlserver/cloudfoundry.org 3 | providers/terraform-provider-csbmssqldbrunfailover/cloudfoundry.org 4 | cloud-service-broker 5 | .idea 6 | csbmssqldbrunfailover.test 7 | .csb.db 8 | .vscode 9 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in cloud-service-broker project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oss-coc@vmware.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # approvers for the cloud-service-broker project are the ones who can approve and merge PRs according 2 | # to cloudfoundry org. 3 | # See https://github.com/cloudfoundry/community/blob/main/toc/ROLES.md#role-summary for more information. 4 | @cloudfoundry/cloud-service-broker -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CSB Brokerpak for Azure 2 | 3 | The Cloud Service Broker team uses GitHub and accepts contributions via 4 | [pull request](https://help.github.com/articles/using-pull-requests). 5 | 6 | See the [CSB docs](https://github.com/cloudfoundry/cloud-service-broker/tree/main/docs) and [Brokerpak docs](https://github.com/cloudfoundry/csb-brokerpak-azure/tree/main/docs) for design notes and other helpful information on getting started. 7 | 8 | ## Contributor License Agreement 9 | 10 | Follow these steps to make a contribution to any of our open source repositories: 11 | 12 | 1. Ensure that you have signed our CLA Agreement [here](https://www.cloudfoundry.org/community/cla). 13 | 1. Set your name and email (these should match the information on your submitted CLA) 14 | 15 | git config --global user.name "Firstname Lastname" 16 | git config --global user.email "your_email@example.com" 17 | 18 | 1. All contributions must be sent using GitHub pull requests as they create a nice audit trail and structured approach. 19 | The originating github user has to either have a github id on-file with the list of approved users that have signed the CLA or they can be a public "member" of a GitHub organization for a group that has signed the corporate CLA. This enables the corporations to manage their users themselves instead of having to tell us when someone joins/leaves an organization. By removing a user from an organization's GitHub account, their new contributions are no longer approved because they are no longer covered under a CLA. 20 | 21 | If a contribution is deemed to be covered by an existing CLA, then it is analyzed for engineering quality and product fit before merging it. 22 | 23 | If a contribution is not covered by the CLA, then the automated CLA system notifies the submitter politely that we cannot identify their CLA and ask them to sign either an individual or corporate CLA. This happens automatically as a comment on pull requests. 24 | 25 | When the project receives a new CLA, it is recorded in the project records, the CLA is added to the database for the automated system uses, then we manually make the Pull Request as having a CLA on-file. 26 | 27 | ## Contribution Workflow 28 | 29 | 1. Fork the repository 30 | 1. Check out `main` of csb-brokerpak-azure 31 | 1. Create a feature branch (`git checkout -b better_brokerpak`) 32 | 1. Make changes on your branch 33 | 1. Run integration tests (`make run-integraion-tests`) 34 | 1. Make clear commit message using [conventional commits style](https://www.conventionalcommits.org/en/v1.0.0/#summary) 35 | 3. Push to your fork (`git push origin better_brokerpak`) 36 | 4. Submit your PR 37 | 38 | ### PR Considerations 39 | We favor pull requests with very small, single commits with a single purpose. 40 | 41 | Your pull request is much more likely to be accepted if: 42 | * Your pull request includes tests (unit and integration) 43 | * Your pull request is small and focused. 44 | * Your pull request has a clear message that conveys the intent of your change. 45 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 2 | 3 | This project is licensed to you under the Apache License, Version 2.0 (the "License"). 4 | You may not use this project except in compliance with the License. 5 | 6 | This project may include a number of subcomponents with separate copyright notices 7 | and license terms. Your use of these subcomponents is subject to the terms and 8 | conditions of the subcomponent's license, as noted in the LICENSE file. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Brokerpak 2 | 3 | A brokerpak for the [Cloud Service Broker](https://github.com/pivotal/cloud-service-broker) that provides support for Azure services. 4 | 5 | ## Development Requirements 6 | 7 | * Either an up-to-date version of Go or [Docker](https://docs.docker.com/get-docker/) 8 | * make - covers development lifecycle steps 9 | 10 | ## Azure account information 11 | 12 | To provision services, the brokerpak currently requires Azure account values. The brokerpak expects them in environment variables: 13 | 14 | * ARM_SUBSCRIPTION_ID 15 | * ARM_TENANT_ID 16 | * ARM_CLIENT_ID 17 | * ARM_CLIENT_SECRET 18 | 19 | ## Development Tools 20 | 21 | A Makefile supports the full local development lifecycle for the brokerpak. 22 | 23 | The make targets can be run either with Docker or installing the required libraries in the local OS. 24 | 25 | Available make targets can be listed by running `make`. 26 | 27 | ### Running with docker 28 | 29 | 1. Install [Docker](https://docs.docker.com/get-docker/) 30 | 2. Launch an interactive shell into some supported image containing all necessary tools. For example: 31 | ``` 32 | # From the root of this repo run: 33 | docker run -it --rm -v "${PWD}:/repo" --workdir "/repo" --entrypoint "/bin/bash" golang:latest 34 | make 35 | ``` 36 | 37 | ### Running with Go 38 | 39 | 1. Make sure you have the right Go version installed (see `go.mod` file). 40 | 41 | The make targets will build the source using the local go installation. 42 | 43 | ### Other targets 44 | 45 | There is a make target to push the broker and brokerpak into a CloudFoundry foundation. It will be necessary to manually configure a few items for the broker to work. 46 | 47 | - `make push-broker` will `cf push` the broker into CloudFoundry. Requires the `cf` cli to be installed. 48 | 49 | The broker gets pushed into CloudFoundry as *cloud-service-broker-azure* It will be necessary to bind a MySQL database to the broker to provide broker state storage. See [Azure Installation](./docs/azure-installation.md) docs for more info. 50 | 51 | ## Broker 52 | The version of Cloud Service Broker to use with this brokerpak is encoded in the `go.mod` file. 53 | The make targets will use this version by default. 54 | 55 | ## Tests 56 | 57 | ### Example tests 58 | 59 | Services definitions declare examples for each plan they provide. Those examples are then run through the whole cycle of `provision`, `bind`, `unbind`, and `delete` when running 60 | 61 | ``` 62 | terminal 1 63 | >> make run 64 | 65 | terminal 2 66 | >> make run-examples 67 | ``` 68 | 69 | ## Acceptance tests 70 | 71 | See [acceptance tests](acceptance-tests/README.md) 72 | 73 | ## Integration tests 74 | 75 | Integration tests can be run with the following command: 76 | 77 | ```bash 78 | make run-integration-tests 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /acceptance-tests/README.md: -------------------------------------------------------------------------------- 1 | # Acceptance Test Tools and Scripts 2 | 3 | Acceptance tests are run as `cf push`'ed applications that verify connectivity in a real Cloud Foundry environment. 4 | 5 | The main pattern is: 6 | 1. `cf create-service` the service instance to test 7 | 1. `cf bind-service` a writer test app to the provisioned service 8 | 1. `cf bind-service` a reader test app to the provisioned service 9 | 1. Check the binding contains CredHub references and not credentials 10 | 1. Use the writer test app to write some data 11 | 1. Use the reader test app to read back the data and check it is the same 12 | 1. `cf unbind-service` for both apps 13 | 1. `cf delete-service` the tested instance 14 | 15 | ## Running the tests 16 | ### Pre-requisite software 17 | - The [Go Programming language](https://golang.org/) 18 | - The [Cloud Foundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) 19 | - The [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) 20 | 21 | ### Environment variables 22 | - ARM_SUBSCRIPTION_ID 23 | - ARM_CLIENT_SECRET 24 | - ARM_TENANT_ID 25 | - ARM_CLIENT_ID 26 | 27 | ### Environment 28 | - A Cloud Foundry instance logged in and targeted 29 | - The Cloud Service Broker and this brokerpak deployed by running `make push-broker` or equivalent -------------------------------------------------------------------------------- /acceptance-tests/acceptance_suite_test.go: -------------------------------------------------------------------------------- 1 | package acceptance_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "csbbrokerpakazure/acceptance-tests/helpers/environment" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | func TestAcceptanceTests(t *testing.T) { 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "Acceptance Tests Suite") 16 | } 17 | 18 | var ( 19 | metadata environment.Metadata 20 | subscriptionID string 21 | ) 22 | 23 | var _ = BeforeSuite(func() { 24 | metadata = environment.ReadMetadata() 25 | subscriptionID = os.Getenv("ARM_SUBSCRIPTION_ID") 26 | Expect(subscriptionID).NotTo(BeEmpty(), "ARM_SUBSCRIPTION_ID environment variable should not be empty") 27 | Expect(os.Getenv("ARM_TENANT_ID")).NotTo(BeEmpty(), "ARM_TENANT_ID environment variable should not be empty") 28 | Expect(os.Getenv("ARM_CLIENT_ID")).NotTo(BeEmpty(), "ARM_CLIENT_ID environment variable should not be empty") 29 | Expect(os.Getenv("ARM_CLIENT_SECRET")).NotTo(BeEmpty(), "ARM_CLIENT_SECRET environment variable should not be empty") 30 | 31 | _ = os.Setenv("AZURE_SUBSCRIPTION_ID", subscriptionID) 32 | _ = os.Setenv("AZURE_TENANT_ID", os.Getenv("ARM_TENANT_ID")) 33 | _ = os.Setenv("AZURE_CLIENT_ID", os.Getenv("ARM_CLIENT_ID")) 34 | _ = os.Setenv("AZURE_CLIENT_SECRET", os.Getenv("ARM_CLIENT_SECRET")) 35 | }) 36 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mongodbapp/go.mod: -------------------------------------------------------------------------------- 1 | module mongodbapp 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/cloudfoundry-community/go-cfenv v1.18.0 7 | github.com/mitchellh/mapstructure v1.5.0 8 | go.mongodb.org/mongo-driver/v2 v2.2.1 9 | ) 10 | 11 | require ( 12 | github.com/golang/snappy v1.0.0 // indirect 13 | github.com/klauspost/compress v1.16.7 // indirect 14 | github.com/onsi/gomega v1.10.5 // indirect 15 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 16 | github.com/xdg-go/scram v1.1.2 // indirect 17 | github.com/xdg-go/stringprep v1.0.4 // indirect 18 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 19 | golang.org/x/crypto v0.33.0 // indirect 20 | golang.org/x/sync v0.11.0 // indirect 21 | golang.org/x/text v0.22.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mongodbapp/internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "go.mongodb.org/mongo-driver/v2/mongo" 9 | "go.mongodb.org/mongo-driver/v2/mongo/options" 10 | ) 11 | 12 | const ( 13 | documentNameKey = "name" 14 | documentDataKey = "data" 15 | documentTTLKey = "ttl" 16 | ) 17 | 18 | func App(uri string) http.Handler { 19 | client := connect(uri) 20 | 21 | r := http.NewServeMux() 22 | r.HandleFunc("GET /", handleListDatabases(client)) 23 | r.HandleFunc("GET /{database}", handleListCollections(client)) 24 | r.HandleFunc("GET /{database}/{collection}/{document}", handleFetchDocument(client)) 25 | r.HandleFunc("PUT /{database}/{collection}/{document}", handleStoreDocument(client)) 26 | 27 | return r 28 | } 29 | 30 | func connect(uri string) *mongo.Client { 31 | client, err := mongo.Connect(options.Client().ApplyURI(uri)) 32 | if err != nil { 33 | log.Fatalf("error connecting to MongoDB: %s", err) 34 | } 35 | 36 | return client 37 | } 38 | 39 | func fail(w http.ResponseWriter, code int, format string, a ...any) { 40 | msg := fmt.Sprintf(format, a...) 41 | log.Println(msg) 42 | http.Error(w, msg, code) 43 | } 44 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mongodbapp/internal/app/fetch_document.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "go.mongodb.org/mongo-driver/v2/bson" 9 | "go.mongodb.org/mongo-driver/v2/mongo" 10 | ) 11 | 12 | func handleFetchDocument(client *mongo.Client) func(w http.ResponseWriter, r *http.Request) { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | log.Println("Handling fetch.") 15 | 16 | databaseName := r.PathValue("database") 17 | if databaseName == "" { 18 | fail(w, http.StatusBadRequest, "database name must be supplied") 19 | } 20 | collectionName := r.PathValue("collection") 21 | if collectionName == "" { 22 | fail(w, http.StatusBadRequest, "collection name must be supplied") 23 | } 24 | documentName := r.PathValue("document") 25 | if documentName == "" { 26 | fail(w, http.StatusBadRequest, "document name must be supplied") 27 | } 28 | 29 | filter := bson.D{{Key: documentNameKey, Value: documentName}} 30 | result := client.Database(databaseName).Collection(collectionName).FindOne(r.Context(), filter) 31 | if result.Err() != nil { 32 | fail(w, http.StatusNotFound, "error finding document: %s", result.Err()) 33 | return 34 | } 35 | 36 | var receiver bson.D 37 | if err := result.Decode(&receiver); err != nil { 38 | fail(w, http.StatusNotFound, "error decoding document: %s", err) 39 | return 40 | } 41 | 42 | var data any 43 | for _, e := range receiver { 44 | if e.Key == documentDataKey { 45 | data = e.Value 46 | } 47 | } 48 | 49 | if data == nil { 50 | fail(w, http.StatusNotFound, "error find document data: %+v", receiver) 51 | return 52 | } 53 | 54 | w.WriteHeader(http.StatusOK) 55 | w.Header().Set("Content-Type", "text/html") 56 | _, err := w.Write([]byte(fmt.Sprintf("%v", data))) 57 | if err != nil { 58 | log.Printf("Error writing value: %s", err) 59 | return 60 | } 61 | 62 | log.Printf("Data %q retrived from document %q.", data, documentName) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mongodbapp/internal/app/list_collections.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "go.mongodb.org/mongo-driver/v2/bson" 10 | "go.mongodb.org/mongo-driver/v2/mongo" 11 | ) 12 | 13 | func handleListCollections(client *mongo.Client) func(w http.ResponseWriter, r *http.Request) { 14 | return func(w http.ResponseWriter, r *http.Request) { 15 | log.Println("Handling list collections.") 16 | 17 | databaseName := r.PathValue("database") 18 | if databaseName == "" { 19 | fail(w, http.StatusBadRequest, "database name must be supplied") 20 | } 21 | 22 | list, err := client.Database(databaseName).ListCollectionNames(r.Context(), bson.D{}) 23 | if err != nil { 24 | fail(w, http.StatusNotFound, "error listing collections: %s", err) 25 | return 26 | } 27 | 28 | data, err := json.Marshal(list) 29 | if err != nil { 30 | fail(w, http.StatusNotFound, "JSON error: %s", err) 31 | return 32 | } 33 | 34 | w.WriteHeader(http.StatusOK) 35 | w.Header().Set("Content-Type", "text/html") 36 | _, err = w.Write(data) 37 | if err != nil { 38 | log.Printf("Error writing value: %s", err) 39 | return 40 | } 41 | 42 | log.Printf("Listed collections: %s", strings.Join(list, ", ")) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mongodbapp/internal/app/list_databases.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "go.mongodb.org/mongo-driver/v2/bson" 10 | "go.mongodb.org/mongo-driver/v2/mongo" 11 | ) 12 | 13 | func handleListDatabases(client *mongo.Client) func(w http.ResponseWriter, r *http.Request) { 14 | return func(w http.ResponseWriter, r *http.Request) { 15 | log.Println("Handling list database.") 16 | list, err := client.ListDatabaseNames(r.Context(), bson.D{}) 17 | if err != nil { 18 | fail(w, http.StatusNotFound, "error listing databases: %s", err) 19 | return 20 | } 21 | 22 | data, err := json.Marshal(list) 23 | if err != nil { 24 | fail(w, http.StatusNotFound, "JSON error: %s", err) 25 | return 26 | } 27 | 28 | w.WriteHeader(http.StatusOK) 29 | w.Header().Set("Content-Type", "text/html") 30 | _, err = w.Write(data) 31 | if err != nil { 32 | log.Printf("Error writing value: %s", err) 33 | return 34 | } 35 | 36 | log.Printf("Listed database: %s", strings.Join(list, ", ")) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mongodbapp/internal/app/store_document.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "go.mongodb.org/mongo-driver/v2/bson" 9 | "go.mongodb.org/mongo-driver/v2/mongo" 10 | ) 11 | 12 | func handleStoreDocument(client *mongo.Client) func(w http.ResponseWriter, r *http.Request) { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | log.Println("Handling store.") 15 | 16 | databaseName := r.PathValue("database") 17 | if databaseName == "" { 18 | fail(w, http.StatusBadRequest, "database name must be supplied") 19 | } 20 | collectionName := r.PathValue("collection") 21 | if collectionName == "" { 22 | fail(w, http.StatusBadRequest, "collection name must be supplied") 23 | } 24 | documentName := r.PathValue("document") 25 | if documentName == "" { 26 | fail(w, http.StatusBadRequest, "document name must be supplied") 27 | } 28 | 29 | rawData, err := io.ReadAll(r.Body) 30 | if err != nil { 31 | fail(w, http.StatusBadRequest, "Error parsing data: %s", err) 32 | return 33 | } 34 | 35 | data := string(rawData) 36 | document := bson.M{documentNameKey: documentName, documentDataKey: data, documentTTLKey: int32(-1)} 37 | 38 | result, err := client.Database(databaseName).Collection(collectionName).InsertOne(r.Context(), document) 39 | if err != nil { 40 | fail(w, http.StatusFailedDependency, "Error creating document %q with data %q in database %q, collection %q: %s", documentName, data, databaseName, collectionName, err) 41 | return 42 | } 43 | 44 | w.WriteHeader(http.StatusCreated) 45 | log.Printf("Created document %q (named %q) with data %q in database %q, collection %q.", result.InsertedID, documentName, data, databaseName, collectionName) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mongodbapp/internal/credentials/credentials.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudfoundry-community/go-cfenv" 7 | "github.com/mitchellh/mapstructure" 8 | ) 9 | 10 | func Read() (string, error) { 11 | app, err := cfenv.Current() 12 | if err != nil { 13 | return "", fmt.Errorf("error reading app env: %w", err) 14 | } 15 | svs, err := app.Services.WithTag("mongodb") 16 | if err != nil { 17 | return "", fmt.Errorf("error reading MongoDB service details") 18 | } 19 | 20 | var m struct { 21 | URI string `mapstructure:"uri"` 22 | } 23 | 24 | if err := mapstructure.Decode(svs[0].Credentials, &m); err != nil { 25 | return "", fmt.Errorf("failed to decode credentials: %w", err) 26 | } 27 | 28 | if m.URI == "" { 29 | return "", fmt.Errorf("parsed credentials are not valid") 30 | } 31 | 32 | return m.URI, nil 33 | } 34 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mongodbapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "mongodbapp/internal/app" 7 | "mongodbapp/internal/credentials" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting.") 14 | 15 | log.Println("Reading credentials.") 16 | creds, err := credentials.Read() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | port := port() 22 | log.Printf("Listening on port: %s", port) 23 | http.Handle("/", app.App(creds)) 24 | http.ListenAndServe(port, nil) 25 | } 26 | 27 | func port() string { 28 | if port := os.Getenv("PORT"); port != "" { 29 | return fmt.Sprintf(":%s", port) 30 | } 31 | return ":8080" 32 | } 33 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/go.mod: -------------------------------------------------------------------------------- 1 | module mssqlapp 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/cloudfoundry-community/go-cfenv v1.18.0 7 | github.com/denisenkom/go-mssqldb v0.12.3 8 | github.com/mitchellh/mapstructure v1.5.0 9 | ) 10 | 11 | require ( 12 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect 13 | github.com/golang-sql/sqlexp v0.1.0 // indirect 14 | github.com/onsi/gomega v1.10.5 // indirect 15 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "regexp" 9 | 10 | _ "github.com/denisenkom/go-mssqldb" 11 | ) 12 | 13 | const ( 14 | tableName = "test" 15 | keyColumn = "keyname" 16 | valueColumn = "valuedata" 17 | ) 18 | 19 | func App(config string) http.Handler { 20 | db := connect(config) 21 | defer db.Close() 22 | if err := db.Ping(); err != nil { 23 | log.Fatalf("failed to ping database: %s", err) 24 | } 25 | 26 | r := http.NewServeMux() 27 | r.HandleFunc("GET /", aliveness) 28 | r.HandleFunc("PUT /{schema}", handleCreateSchema(config)) 29 | r.HandleFunc("POST /{schema}", handleFillDatabase(config)) 30 | r.HandleFunc("DELETE /{schema}", handleDropSchema(config)) 31 | r.HandleFunc("PUT /{schema}/{key}", handleSet(config)) 32 | r.HandleFunc("GET /{schema}/{key}", handleGet(config)) 33 | 34 | return r 35 | } 36 | 37 | func aliveness(w http.ResponseWriter, r *http.Request) { 38 | log.Printf("Handled aliveness test.") 39 | w.WriteHeader(http.StatusNoContent) 40 | } 41 | 42 | func connect(config string) *sql.DB { 43 | db, err := sql.Open("sqlserver", config) 44 | if err != nil { 45 | log.Fatalf("failed to connect to database: %s", err) 46 | } 47 | 48 | return db 49 | } 50 | 51 | func schemaName(r *http.Request) (string, error) { 52 | schema := r.PathValue("schema") 53 | 54 | switch { 55 | case schema == "": 56 | return "", fmt.Errorf("schema name must be supplied") 57 | case len(schema) > 50: 58 | return "", fmt.Errorf("schema name too long") 59 | case !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(schema): 60 | return "", fmt.Errorf("schema name contains invalid characters") 61 | default: 62 | return schema, nil 63 | } 64 | } 65 | 66 | func fail(w http.ResponseWriter, code int, format string, a ...any) { 67 | msg := fmt.Sprintf(format, a...) 68 | log.Println(msg) 69 | http.Error(w, msg, code) 70 | } 71 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/internal/app/create_schema.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func handleCreateSchema(config string) func(w http.ResponseWriter, r *http.Request) { 10 | return func(w http.ResponseWriter, r *http.Request) { 11 | log.Println("Handling create schema.") 12 | db := connect(config) 13 | defer db.Close() 14 | 15 | schema, err := schemaName(r) 16 | if err != nil { 17 | fail(w, http.StatusInternalServerError, "schema name error: %s", err) 18 | return 19 | } 20 | 21 | statement := fmt.Sprintf(`CREATE SCHEMA %s`, schema) 22 | switch r.URL.Query().Get("dbo") { 23 | case "", "true": 24 | statement = statement + " AUTHORIZATION dbo" 25 | case "false": 26 | default: 27 | fail(w, http.StatusBadRequest, "invalid value for dbo") 28 | return 29 | } 30 | 31 | if _, err = db.Exec(statement); err != nil { 32 | fail(w, http.StatusBadRequest, "failed to create schema: %s", err) 33 | return 34 | } 35 | 36 | if _, err = db.Exec(fmt.Sprintf(`CREATE TABLE %s.%s (%s VARCHAR(255) NOT NULL, %s VARCHAR(max) NOT NULL)`, schema, tableName, keyColumn, valueColumn)); err != nil { 37 | fail(w, http.StatusBadRequest, "error creating table: %s", err) 38 | return 39 | } 40 | 41 | w.WriteHeader(http.StatusCreated) 42 | log.Printf("Schema %q created", schema) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/internal/app/drop_schema.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func handleDropSchema(config string) func(w http.ResponseWriter, r *http.Request) { 10 | return func(w http.ResponseWriter, r *http.Request) { 11 | log.Println("Handling drop schema.") 12 | db := connect(config) 13 | defer db.Close() 14 | 15 | schema, err := schemaName(r) 16 | if err != nil { 17 | fail(w, http.StatusInternalServerError, "schema name error: %s", err) 18 | return 19 | } 20 | 21 | if _, err = db.Exec(fmt.Sprintf(`DROP TABLE %s.%s`, schema, tableName)); err != nil { 22 | fail(w, http.StatusBadRequest, "error dropping table: %s", err) 23 | return 24 | } 25 | 26 | if _, err = db.Exec(fmt.Sprintf(`DROP SCHEMA %s`, schema)); err != nil { 27 | fail(w, http.StatusBadRequest, "error dropping schema: %s", err) 28 | return 29 | } 30 | 31 | w.WriteHeader(http.StatusNoContent) 32 | log.Printf("Schema %q dropped", schema) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/internal/app/fill_db.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | func handleFillDatabase(config string) func(w http.ResponseWriter, r *http.Request) { 12 | return func(w http.ResponseWriter, r *http.Request) { 13 | log.Println("Handling fill database.") 14 | db := connect(config) 15 | defer db.Close() 16 | 17 | schema, err := schemaName(r) 18 | if err != nil { 19 | fail(w, http.StatusInternalServerError, "schema name error: %s", err) 20 | return 21 | } 22 | 23 | stmt, err := db.Prepare(fmt.Sprintf(`INSERT INTO %s.%s (%s, %s) VALUES (@p1, REPLICATE(CAST(@p2 AS VARCHAR(max)), 100000))`, schema, tableName, keyColumn, valueColumn)) 24 | if err != nil { 25 | fail(w, http.StatusInternalServerError, "error preparing statement: %s", err) 26 | return 27 | } 28 | defer stmt.Close() 29 | 30 | row := randomString() 31 | log.Printf("inserting row: %s\n", row) 32 | _, err = stmt.Exec(row, randomString()) 33 | switch { 34 | case err == nil: 35 | log.Println("inserted ok") 36 | w.WriteHeader(http.StatusOK) 37 | case strings.Contains(err.Error(), "has reached its size quota"): 38 | log.Println("database full") 39 | w.WriteHeader(http.StatusTooManyRequests) 40 | default: 41 | log.Printf("error inserting into database: %s\n", err) 42 | w.WriteHeader(http.StatusInternalServerError) 43 | } 44 | } 45 | } 46 | 47 | func randomString() string { 48 | buf := make([]byte, 50) 49 | rand.Read(buf) 50 | return fmt.Sprintf("%x", buf) 51 | } 52 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/internal/app/get.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func handleGet(config string) func(w http.ResponseWriter, r *http.Request) { 10 | return func(w http.ResponseWriter, r *http.Request) { 11 | log.Println("Handling get.") 12 | db := connect(config) 13 | defer db.Close() 14 | 15 | schema, err := schemaName(r) 16 | if err != nil { 17 | fail(w, http.StatusInternalServerError, "schema name error: %s", err) 18 | return 19 | } 20 | 21 | key := r.PathValue("key") 22 | if key == "" { 23 | fail(w, http.StatusBadRequest, "key must be supplied") 24 | return 25 | } 26 | 27 | stmt, err := db.Prepare(fmt.Sprintf(`SELECT %s from %s.%s WHERE %s = @p1`, valueColumn, schema, tableName, keyColumn)) 28 | if err != nil { 29 | fail(w, http.StatusInternalServerError, "error preparing statement: %s", err) 30 | return 31 | } 32 | defer stmt.Close() 33 | 34 | rows, err := stmt.Query(key) 35 | if err != nil { 36 | fail(w, http.StatusNotFound, "failed to select value for key: %s", key) 37 | return 38 | } 39 | defer rows.Close() 40 | 41 | if !rows.Next() { 42 | fail(w, http.StatusNotFound, "failed to find value for key: %s", key) 43 | return 44 | } 45 | 46 | var value string 47 | if err := rows.Scan(&value); err != nil { 48 | fail(w, http.StatusNotFound, "failed to retrieve value for key: %s", key) 49 | return 50 | } 51 | 52 | w.WriteHeader(http.StatusOK) 53 | w.Header().Set("Content-Type", "text/html") 54 | _, err = w.Write([]byte(value)) 55 | 56 | if err != nil { 57 | log.Printf("Error writing value: %s", err) 58 | return 59 | } 60 | 61 | log.Printf("Value %q retrived from key %q in schema %s.", value, key, schema) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/internal/app/set.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func handleSet(config string) func(w http.ResponseWriter, r *http.Request) { 11 | return func(w http.ResponseWriter, r *http.Request) { 12 | log.Println("Handling set.") 13 | db := connect(config) 14 | defer db.Close() 15 | 16 | schema, err := schemaName(r) 17 | if err != nil { 18 | fail(w, http.StatusInternalServerError, "schema name error: %s", err) 19 | return 20 | } 21 | 22 | key := r.PathValue("key") 23 | if key == "" { 24 | fail(w, http.StatusBadRequest, "key must be supplied") 25 | return 26 | } 27 | 28 | rawValue, err := io.ReadAll(r.Body) 29 | if err != nil { 30 | fail(w, http.StatusBadRequest, "error parsing value: %s", err) 31 | return 32 | } 33 | 34 | stmt, err := db.Prepare(fmt.Sprintf(`INSERT INTO %s.%s (%s, %s) VALUES (@p1, @p2)`, schema, tableName, keyColumn, valueColumn)) 35 | if err != nil { 36 | fail(w, http.StatusInternalServerError, "error preparing statement: %s", err) 37 | return 38 | } 39 | defer stmt.Close() 40 | 41 | _, err = stmt.Exec(key, string(rawValue)) 42 | if err != nil { 43 | fail(w, http.StatusBadRequest, "failed to insert value: %s", err) 44 | return 45 | } 46 | 47 | w.WriteHeader(http.StatusCreated) 48 | log.Printf("Key %q set to value %q in schema %q.", key, string(rawValue), schema) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/internal/credentials/config.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type Config struct { 10 | UserID string `mapstructure:"username" config:"user id"` 11 | Password string `mapstructure:"password" config:"password"` 12 | Server string `mapstructure:"hostname" config:"server"` 13 | Port int `mapstructure:"port" config:"port"` 14 | Database string `mapstructure:"name" config:"database"` 15 | } 16 | 17 | func (c Config) Valid() bool { 18 | for _, v := range c.toMap() { 19 | if reflect.ValueOf(v).IsZero() { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | 26 | func (c Config) String() string { 27 | params := c.toMap() 28 | params["encrypt"] = true 29 | 30 | var s strings.Builder 31 | for k, v := range params { 32 | s.WriteString(k) 33 | switch t := v.(type) { 34 | case int: 35 | s.WriteString(fmt.Sprintf("=%d; ", t)) 36 | case bool: 37 | s.WriteString(fmt.Sprintf("=%t; ", t)) 38 | default: 39 | s.WriteString(fmt.Sprintf("=%s; ", t)) 40 | } 41 | } 42 | return s.String() 43 | } 44 | 45 | func (c Config) toMap() map[string]any { 46 | m := make(map[string]any) 47 | v := reflect.ValueOf(c) 48 | t := v.Type() 49 | for i := 0; i < t.NumField(); i++ { 50 | key := t.Field(i).Tag.Get("config") 51 | value := v.Field(i).Interface() 52 | m[key] = value 53 | } 54 | return m 55 | } 56 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/internal/credentials/credentials.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/cloudfoundry-community/go-cfenv" 8 | "github.com/mitchellh/mapstructure" 9 | ) 10 | 11 | func Read() (string, error) { 12 | app, err := cfenv.Current() 13 | if err != nil { 14 | return "", fmt.Errorf("error reading app env: %w", err) 15 | } 16 | if svs, err := app.Services.WithTag("mssql"); err == nil { 17 | log.Println("found tag: mssql") 18 | return readService(svs) 19 | } 20 | if svs, err := app.Services.WithLabel("azure-sqldb"); err == nil { 21 | log.Println("found label: azure-sqldb") 22 | return readService(svs) 23 | } 24 | if svs, err := app.Services.WithLabel("azure-sqldb-failover-group"); err == nil { 25 | log.Println("found label: azure-sqldb-failover-group") 26 | return readService(svs) 27 | } 28 | 29 | return "", fmt.Errorf("error reading MSSQL service details") 30 | } 31 | 32 | func readService(svs []cfenv.Service) (string, error) { 33 | var c Config 34 | if err := mapstructure.Decode(svs[0].Credentials, &c); err != nil { 35 | return "", fmt.Errorf("failed to decode credentials: %w", err) 36 | } 37 | 38 | if !c.Valid() { 39 | return "", fmt.Errorf("parsed credentials are not valid") 40 | } 41 | 42 | return c.String(), nil 43 | } 44 | -------------------------------------------------------------------------------- /acceptance-tests/apps/mssqlapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "mssqlapp/internal/app" 7 | "mssqlapp/internal/credentials" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting.") 14 | 15 | log.Println("Reading credentials.") 16 | config, err := credentials.Read() 17 | if err != nil { 18 | log.Fatalf("failed to read credentials: %s", err) 19 | } 20 | 21 | port := port() 22 | log.Printf("Listening on port: %s", port) 23 | http.Handle("/", app.App(config)) 24 | http.ListenAndServe(port, nil) 25 | } 26 | 27 | func port() string { 28 | if port := os.Getenv("PORT"); port != "" { 29 | return fmt.Sprintf(":%s", port) 30 | } 31 | return ":8080" 32 | } 33 | -------------------------------------------------------------------------------- /acceptance-tests/apps/postgresqlapp/go.mod: -------------------------------------------------------------------------------- 1 | module postgresqlapp 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/cloudfoundry-community/go-cfenv v1.18.0 7 | github.com/jackc/pgx/v5 v5.7.5 8 | github.com/mitchellh/mapstructure v1.5.0 9 | ) 10 | 11 | require ( 12 | github.com/jackc/pgpassfile v1.0.0 // indirect 13 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 14 | github.com/jackc/puddle/v2 v2.2.2 // indirect 15 | github.com/onsi/gomega v1.10.5 // indirect 16 | golang.org/x/crypto v0.37.0 // indirect 17 | golang.org/x/sync v0.13.0 // indirect 18 | golang.org/x/text v0.24.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /acceptance-tests/apps/postgresqlapp/internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "regexp" 9 | 10 | _ "github.com/jackc/pgx/v5/stdlib" 11 | ) 12 | 13 | const ( 14 | tableName = "test" 15 | keyColumn = "keyname" 16 | valueColumn = "valuedata" 17 | ) 18 | 19 | func App(uri string) http.Handler { 20 | db := connect(uri) 21 | 22 | r := http.NewServeMux() 23 | r.HandleFunc("GET /", aliveness) 24 | r.HandleFunc("PUT /{schema}", handleCreateSchema(db)) 25 | r.HandleFunc("DELETE /{schema}", handleDropSchema(db)) 26 | r.HandleFunc("PUT /{schema}/{key}", handleSet(db)) 27 | r.HandleFunc("GET /{schema}/{key}", handleGet(db)) 28 | 29 | return r 30 | } 31 | 32 | func aliveness(w http.ResponseWriter, r *http.Request) { 33 | log.Printf("Handled aliveness test.") 34 | w.WriteHeader(http.StatusNoContent) 35 | } 36 | 37 | func connect(uri string) *sql.DB { 38 | db, err := sql.Open("pgx", uri) 39 | if err != nil { 40 | log.Fatalf("failed to connect to database: %s", err) 41 | } 42 | db.SetMaxIdleConns(0) 43 | return db 44 | } 45 | 46 | func schemaName(r *http.Request) (string, error) { 47 | schema := r.PathValue("schema") 48 | 49 | switch { 50 | case schema == "": 51 | return "", fmt.Errorf("schema name must be supplied") 52 | case len(schema) > 50: 53 | return "", fmt.Errorf("schema name too long") 54 | case !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(schema): 55 | return "", fmt.Errorf("schema name contains invalid characters") 56 | default: 57 | return schema, nil 58 | } 59 | } 60 | 61 | func fail(w http.ResponseWriter, code int, format string, a ...any) { 62 | msg := fmt.Sprintf(format, a...) 63 | log.Println(msg) 64 | http.Error(w, msg, code) 65 | } 66 | -------------------------------------------------------------------------------- /acceptance-tests/apps/postgresqlapp/internal/app/create_schema.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func handleCreateSchema(db *sql.DB) func(w http.ResponseWriter, r *http.Request) { 11 | return func(w http.ResponseWriter, r *http.Request) { 12 | log.Println("Handling create schema.") 13 | 14 | schema, err := schemaName(r) 15 | if err != nil { 16 | fail(w, http.StatusInternalServerError, "Schema name error: %s", err) 17 | return 18 | } 19 | 20 | _, err = db.Exec(fmt.Sprintf(`CREATE SCHEMA %s`, schema)) 21 | if err != nil { 22 | fail(w, http.StatusBadRequest, "Error creating schema: %s", err) 23 | return 24 | } 25 | 26 | _, err = db.Exec(fmt.Sprintf(`GRANT ALL ON SCHEMA %s TO PUBLIC`, schema)) 27 | if err != nil { 28 | fail(w, http.StatusBadRequest, "Error granting schema permissions: %s", err) 29 | return 30 | } 31 | 32 | _, err = db.Exec(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.%s (%s VARCHAR(255) NOT NULL, %s VARCHAR(255) NOT NULL)`, schema, tableName, keyColumn, valueColumn)) 33 | if err != nil { 34 | fail(w, http.StatusBadRequest, "Error creating table: %s", err) 35 | return 36 | } 37 | 38 | _, err = db.Exec(fmt.Sprintf(`GRANT ALL ON TABLE %s.%s TO PUBLIC`, schema, tableName)) 39 | if err != nil { 40 | fail(w, http.StatusBadRequest, "Error granting table permissions: %s", err) 41 | return 42 | } 43 | 44 | w.WriteHeader(http.StatusCreated) 45 | log.Printf("Schema %q created", schema) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /acceptance-tests/apps/postgresqlapp/internal/app/drop_schema.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func handleDropSchema(db *sql.DB) func(w http.ResponseWriter, r *http.Request) { 11 | return func(w http.ResponseWriter, r *http.Request) { 12 | log.Println("Handling drop schema.") 13 | 14 | schema, err := schemaName(r) 15 | if err != nil { 16 | fail(w, http.StatusInternalServerError, "Schema name error: %s\n", err) 17 | return 18 | } 19 | 20 | _, err = db.Exec(fmt.Sprintf(`DROP TABLE %s.%s`, schema, tableName)) 21 | if err != nil { 22 | fail(w, http.StatusBadRequest, "Error dropping table: %s", err) 23 | return 24 | } 25 | 26 | _, err = db.Exec(fmt.Sprintf(`DROP SCHEMA %s`, schema)) 27 | if err != nil { 28 | fail(w, http.StatusBadRequest, "Error creating schema: %s", err) 29 | return 30 | } 31 | 32 | w.WriteHeader(http.StatusNoContent) 33 | log.Printf("Schema %q dropped", schema) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /acceptance-tests/apps/postgresqlapp/internal/app/get.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func handleGet(db *sql.DB) func(w http.ResponseWriter, r *http.Request) { 11 | return func(w http.ResponseWriter, r *http.Request) { 12 | log.Println("Handling get.") 13 | 14 | schema, err := schemaName(r) 15 | if err != nil { 16 | fail(w, http.StatusInternalServerError, "Schema name error: %s", err) 17 | return 18 | } 19 | 20 | key := r.PathValue("key") 21 | if key == "" { 22 | fail(w, http.StatusBadRequest, "key name must be supplied") 23 | return 24 | } 25 | 26 | stmt, err := db.Prepare(fmt.Sprintf(`SELECT %s from %s.%s WHERE %s = $1`, valueColumn, schema, tableName, keyColumn)) 27 | if err != nil { 28 | fail(w, http.StatusBadRequest, "Error preparing statement: %s", err) 29 | return 30 | } 31 | defer stmt.Close() 32 | 33 | rows, err := stmt.Query(key) 34 | if err != nil { 35 | fail(w, http.StatusNotFound, "Error selecting value: %s", err) 36 | return 37 | } 38 | defer rows.Close() 39 | 40 | if !rows.Next() { 41 | fail(w, http.StatusNotFound, "Error finding value: %s", err) 42 | return 43 | } 44 | 45 | var value string 46 | if err := rows.Scan(&value); err != nil { 47 | fail(w, http.StatusNotFound, "Error retrieving value: %s", err) 48 | return 49 | } 50 | 51 | w.WriteHeader(http.StatusOK) 52 | w.Header().Set("Content-Type", "text/html") 53 | _, err = w.Write([]byte(value)) 54 | 55 | if err != nil { 56 | log.Printf("Error writing value: %s", err) 57 | return 58 | } 59 | 60 | log.Printf("Value %q retrived from key %q.", value, key) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /acceptance-tests/apps/postgresqlapp/internal/app/set.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | func handleSet(db *sql.DB) func(w http.ResponseWriter, r *http.Request) { 12 | return func(w http.ResponseWriter, r *http.Request) { 13 | log.Println("Handling set.") 14 | 15 | schema, err := schemaName(r) 16 | if err != nil { 17 | fail(w, http.StatusInternalServerError, "Schema name error: %s", err) 18 | return 19 | } 20 | 21 | key := r.PathValue("key") 22 | if key == "" { 23 | fail(w, http.StatusBadRequest, "key name must be supplied") 24 | return 25 | } 26 | 27 | rawValue, err := io.ReadAll(r.Body) 28 | if err != nil { 29 | fail(w, http.StatusBadRequest, "Error parsing value: %s", err) 30 | return 31 | } 32 | 33 | if schema == "public" { 34 | _, err = db.Exec(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (%s VARCHAR(255) NOT NULL, %s VARCHAR(255) NOT NULL)`, tableName, keyColumn, valueColumn)) 35 | if err != nil { 36 | log.Fatalf("failed to create test table: %s", err) 37 | } 38 | } 39 | 40 | stmt, err := db.Prepare(fmt.Sprintf(`INSERT INTO %s.%s (%s, %s) VALUES ($1, $2)`, schema, tableName, keyColumn, valueColumn)) 41 | if err != nil { 42 | fail(w, http.StatusInternalServerError, "Error preparing statement: %s", err) 43 | return 44 | } 45 | defer stmt.Close() 46 | 47 | _, err = stmt.Exec(key, string(rawValue)) 48 | if err != nil { 49 | fail(w, http.StatusBadRequest, "Error inserting values: %s", err) 50 | return 51 | } 52 | 53 | w.WriteHeader(http.StatusCreated) 54 | log.Printf("Key %q set to value %q.", key, string(rawValue)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /acceptance-tests/apps/postgresqlapp/internal/credentials/credentials.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudfoundry-community/go-cfenv" 7 | "github.com/mitchellh/mapstructure" 8 | ) 9 | 10 | func Read() (string, error) { 11 | app, err := cfenv.Current() 12 | if err != nil { 13 | return "", fmt.Errorf("error reading app env: %w", err) 14 | } 15 | svs, err := app.Services.WithTag("postgresql") 16 | if err != nil { 17 | return "", fmt.Errorf("error reading PostgreSQL service details") 18 | } 19 | 20 | var m struct { 21 | URI string `mapstructure:"uri"` 22 | } 23 | 24 | if err := mapstructure.Decode(svs[0].Credentials, &m); err != nil { 25 | return "", fmt.Errorf("failed to decode credentials: %w", err) 26 | } 27 | 28 | if m.URI == "" { 29 | return "", fmt.Errorf("parsed credentials are not valid") 30 | } 31 | 32 | return m.URI, nil 33 | } 34 | -------------------------------------------------------------------------------- /acceptance-tests/apps/postgresqlapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "postgresqlapp/internal/app" 9 | "postgresqlapp/internal/credentials" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting.") 14 | 15 | log.Println("Reading credentials.") 16 | creds, err := credentials.Read() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | port := port() 22 | log.Printf("Listening on port: %s", port) 23 | http.Handle("/", app.App(creds)) 24 | http.ListenAndServe(port, nil) 25 | } 26 | 27 | func port() string { 28 | if port := os.Getenv("PORT"); port != "" { 29 | return fmt.Sprintf(":%s", port) 30 | } 31 | return ":8080" 32 | } 33 | -------------------------------------------------------------------------------- /acceptance-tests/apps/redisapp/go.mod: -------------------------------------------------------------------------------- 1 | module redisapp 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/cloudfoundry-community/go-cfenv v1.18.0 7 | github.com/mitchellh/mapstructure v1.5.0 8 | github.com/redis/go-redis/v9 v9.9.0 9 | ) 10 | 11 | require ( 12 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 13 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 14 | github.com/onsi/gomega v1.18.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /acceptance-tests/apps/redisapp/internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | func App(options *redis.Options) http.HandlerFunc { 13 | client := redis.NewClient(options) 14 | 15 | return func(w http.ResponseWriter, r *http.Request) { 16 | key := strings.Trim(r.URL.Path, "/") 17 | switch r.Method { 18 | case http.MethodHead: 19 | aliveness(w, r) 20 | case http.MethodGet: 21 | handleGet(w, r, key, client) 22 | case http.MethodPut: 23 | handleSet(w, r, key, client) 24 | default: 25 | fail(w, http.StatusMethodNotAllowed, http.StatusText(http.StatusMethodNotAllowed)) 26 | } 27 | } 28 | } 29 | 30 | func aliveness(w http.ResponseWriter, r *http.Request) { 31 | log.Printf("Handled aliveness test.") 32 | w.WriteHeader(http.StatusNoContent) 33 | } 34 | 35 | func fail(w http.ResponseWriter, code int, format string, a ...any) { 36 | msg := fmt.Sprintf(format, a...) 37 | log.Println(msg) 38 | http.Error(w, msg, code) 39 | } 40 | -------------------------------------------------------------------------------- /acceptance-tests/apps/redisapp/internal/app/get.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | func handleGet(w http.ResponseWriter, r *http.Request, key string, client *redis.Client) { 11 | log.Println("Handling get.") 12 | 13 | value, err := client.Get(r.Context(), key).Result() 14 | if err != nil { 15 | fail(w, http.StatusNotFound, "Error retrieving value: %s", err) 16 | return 17 | } 18 | 19 | w.WriteHeader(http.StatusOK) 20 | w.Header().Set("Content-Type", "text/html") 21 | _, err = w.Write([]byte(value)) 22 | 23 | if err != nil { 24 | log.Printf("Error writing value: %s", err) 25 | return 26 | } 27 | 28 | log.Printf("Value %q retrieved from key %q.", value, key) 29 | } 30 | -------------------------------------------------------------------------------- /acceptance-tests/apps/redisapp/internal/app/set.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | func handleSet(w http.ResponseWriter, r *http.Request, key string, client *redis.Client) { 12 | log.Println("Handling set.") 13 | 14 | rawValue, err := io.ReadAll(r.Body) 15 | if err != nil { 16 | fail(w, http.StatusBadRequest, "Error parsing value: %s", err) 17 | http.Error(w, "Failed to parse value.", http.StatusBadRequest) 18 | return 19 | } 20 | 21 | value := string(rawValue) 22 | if err := client.Set(r.Context(), key, value, 0).Err(); err != nil { 23 | fail(w, http.StatusFailedDependency, "Error setting key %q to value %q: %s", key, value, err) 24 | return 25 | } 26 | 27 | w.WriteHeader(http.StatusCreated) 28 | log.Printf("Key %q set to value %q.", key, value) 29 | } 30 | -------------------------------------------------------------------------------- /acceptance-tests/apps/redisapp/internal/credentials/credentials.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | 7 | "github.com/cloudfoundry-community/go-cfenv" 8 | "github.com/mitchellh/mapstructure" 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | func Read() (*redis.Options, error) { 13 | app, err := cfenv.Current() 14 | if err != nil { 15 | return nil, fmt.Errorf("error reading app env: %w", err) 16 | } 17 | svs, err := app.Services.WithTag("redis") 18 | if err != nil { 19 | return nil, fmt.Errorf("error reading Redis service details") 20 | } 21 | 22 | var r struct { 23 | Host string `mapstructure:"host"` 24 | Password string `mapstructure:"password"` 25 | TLSPort int `mapstructure:"tls_port"` 26 | } 27 | 28 | if err := mapstructure.Decode(svs[0].Credentials, &r); err != nil { 29 | return nil, fmt.Errorf("failed to decode credentials: %w", err) 30 | } 31 | 32 | if r.Host == "" || r.Password == "" || r.TLSPort == 0 { 33 | return nil, fmt.Errorf("parsed credentials are not valid") 34 | } 35 | 36 | return &redis.Options{ 37 | Addr: fmt.Sprintf("%s:%d", r.Host, r.TLSPort), 38 | Password: r.Password, 39 | DB: 0, 40 | TLSConfig: &tls.Config{}, 41 | }, nil 42 | } 43 | -------------------------------------------------------------------------------- /acceptance-tests/apps/redisapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "redisapp/internal/app" 9 | "redisapp/internal/credentials" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting.") 14 | 15 | log.Println("Reading credentials.") 16 | creds, err := credentials.Read() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | port := port() 22 | log.Printf("Listening on port: %s", port) 23 | http.Handle("/", app.App(creds)) 24 | http.ListenAndServe(port, nil) 25 | } 26 | 27 | func port() string { 28 | if port := os.Getenv("PORT"); port != "" { 29 | return fmt.Sprintf(":%s", port) 30 | } 31 | return ":8080" 32 | } 33 | -------------------------------------------------------------------------------- /acceptance-tests/ginkgo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | go tool ginkgo "$@" 3 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/app.go: -------------------------------------------------------------------------------- 1 | // Package apps manages the test app lifecycle 2 | package apps 3 | 4 | type App struct { 5 | Name string 6 | URL string 7 | start bool 8 | buildpack string 9 | memory string 10 | disk string 11 | manifest string 12 | dir string 13 | } 14 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/delete.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "time" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 7 | 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gexec" 10 | ) 11 | 12 | func (a *App) Delete() { 13 | Delete(a) 14 | } 15 | 16 | func Delete(apps ...*App) { 17 | for _, app := range apps { 18 | session := cf.Start("delete", "-f", app.Name) 19 | Eventually(session, 5*time.Minute).Should(gexec.Exit()) 20 | checkSuccess(session.ExitCode(), app.Name) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/http.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func (a *App) GET(path string) string { 15 | return a.GETf("%s", path) 16 | } 17 | 18 | func (a *App) GETf(format string, s ...any) string { 19 | url := a.urlf(format, s...) 20 | var data []byte 21 | 22 | Eventually(func(g Gomega) *http.Response { 23 | GinkgoWriter.Printf("HTTP GET: %s\n", url) 24 | response, err := http.Get(url) 25 | g.Expect(err).NotTo(HaveOccurred()) 26 | 27 | GinkgoWriter.Printf("HTTP Status: %s\n", response.Status) 28 | 29 | defer response.Body.Close() 30 | data, err = io.ReadAll(response.Body) 31 | g.Expect(err).NotTo(HaveOccurred()) 32 | 33 | GinkgoWriter.Printf("Recieved: %s\n", string(data)) 34 | 35 | return response 36 | }).WithPolling(5 * time.Second).WithTimeout(time.Minute).Should(HaveHTTPStatus(http.StatusOK)) 37 | 38 | return string(data) 39 | } 40 | 41 | func (a *App) PUT(data, path string) { 42 | a.PUTf(data, "%s", path) 43 | } 44 | 45 | func (a *App) PUTf(data, format string, s ...any) { 46 | url := a.urlf(format, s...) 47 | GinkgoWriter.Printf("HTTP PUT: %s\n", url) 48 | GinkgoWriter.Printf("Sending data: %s\n", data) 49 | request, err := http.NewRequest(http.MethodPut, url, strings.NewReader(data)) 50 | Expect(err).NotTo(HaveOccurred()) 51 | request.Header.Set("Content-Type", "text/html") 52 | response, err := http.DefaultClient.Do(request) 53 | Expect(err).NotTo(HaveOccurred()) 54 | Expect(response).To(HaveHTTPStatus(http.StatusCreated, http.StatusOK)) 55 | } 56 | 57 | func (a *App) DELETE(path string) { 58 | a.DELETEf("%s", path) 59 | } 60 | 61 | func (a *App) DELETEf(format string, s ...any) { 62 | url := a.urlf(format, s...) 63 | GinkgoWriter.Printf("HTTP DELETE: %s\n", url) 64 | request, err := http.NewRequest(http.MethodDelete, url, nil) 65 | Expect(err).NotTo(HaveOccurred()) 66 | 67 | response, err := http.DefaultClient.Do(request) 68 | Expect(err).NotTo(HaveOccurred()) 69 | Expect(response).To(HaveHTTPStatus(http.StatusGone, http.StatusNoContent)) 70 | } 71 | 72 | func (a *App) urlf(format string, s ...any) string { 73 | base := a.URL 74 | path := fmt.Sprintf(format, s...) 75 | switch { 76 | case len(path) == 0: 77 | return base 78 | case path[0] != '/': 79 | return fmt.Sprintf("%s/%s", base, path) 80 | default: 81 | return base + path 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/log.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "fmt" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | ) 10 | 11 | func checkSuccess(code int, name string) { 12 | if code != 0 { 13 | fmt.Fprintln(GinkgoWriter, "Operation FAILED. Getting logs...") 14 | cf.Run("logs", name, "--recent") 15 | Fail("App operation failed") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/prebuild.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path" 8 | "time" 9 | 10 | "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gexec" 13 | ) 14 | 15 | func WithPreBuild(source string) Option { 16 | dir := ginkgo.GinkgoT().TempDir() 17 | name := path.Base(source) 18 | command := exec.Command("go", "build", "-o", fmt.Sprintf("%s/%s", dir, name)) 19 | command.Dir = source 20 | command.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64") 21 | 22 | session, err := gexec.Start(command, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter) 23 | Expect(err).NotTo(HaveOccurred()) 24 | Eventually(session, 5*time.Minute).Should(gexec.Exit(0)) 25 | 26 | err = os.WriteFile(path.Join(dir, "Procfile"), []byte(fmt.Sprintf("web: ./%s\n", name)), 0555) 27 | Expect(err).NotTo(HaveOccurred()) 28 | 29 | return WithOptions( 30 | WithBinaryBuildpack(), 31 | func(a *App) { 32 | a.dir = dir 33 | }, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/push.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 10 | "csbbrokerpakazure/acceptance-tests/helpers/random" 11 | 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | "github.com/onsi/gomega/gexec" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | const pushWaitTime = 20 * time.Minute 19 | 20 | type Option func(*App) 21 | 22 | func Push(opts ...Option) *App { 23 | defaults := []Option{WithName(random.Name(random.WithPrefix("app")))} 24 | var app App 25 | app.Push(append(defaults, opts...)...) 26 | return &app 27 | } 28 | 29 | func (a *App) Push(opts ...Option) { 30 | WithOptions(opts...)(a) 31 | 32 | cmd := []string{"push"} 33 | if !a.start { 34 | cmd = append(cmd, "--no-start") 35 | } 36 | if a.buildpack != "" { 37 | cmd = append(cmd, "-b", a.buildpack) 38 | } 39 | if a.memory != "" { 40 | cmd = append(cmd, "-m", a.memory) 41 | } 42 | if a.disk != "" { 43 | cmd = append(cmd, "-k", a.disk) 44 | } 45 | if a.manifest != "" { 46 | cmd = append(cmd, "-f", a.manifest) 47 | } 48 | 49 | if a.dir == "" { 50 | Fail("App directory must be specified") 51 | } 52 | cmd = append(cmd, "-p", a.dir) 53 | 54 | if a.Name == "" { 55 | Fail("App name must be specified") 56 | } 57 | cmd = append(cmd, a.Name) 58 | 59 | session := cf.Start(cmd...) 60 | Eventually(session, pushWaitTime).Should(gexec.Exit()) 61 | checkSuccess(session.ExitCode(), a.Name) 62 | 63 | if session.ExitCode() != 0 { 64 | GinkgoWriter.Printf("FAILED to push app. Getting logs...") 65 | cf.Run("logs", a.Name, "--recent") 66 | Fail("App failed to push") 67 | } 68 | 69 | a.URL = url(a.Name) 70 | } 71 | 72 | func WithBinaryBuildpack() Option { 73 | return func(a *App) { 74 | a.buildpack = "binary_buildpack" 75 | a.memory = "50MB" 76 | } 77 | } 78 | 79 | func WithName(name string) Option { 80 | return func(a *App) { 81 | a.Name = name 82 | } 83 | } 84 | func (a *App) CleanFileFromAppDir(filename string) { 85 | Expect(os.Remove(filepath.Join(a.dir, filename))).To(Succeed()) 86 | } 87 | 88 | func WithYAMLFile(filename string, contents map[string]any) Option { 89 | return func(a *App) { 90 | //convert to yaml parsable by hil 91 | for k, v := range contents { 92 | valueBytes, err := json.Marshal(v) 93 | Expect(err).NotTo(HaveOccurred()) 94 | contents[k] = string(valueBytes) 95 | } 96 | bytes, err := yaml.Marshal(contents) 97 | Expect(err).NotTo(HaveOccurred()) 98 | Expect(os.WriteFile(filepath.Join(a.dir, filename), bytes, 0666)).To(Succeed()) 99 | } 100 | 101 | } 102 | 103 | func WithDir(dir string) Option { 104 | return func(a *App) { 105 | a.dir = dir 106 | } 107 | } 108 | 109 | func WithManifest(manifest string) Option { 110 | return func(a *App) { 111 | a.manifest = manifest 112 | } 113 | } 114 | 115 | func WithStartedState() Option { 116 | return func(a *App) { 117 | a.start = true 118 | } 119 | } 120 | 121 | func WithMemory(memory string) Option { 122 | return func(a *App) { 123 | a.memory = memory 124 | } 125 | } 126 | 127 | func WithDisk(disk string) Option { 128 | return func(a *App) { 129 | a.disk = disk 130 | } 131 | } 132 | 133 | func WithOptions(opts ...Option) Option { 134 | return func(a *App) { 135 | for _, o := range opts { 136 | o(a) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/restage.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "time" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 7 | 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gexec" 10 | ) 11 | 12 | func (a *App) Restage() { 13 | Restage(a) 14 | } 15 | 16 | func Restage(apps ...*App) { 17 | for _, app := range apps { 18 | session := cf.Start("restage", app.Name) 19 | Eventually(session, 5*time.Minute).Should(gexec.Exit()) 20 | checkSuccess(session.ExitCode(), app.Name) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/restart.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "time" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 7 | 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gexec" 10 | ) 11 | 12 | func (a *App) Restart() { 13 | Restart(a) 14 | } 15 | 16 | func Restart(apps ...*App) { 17 | for _, app := range apps { 18 | session := cf.Start("restart", app.Name) 19 | Eventually(session, 5*time.Minute).Should(gexec.Exit()) 20 | checkSuccess(session.ExitCode(), app.Name) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/setenv.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 7 | 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | type EnvVar struct { 12 | Name string 13 | Value any 14 | } 15 | 16 | func (e EnvVar) ValueString() string { 17 | switch v := e.Value.(type) { 18 | case string: 19 | return v 20 | default: 21 | data, err := json.Marshal(v) 22 | Expect(err).NotTo(HaveOccurred()) 23 | return string(data) 24 | } 25 | } 26 | 27 | func (a *App) SetEnv(env ...EnvVar) { 28 | SetEnv(a.Name, env...) 29 | } 30 | 31 | func SetEnv(name string, env ...EnvVar) { 32 | for _, envVar := range env { 33 | v := envVar.ValueString() 34 | if v == "" { 35 | cf.Run("unset-env", name, envVar.Name) 36 | } else { 37 | cf.Run("set-env", name, envVar.Name, v) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/start.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "time" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 7 | 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gexec" 10 | ) 11 | 12 | func (a *App) Start() { 13 | Start(a) 14 | } 15 | 16 | func Start(apps ...*App) { 17 | for _, app := range apps { 18 | session := cf.Start("start", app.Name) 19 | Eventually(session, 5*time.Minute).Should(gexec.Exit()) 20 | checkSuccess(session.ExitCode(), app.Name) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/testapps.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "csbbrokerpakazure/acceptance-tests/helpers/testpath" 5 | ) 6 | 7 | type AppCode string 8 | 9 | const ( 10 | MongoDB AppCode = "mongodbapp" 11 | MSSQL AppCode = "mssqlapp" 12 | PostgreSQL AppCode = "postgresqlapp" 13 | Redis AppCode = "redisapp" 14 | ) 15 | 16 | func (a AppCode) Dir() string { 17 | return testpath.BrokerpakFile("acceptance-tests", "apps", string(a)) 18 | } 19 | 20 | func WithApp(app AppCode) Option { 21 | return WithOptions(WithPreBuild(app.Dir()), WithMemory("100MB"), WithDisk("250MB")) 22 | } 23 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/apps/url.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 8 | 9 | . "github.com/onsi/gomega" 10 | 11 | "code.cloudfoundry.org/jsonry" 12 | ) 13 | 14 | func url(name string) string { 15 | env, _ := cf.Run("curl", fmt.Sprintf("/v3/apps/%s/env", guid(name))) 16 | var receiver struct { 17 | BrokerURL []string `jsonry:"application_env_json.VCAP_APPLICATION.application_uris[]"` 18 | } 19 | err := jsonry.Unmarshal([]byte(env), &receiver) 20 | Expect(err).NotTo(HaveOccurred()) 21 | return fmt.Sprintf("http://%s", receiver.BrokerURL[0]) 22 | } 23 | 24 | func guid(name string) string { 25 | out, _ := cf.Run("app", "--guid", name) 26 | return strings.TrimSpace(out) 27 | } 28 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/az/start.go: -------------------------------------------------------------------------------- 1 | // Package az to wrap executing the az cli 2 | package az 3 | 4 | import ( 5 | "os/exec" 6 | "strings" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/gexec" 12 | ) 13 | 14 | func Run(args ...string) { 15 | GinkgoHelper() 16 | 17 | GinkgoWriter.Printf("Running: az %s\n", strings.Join(args, " ")) 18 | command := exec.Command("az", args...) 19 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 20 | Expect(err).NotTo(HaveOccurred()) 21 | Eventually(session).WithTimeout(time.Hour).Should(gexec.Exit(0)) 22 | } 23 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/bindings/bind.go: -------------------------------------------------------------------------------- 1 | // Package bindings manages service bindings 2 | package bindings 3 | 4 | import ( 5 | "time" 6 | 7 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 8 | "csbbrokerpakazure/acceptance-tests/helpers/random" 9 | 10 | . "github.com/onsi/gomega" 11 | . "github.com/onsi/gomega/gexec" 12 | ) 13 | 14 | const timeout = 10 * time.Minute 15 | 16 | type Binding struct { 17 | name string 18 | serviceInstanceName string 19 | appName string 20 | } 21 | 22 | func Bind(serviceInstanceName, appName string) *Binding { 23 | name := random.Name() 24 | session := cf.Start("bind-service", appName, serviceInstanceName, "--binding-name", name) 25 | Eventually(session).WithTimeout(timeout).Should(Exit(0)) 26 | return &Binding{ 27 | name: name, 28 | serviceInstanceName: serviceInstanceName, 29 | appName: appName, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/bindings/credential.go: -------------------------------------------------------------------------------- 1 | package bindings 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 8 | 9 | "code.cloudfoundry.org/jsonry" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func (b *Binding) Credential() any { 15 | out, _ := cf.Run("app", "--guid", b.appName) 16 | env, _ := cf.Run("curl", fmt.Sprintf("/v3/apps/%s/env", strings.TrimSpace(out))) 17 | 18 | var receiver struct { 19 | Services map[string]any `jsonry:"system_env_json.VCAP_SERVICES"` 20 | } 21 | Expect(jsonry.Unmarshal([]byte(env), &receiver)).NotTo(HaveOccurred()) 22 | 23 | for _, bindings := range receiver.Services { 24 | Expect(bindings).To(BeAssignableToTypeOf([]any{})) 25 | for _, bnd := range bindings.([]any) { 26 | if n, ok := bnd.(map[string]any)["name"]; ok && n == b.name { 27 | Expect(bnd).To(HaveKey("credentials")) 28 | return bnd.(map[string]any)["credentials"] 29 | } 30 | } 31 | } 32 | 33 | Fail(fmt.Sprintf("could not find data for binding: %q\n%+v", b.name, receiver.Services)) 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/bindings/unbind.go: -------------------------------------------------------------------------------- 1 | package bindings 2 | 3 | import ( 4 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 5 | 6 | . "github.com/onsi/gomega" 7 | gexec "github.com/onsi/gomega/gexec" 8 | ) 9 | 10 | func (b *Binding) Unbind() { 11 | session := cf.Start("unbind-service", b.appName, b.serviceInstanceName) 12 | Eventually(session).WithTimeout(timeout).Should(gexec.Exit(0)) 13 | } 14 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokerpaks/brokerpaks.go: -------------------------------------------------------------------------------- 1 | // Package brokerpaks is used for downloading brokerpaks and associated resources for upgrade tests 2 | package brokerpaks 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | ) 11 | 12 | const brokerpak = "cloudfoundry/csb-brokerpak-azure" 13 | 14 | // DownloadBrokerpak will download the brokerpak of the specified 15 | // version and return the directory where it has been placed. 16 | // The download is skipped if it has previously been downloaded. 17 | // Includes downloading the corresponding broker and ".envrc" file 18 | func DownloadBrokerpak(version, dir string) string { 19 | // Brokerpak 20 | basename := fmt.Sprintf("azure-services-%s.brokerpak", version) 21 | uri := fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", brokerpak, version, basename) 22 | downloadUnlessCached(dir, basename, uri) 23 | 24 | // ".envrc" file 25 | envrcURI := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/.envrc", brokerpak, version) 26 | downloadUnlessCached(dir, ".envrc", envrcURI) 27 | 28 | // broker 29 | brokerVersion := readBrokerVersion(version) 30 | brokerURI := fmt.Sprintf("https://github.com/cloudfoundry/cloud-service-broker/releases/download/%s/cloud-service-broker.linux", brokerVersion) 31 | downloadUnlessCached(dir, "cloud-service-broker", brokerURI) 32 | if err := os.Chmod(filepath.Join(dir, "cloud-service-broker"), 0777); err != nil { 33 | panic(err) 34 | } 35 | 36 | return dir 37 | } 38 | 39 | // readBrokerVersion will use the specified brokerpak version to determine the corresponding broker version 40 | func readBrokerVersion(version string) string { 41 | body := newClient().get(fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/go.mod", brokerpak, version), "text/plain") 42 | defer body.Close() 43 | data := must(io.ReadAll(body)) 44 | 45 | matches := regexp.MustCompile(`(?m)^\s*github\.com/cloudfoundry/cloud-service-broker(/v\d+)?\s+(\S+)\s*$`).FindSubmatch(data) 46 | if len(matches) != 3 { 47 | panic(fmt.Sprintf("Could not extract CSB version from go.mod file: %q", data)) 48 | } 49 | 50 | brokerVersion := string(matches[2]) 51 | fmt.Printf("Brokerpak version %q uses broker version %q\n", version, brokerVersion) 52 | return brokerVersion 53 | } 54 | 55 | // downloadUnlessCached will download a file to a known location, unless it's already there 56 | func downloadUnlessCached(dir, basename, uri string) { 57 | target := filepath.Join(dir, basename) 58 | 59 | _, err := os.Stat(target) 60 | switch err { 61 | case nil: 62 | fmt.Printf("Found %q cached at %q.\n", uri, target) 63 | default: 64 | fmt.Printf("Downloading %q to %q.\n", uri, target) 65 | newClient().download(target, uri) 66 | } 67 | } 68 | 69 | // TargetDir will determine the target directory for a version and make sure that it exists 70 | func TargetDir(version string) string { 71 | pwd, err := os.Getwd() 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | dir := filepath.Join(pwd, "versions", version) 77 | if err := os.MkdirAll(dir, 0777); err != nil { 78 | panic(err) 79 | } 80 | 81 | return dir 82 | } 83 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokerpaks/client.go: -------------------------------------------------------------------------------- 1 | package brokerpaks 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | func newClient() *client { 11 | return &client{ 12 | token: os.Getenv("GITHUB_TOKEN"), 13 | } 14 | } 15 | 16 | // client is a microscopic GitHub client allowing HTTP GET 17 | type client struct { 18 | token string 19 | } 20 | 21 | // get will do a HTTP GET to a body 22 | func (c client) get(path, mimeType string) io.ReadCloser { 23 | req := must(http.NewRequest(http.MethodGet, path, nil)) 24 | 25 | req.Header.Add("Accept", mimeType) 26 | if c.token != "" { 27 | req.Header.Add("Authorization", fmt.Sprintf("token %s", c.token)) 28 | } 29 | 30 | res := must(http.DefaultClient.Do(req)) 31 | if res.StatusCode != http.StatusOK { 32 | panic(fmt.Sprintf("expected HTTP 200 but got %d: %s", res.StatusCode, res.Status)) 33 | } 34 | return res.Body 35 | } 36 | 37 | // download will do an HTTP GET to a file 38 | func (c client) download(target, uri string) { 39 | fh := must(os.Create(target)) 40 | defer fh.Close() 41 | 42 | body := c.get(uri, "application/octet-stream") 43 | defer body.Close() 44 | 45 | _, err := io.Copy(fh, body) 46 | if err != nil { 47 | panic(err) 48 | } 49 | } 50 | 51 | func must[A any](input A, err error) A { 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | return input 57 | } 58 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokerpaks/prepare/prepare.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/brokerpaks" 7 | ) 8 | 9 | func main() { 10 | var version, dir string 11 | flag.StringVar(&version, "version", "", "version to upgrade from") 12 | flag.StringVar(&dir, "dir", "", "directory to install to") 13 | flag.Parse() 14 | 15 | if version == "" { 16 | version = brokerpaks.LatestVersion() 17 | } 18 | 19 | if dir == "" { 20 | dir = brokerpaks.TargetDir(version) 21 | } 22 | 23 | brokerpaks.DownloadBrokerpak(version, dir) 24 | } 25 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokerpaks/versions.go: -------------------------------------------------------------------------------- 1 | package brokerpaks 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "sort" 8 | 9 | "github.com/blang/semver/v4" 10 | ) 11 | 12 | // LatestVersion will determine the latest released version of the brokerpak 13 | // according to semantic versioning 14 | func LatestVersion() string { 15 | versions := Versions() 16 | latest := versions[len(versions)-1].String() 17 | fmt.Printf("Latest brokerpak version: %s\n", latest) 18 | return latest 19 | } 20 | 21 | // Versions will get all the released Versions 22 | func Versions() []semver.Version { 23 | body := newClient().get(fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=100", brokerpak), "application/json") // max per page 24 | defer body.Close() 25 | data := must(io.ReadAll(body)) 26 | 27 | var receiver []struct { 28 | TagName string `json:"tag_name"` 29 | } 30 | if err := json.Unmarshal(data, &receiver); err != nil { 31 | panic(err) 32 | } 33 | 34 | var versions []semver.Version 35 | for _, r := range receiver { 36 | v := must(semver.ParseTolerant(r.TagName)) 37 | if len(v.Pre) == 0 { // skip pre-release 38 | versions = append(versions, v) 39 | } 40 | } 41 | sort.SliceStable(versions, func(i, j int) bool { return versions[i].LT(versions[j]) }) 42 | 43 | return versions 44 | } 45 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokerpaks/versions/versions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/brokerpaks" 7 | ) 8 | 9 | func main() { 10 | for _, v := range brokerpaks.Versions() { 11 | fmt.Println(v) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/broker.go: -------------------------------------------------------------------------------- 1 | // Package brokers manages service brokers 2 | package brokers 3 | 4 | import "csbbrokerpakazure/acceptance-tests/helpers/apps" 5 | 6 | type Broker struct { 7 | Name string 8 | username string 9 | password string 10 | secrets []EncryptionSecret 11 | dir string 12 | envExtras []apps.EnvVar 13 | app *apps.App 14 | } 15 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/create.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 11 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 12 | "csbbrokerpakazure/acceptance-tests/helpers/random" 13 | "csbbrokerpakazure/acceptance-tests/helpers/testpath" 14 | 15 | "github.com/onsi/gomega" 16 | ) 17 | 18 | type Option func(broker *Broker) 19 | 20 | func Create(opts ...Option) *Broker { 21 | broker := defaultConfig(opts...) 22 | 23 | brokerApp := apps.Push( 24 | apps.WithName(broker.Name), 25 | apps.WithDir(broker.dir), 26 | apps.WithManifest(newManifest( 27 | withName(broker.Name), 28 | withEnv(broker.env()...), 29 | )), 30 | ) 31 | 32 | schemaName := strings.ReplaceAll(broker.Name, "-", "_") 33 | cf.Run("bind-service", broker.Name, "csb-sql", "-c", fmt.Sprintf(`{"schema":"%s"}`, schemaName)) 34 | 35 | brokerApp.Start() 36 | 37 | cf.Run("create-service-broker", broker.Name, broker.username, broker.password, brokerApp.URL, "--space-scoped") 38 | 39 | broker.app = brokerApp 40 | return &broker 41 | } 42 | 43 | func WithOptions(opts ...Option) Option { 44 | return func(b *Broker) { 45 | for _, o := range opts { 46 | o(b) 47 | } 48 | } 49 | } 50 | 51 | func WithName(name string) Option { 52 | return func(b *Broker) { 53 | b.Name = name 54 | } 55 | } 56 | 57 | func WithPrefix(prefix string) Option { 58 | return func(b *Broker) { 59 | b.Name = random.Name(random.WithPrefix(prefix)) 60 | } 61 | } 62 | 63 | func WithSourceDir(dir string) Option { 64 | return func(b *Broker) { 65 | gomega.Expect(filepath.Join(dir, "cloud-service-broker")).To(gomega.BeAnExistingFile()) 66 | b.dir = dir 67 | } 68 | } 69 | func WithConfig(config map[string]interface{}, dir string) { 70 | 71 | bytes, err := json.Marshal(config) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | os.WriteFile(fmt.Sprintf("%s/config.yml", dir), bytes, 0666) 77 | 78 | } 79 | func WithEnv(env ...apps.EnvVar) Option { 80 | 81 | return func(b *Broker) { 82 | b.envExtras = append(b.envExtras, env...) 83 | } 84 | } 85 | 86 | func WithReleaseEnv(dir string) Option { 87 | return func(b *Broker) { 88 | b.envExtras = append(b.envExtras, readEnvrcServices(filepath.Join(dir, ".envrc"))...) 89 | } 90 | } 91 | 92 | func WithLatestEnv() Option { 93 | return func(b *Broker) { 94 | b.envExtras = append(b.envExtras, b.latestEnv()...) 95 | } 96 | } 97 | 98 | func WithUsername(username string) Option { 99 | return func(b *Broker) { 100 | b.username = username 101 | } 102 | } 103 | 104 | func WithPassword(password string) Option { 105 | return func(b *Broker) { 106 | b.password = password 107 | } 108 | } 109 | 110 | func defaultConfig(opts ...Option) (broker Broker) { 111 | defaults := []Option{ 112 | WithName(random.Name(random.WithPrefix("broker"))), 113 | WithSourceDir(testpath.BrokerpakRoot()), 114 | WithUsername(random.Name()), 115 | WithPassword(random.Password()), 116 | WithEncryptionSecret(random.Password()), 117 | } 118 | WithOptions(append(defaults, opts...)...)(&broker) 119 | return broker 120 | } 121 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/default.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 8 | 9 | "code.cloudfoundry.org/jsonry" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var defaultBrokerName string 14 | 15 | func DefaultBrokerName() string { 16 | if defaultBrokerName != "" { 17 | return defaultBrokerName 18 | } 19 | 20 | var receiver struct { 21 | Names []string `jsonry:"resources.name"` 22 | } 23 | out, _ := cf.Run("curl", "/v3/service_brokers") 24 | Expect(jsonry.Unmarshal([]byte(out), &receiver)).NotTo(HaveOccurred()) 25 | 26 | username := os.Getenv("USER") 27 | for _, n := range receiver.Names { 28 | if n == "broker-cf-test" || n == "cloud-service-broker-azure" { 29 | defaultBrokerName = n 30 | return n 31 | } 32 | 33 | if username != "" && n == fmt.Sprintf("csb-%s", username) { 34 | defaultBrokerName = n 35 | return n 36 | } 37 | } 38 | 39 | panic("could not determine default broker name") 40 | } 41 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/delete.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import ( 4 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 5 | ) 6 | 7 | func (b *Broker) Delete() { 8 | // This is implicit when deleting the app, but sometimes that fails, so this ensures the resource is freed 9 | cf.Run("unbind-service", b.Name, "csb-sql") 10 | 11 | cf.Run("delete-service-broker", b.Name, "-f") 12 | b.app.Delete() 13 | } 14 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/encryption.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import "code.cloudfoundry.org/jsonry" 4 | 5 | type EncryptionSecret struct { 6 | Password string `jsonry:"password.secret"` 7 | Label string `json:"label"` 8 | Primary bool `json:"primary"` 9 | } 10 | 11 | func (e *EncryptionSecret) MarshalJSON() ([]byte, error) { 12 | return jsonry.Marshal(e) 13 | } 14 | 15 | func WithEncryptionSecret(password string) Option { 16 | return func(b *Broker) { 17 | b.secrets = append(b.secrets, EncryptionSecret{ 18 | Password: password, 19 | Label: "default", 20 | Primary: true, 21 | }) 22 | } 23 | } 24 | 25 | func WithEncryptionSecrets(secrets ...EncryptionSecret) Option { 26 | return func(b *Broker) { 27 | b.secrets = secrets 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/env.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "csbbrokerpakazure/acceptance-tests/helpers/testpath" 8 | 9 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | ) 13 | 14 | func (b *Broker) env() []apps.EnvVar { 15 | var result []apps.EnvVar 16 | 17 | for name, required := range map[string]bool{ 18 | "ARM_SUBSCRIPTION_ID": true, 19 | "ARM_TENANT_ID": true, 20 | "ARM_CLIENT_ID": true, 21 | "ARM_CLIENT_SECRET": true, 22 | "GSB_BROKERPAK_BUILTIN_PATH": false, 23 | "GSB_PROVISION_DEFAULTS": false, 24 | "CH_CRED_HUB_URL": false, 25 | "CH_UAA_URL": false, 26 | "CH_UAA_CLIENT_NAME": false, 27 | "CH_UAA_CLIENT_SECRET": false, 28 | "CH_SKIP_SSL_VALIDATION": false, 29 | } { 30 | val, ok := os.LookupEnv(name) 31 | switch { 32 | case ok: 33 | result = append(result, apps.EnvVar{Name: name, Value: val}) 34 | case required: 35 | ginkgo.Fail(fmt.Sprintf("You must set the %s environment variable", name)) 36 | } 37 | } 38 | 39 | result = append(result, 40 | apps.EnvVar{Name: "SECURITY_USER_NAME", Value: b.username}, 41 | apps.EnvVar{Name: "SECURITY_USER_PASSWORD", Value: b.password}, 42 | apps.EnvVar{Name: "DB_TLS", Value: "skip-verify"}, 43 | apps.EnvVar{Name: "ENCRYPTION_ENABLED", Value: true}, 44 | apps.EnvVar{Name: "ENCRYPTION_PASSWORDS", Value: b.secrets}, 45 | apps.EnvVar{Name: "BROKERPAK_UPDATES_ENABLED", Value: true}, 46 | apps.EnvVar{Name: "TERRAFORM_UPGRADES_ENABLED", Value: true}, 47 | apps.EnvVar{Name: "CSB_DISABLE_TF_UPGRADE_PROVIDER_RENAMES", Value: true}, 48 | apps.EnvVar{Name: "GSB_COMPATIBILITY_ENABLE_GCP_DEPRECATED_SERVICES", Value: true}, 49 | ) 50 | 51 | return append(result, b.envExtras...) 52 | } 53 | 54 | func (b *Broker) latestEnv() []apps.EnvVar { 55 | return readEnvrcServices(testpath.BrokerpakFile(".envrc")) 56 | } 57 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/envrc.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 11 | 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var serviceMatcher = regexp.MustCompile(`^\s*export\s+(GSB_SERVICE_[\w_]+?)='(.*)'\s*$`) 16 | 17 | func readEnvrcServices(path string) (result []apps.EnvVar) { 18 | data, err := os.ReadFile(path) 19 | Expect(err).NotTo(HaveOccurred()) 20 | 21 | for _, line := range strings.Split(string(data), "\n") { 22 | m := serviceMatcher.FindStringSubmatch(line) 23 | const expectedNumberOfMatches = 3 24 | if len(m) != expectedNumberOfMatches { 25 | continue 26 | } 27 | name, rawValue := m[1], m[2] 28 | 29 | var r any 30 | Expect(json.Unmarshal([]byte(rawValue), &r)).To(Succeed(), func() string { 31 | return fmt.Sprintf("JSON parsing error %q for service %q: %s", err, name, rawValue) 32 | }) 33 | tidyValue, err := json.Marshal(r) 34 | Expect(err).NotTo(HaveOccurred()) 35 | 36 | result = append(result, apps.EnvVar{Name: name, Value: string(tidyValue)}) 37 | } 38 | 39 | return result 40 | } 41 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/manifest.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | func newManifest(opts ...manifestOption) string { 15 | 16 | m := manifestModel{ 17 | Version: 1, 18 | Applications: []applicationModel{ 19 | { 20 | Command: "./cloud-service-broker serve", 21 | Memory: "750MB", 22 | Disk: "2G", 23 | Buildpacks: []string{"binary_buildpack"}, 24 | RandomRoute: true, 25 | Environment: make(map[string]string), 26 | }, 27 | }, 28 | } 29 | 30 | for _, o := range opts { 31 | o(&m) 32 | } 33 | 34 | data, err := yaml.Marshal(m) 35 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 36 | 37 | dir := ginkgo.GinkgoT().TempDir() 38 | path := filepath.Join(dir, "manifest.yml") 39 | gomega.Expect(os.WriteFile(path, data, 0666)).To(gomega.Succeed()) 40 | 41 | return path 42 | } 43 | 44 | type manifestOption func(*manifestModel) 45 | 46 | type manifestModel struct { 47 | Version int `yaml:"version"` 48 | Applications []applicationModel `yaml:"applications"` 49 | } 50 | 51 | type applicationModel struct { 52 | Name string `yaml:"name"` 53 | Command string `yaml:"command"` 54 | Memory string `yaml:"memory,omitempty"` 55 | Disk string `yaml:"disk_quota,omitempty"` 56 | Buildpacks []string `yaml:"buildpacks,omitempty"` 57 | RandomRoute bool `yaml:"random-route"` 58 | Environment map[string]string `yaml:"env,omitempty"` 59 | } 60 | 61 | func withName(name string) manifestOption { 62 | return func(m *manifestModel) { 63 | m.Applications[0].Name = name 64 | } 65 | } 66 | func withCustomStartCommand(command string) manifestOption { 67 | return func(m *manifestModel) { 68 | m.Applications[0].Command = command 69 | } 70 | } 71 | 72 | func withEnv(env ...apps.EnvVar) manifestOption { 73 | return func(m *manifestModel) { 74 | for _, e := range env { 75 | m.Applications[0].Environment[e.Name] = e.ValueString() 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/brokers/update.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import ( 4 | "slices" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 7 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 8 | ) 9 | 10 | func (b *Broker) UpgradeBroker(dir string, env ...apps.EnvVar) { 11 | b.envExtras = slices.Concat(b.envExtras, b.latestEnv(), env) 12 | 13 | b.app.Push( 14 | apps.WithName(b.Name), 15 | apps.WithDir(dir), 16 | apps.WithStartedState(), 17 | apps.WithManifest(newManifest( 18 | withName(b.Name), 19 | withEnv(b.env()...), 20 | )), 21 | ) 22 | 23 | cf.Run("update-service-broker", b.Name, b.username, b.password, b.app.URL) 24 | } 25 | 26 | func (b *Broker) UpdateEnv(env ...apps.EnvVar) { 27 | WithEnv(env...)(b) 28 | b.app.SetEnv(env...) 29 | b.app.Restart() 30 | 31 | cf.Run("update-service-broker", b.Name, b.username, b.password, b.app.URL) 32 | } 33 | 34 | func (b *Broker) UpdateConfig(config map[string]interface{}) { 35 | b.app.Push( 36 | apps.WithName(b.Name), 37 | apps.WithYAMLFile("config.yml", config), 38 | apps.WithManifest(newManifest( 39 | withName(b.Name), 40 | withEnv(b.env()...), 41 | withCustomStartCommand("./cloud-service-broker --config config.yml serve"), 42 | )), 43 | ) 44 | 45 | b.app.CleanFileFromAppDir("config.yml") 46 | 47 | b.app.Start() 48 | } 49 | 50 | func (b *Broker) UpdateEncryptionSecrets(secrets ...EncryptionSecret) { 51 | WithEncryptionSecrets(secrets...) 52 | b.app.SetEnv(b.env()...) 53 | 54 | cf.Run("update-service-broker", b.Name, b.username, b.password, b.app.URL) 55 | } 56 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/cf/run.go: -------------------------------------------------------------------------------- 1 | // Package cf wraps the CF CLI 2 | package cf 3 | 4 | import ( 5 | "time" 6 | 7 | . "github.com/onsi/gomega" 8 | "github.com/onsi/gomega/gexec" 9 | ) 10 | 11 | func Run(args ...string) (string, string) { 12 | session := Start(args...) 13 | Eventually(session, time.Minute).Should(gexec.Exit(0)) 14 | return string(session.Out.Contents()), string(session.Err.Contents()) 15 | } 16 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/cf/start.go: -------------------------------------------------------------------------------- 1 | package cf 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gexec" 10 | ) 11 | 12 | func Start(args ...string) *gexec.Session { 13 | GinkgoWriter.Printf("Running: cf %s\n", strings.Join(args, " ")) 14 | command := exec.Command("cf", args...) 15 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 16 | Expect(err).NotTo(HaveOccurred()) 17 | return session 18 | } 19 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/environment/metadata.go: -------------------------------------------------------------------------------- 1 | // Package environment manages environment variables 2 | package environment 3 | 4 | import ( 5 | "os" 6 | 7 | "code.cloudfoundry.org/jsonry" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | type Metadata struct { 12 | ResourceGroup string `jsonry:"name"` 13 | PublicIP string `jsonry:"v2.vm.ssh_ip"` 14 | } 15 | 16 | func ReadMetadata() Metadata { 17 | file := os.Getenv("ENVIRONMENT_LOCK_METADATA") 18 | Expect(file).NotTo(BeEmpty(), "You must set the ENVIRONMENT_LOCK_METADATA environment variable") 19 | 20 | contents, err := os.ReadFile(file) 21 | Expect(err).NotTo(HaveOccurred()) 22 | 23 | var metadata Metadata 24 | Expect(jsonry.Unmarshal(contents, &metadata)).NotTo(HaveOccurred()) 25 | Expect(metadata.ResourceGroup).NotTo(BeEmpty()) 26 | return metadata 27 | } 28 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/lookupplan/lookup_plan.go: -------------------------------------------------------------------------------- 1 | // Package lookupplan is used for looking up plan information from services 2 | package lookupplan 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 9 | 10 | "code.cloudfoundry.org/jsonry" 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | // LookupByID looks up a plan by broker ID. There were historical bugs where duplicate plan ID were used, so we take in 16 | // the service offering name too 17 | func LookupByID(id, serviceOfferingName, serviceBrokerName string) string { 18 | data, _ := cf.Run("curl", fmt.Sprintf("/v3/service_plans?service_broker_names=%s&service_offering_names=%s", serviceBrokerName, serviceOfferingName)) 19 | 20 | var receiver struct { 21 | Resources []struct { 22 | Name string `json:"name"` 23 | ID string `jsonry:"broker_catalog.id"` 24 | } `json:"resources"` 25 | } 26 | Expect(jsonry.Unmarshal([]byte(data), &receiver)).To(Succeed()) 27 | 28 | var matches []string 29 | for _, e := range receiver.Resources { 30 | if e.ID == id { 31 | matches = append(matches, e.Name) 32 | } 33 | } 34 | 35 | switch len(matches) { 36 | case 0: 37 | Fail(fmt.Sprintf("could not find match for plan ID: %s", id)) 38 | case 1: 39 | // ok 40 | default: 41 | Fail(fmt.Sprintf("too many matches for plan ID %q: %s", id, strings.Join(matches, ", "))) 42 | } 43 | return matches[0] 44 | } 45 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/matchers/credhub_ref_matcher.go: -------------------------------------------------------------------------------- 1 | // Package matchers has custom Gomega matchers 2 | package matchers 3 | 4 | import "github.com/onsi/gomega" 5 | 6 | var HaveCredHubRef = gomega.HaveKey("credhub-ref") 7 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/plans/plans.go: -------------------------------------------------------------------------------- 1 | // Package plans provides plan helper functions 2 | package plans 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | 8 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 9 | 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | func ExistsAndAvailable(planName, offeringName, brokerName string) bool { 14 | plansJSON, err := cf.Run("curl", fmt.Sprintf("v3/service_plans?names=%s&service_broker_names=%s&service_offering_names=%s&available=true", planName, brokerName, offeringName)) 15 | Expect(err).To(BeEmpty()) 16 | 17 | type plan struct { 18 | GUID string `json:"guid"` 19 | } 20 | 21 | var receiver struct { 22 | Plans []plan `json:"resources"` 23 | } 24 | 25 | Expect(json.Unmarshal([]byte(plansJSON), &receiver)).NotTo(HaveOccurred()) 26 | return len(receiver.Plans) > 0 27 | } 28 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/random/hex.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func Hexadecimal(opts ...Option) string { 11 | length := cfg(append([]Option{WithMaxLength(20)}, opts...)).length 12 | buf := make([]byte, length/2) 13 | _, err := rand.Read(buf) 14 | Expect(err).NotTo(HaveOccurred()) 15 | return fmt.Sprintf("%x", buf) 16 | } 17 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/random/name.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/Pallinder/go-randomdata" 7 | ) 8 | 9 | // We have seen occasions where the same name has been returned more than once, so 10 | // we should keep a record of previous values to avoid duplicates 11 | var previous map[string]struct{} 12 | 13 | // Name returns a random name that cannot be longer (but may be shorter) than 30 14 | // characters, or a lower limit if specified. 15 | func Name(opts ...Option) string { 16 | c := cfg(append([]Option{WithMaxLength(100), WithDelimiter("-")}, opts...)) 17 | 18 | generate := func() string { 19 | parts := c.prefix 20 | 21 | // Do we have enough available length to add an adjective? 22 | if c.length > len(strings.Join(parts, c.delimiter))+20 { 23 | parts = append(parts, randomdata.Adjective()) 24 | } 25 | parts = append(parts, randomdata.Noun()) 26 | 27 | joined := strings.Join(parts, c.delimiter) 28 | if len(joined) > c.length { 29 | return joined[:c.length] 30 | } 31 | 32 | return joined 33 | } 34 | 35 | if previous == nil { 36 | previous = make(map[string]struct{}) 37 | } 38 | 39 | for { 40 | value := generate() 41 | if _, ok := previous[value]; !ok { 42 | previous[value] = struct{}{} 43 | return value 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/random/password.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "crypto/rand" 5 | "regexp" 6 | "strings" 7 | 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var ( 12 | firstLetter = regexp.MustCompile(`^[a-zA-Z]$`) 13 | subsequentLetter = regexp.MustCompile(`^[~_.a-zA-Z0-9]$`) 14 | ) 15 | 16 | func Password(opts ...Option) string { 17 | length := cfg(append([]Option{WithMaxLength(24)}, opts...)).length 18 | var s strings.Builder 19 | 20 | s.WriteByte(byteMatching(firstLetter)) 21 | 22 | for s.Len() < length { 23 | s.WriteByte(byteMatching(subsequentLetter)) 24 | } 25 | 26 | return s.String() 27 | } 28 | 29 | func byteMatching(re *regexp.Regexp) byte { 30 | buf := make([]byte, 1) 31 | for { 32 | _, err := rand.Read(buf) 33 | Expect(err).NotTo(HaveOccurred()) 34 | if re.MatchString(string(buf)) { 35 | return buf[0] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/random/random.go: -------------------------------------------------------------------------------- 1 | // Package random manages random test data 2 | package random 3 | 4 | type config struct { 5 | prefix []string 6 | delimiter string 7 | length int 8 | } 9 | 10 | type Option func(*config) 11 | 12 | func WithMaxLength(length int) Option { 13 | return func(c *config) { 14 | c.length = length 15 | } 16 | } 17 | 18 | func WithPrefix(prefix ...string) Option { 19 | return func(c *config) { 20 | c.prefix = append(c.prefix, prefix...) 21 | } 22 | } 23 | 24 | func WithDelimiter(delimiter string) Option { 25 | return func(c *config) { 26 | c.delimiter = delimiter 27 | } 28 | } 29 | 30 | func cfg(opts []Option) (c config) { 31 | for _, o := range opts { 32 | o(&c) 33 | } 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/servicekeys/create.go: -------------------------------------------------------------------------------- 1 | package servicekeys 2 | 3 | import ( 4 | "time" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 7 | "csbbrokerpakazure/acceptance-tests/helpers/random" 8 | 9 | . "github.com/onsi/gomega" 10 | . "github.com/onsi/gomega/gexec" 11 | ) 12 | 13 | const timeout = 10 * time.Minute 14 | 15 | type ServiceKey struct { 16 | name string 17 | serviceInstanceName string 18 | } 19 | 20 | func Create(serviceInstanceName string) *ServiceKey { 21 | name := random.Name() 22 | session := cf.Start("create-service-key", serviceInstanceName, name) 23 | Eventually(session).WithTimeout(timeout).Should(Exit(0)) 24 | 25 | return &ServiceKey{ 26 | name: name, 27 | serviceInstanceName: serviceInstanceName, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/servicekeys/delete.go: -------------------------------------------------------------------------------- 1 | // Package servicekeys manages service keys 2 | package servicekeys 3 | 4 | import ( 5 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 6 | 7 | . "github.com/onsi/gomega" 8 | . "github.com/onsi/gomega/gexec" 9 | ) 10 | 11 | func (s *ServiceKey) Delete() { 12 | session := cf.Start("delete-service-key", "-f", s.serviceInstanceName, s.name) 13 | Eventually(session).WithTimeout(timeout).Should(Exit(0)) 14 | } 15 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/bind.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 5 | "csbbrokerpakazure/acceptance-tests/helpers/bindings" 6 | ) 7 | 8 | func (s *ServiceInstance) Bind(app *apps.App) *bindings.Binding { 9 | return bindings.Bind(s.Name, app.Name) 10 | } 11 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/create.go: -------------------------------------------------------------------------------- 1 | // Package services manages service instances 2 | package services 3 | 4 | import ( 5 | "encoding/json" 6 | 7 | "csbbrokerpakazure/acceptance-tests/helpers/brokers" 8 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 9 | "csbbrokerpakazure/acceptance-tests/helpers/random" 10 | 11 | . "github.com/onsi/gomega" 12 | . "github.com/onsi/gomega/gexec" 13 | ) 14 | 15 | type ServiceInstance struct { 16 | Name string 17 | guid string 18 | } 19 | 20 | type config struct { 21 | name string 22 | serviceBrokerName func() string 23 | parameters string 24 | } 25 | 26 | type Option func(*config) 27 | 28 | func CreateInstance(offering, plan string, opts ...Option) *ServiceInstance { 29 | cfg := defaultConfig(offering, plan, opts...) 30 | args := []string{ 31 | "create-service", 32 | "--wait", 33 | offering, 34 | plan, 35 | cfg.name, 36 | "-b", 37 | cfg.serviceBrokerName(), 38 | } 39 | 40 | if cfg.parameters != "" { 41 | args = append(args, "-c", cfg.parameters) 42 | } 43 | 44 | session := cf.Start(args...) 45 | Eventually(session).WithTimeout(operationTimeout).Should(Exit(0), func() string { 46 | out, _ := cf.Run("service", cfg.name) 47 | return out 48 | }) 49 | 50 | return &ServiceInstance{Name: cfg.name} 51 | } 52 | 53 | func WithDefaultBroker() Option { 54 | return func(c *config) { 55 | c.serviceBrokerName = brokers.DefaultBrokerName 56 | } 57 | } 58 | 59 | func WithBroker(broker *brokers.Broker) Option { 60 | return func(c *config) { 61 | c.serviceBrokerName = func() string { return broker.Name } 62 | } 63 | } 64 | 65 | func WithParameters(parameters any) Option { 66 | return func(c *config) { 67 | switch p := parameters.(type) { 68 | case string: 69 | c.parameters = p 70 | default: 71 | params, err := json.Marshal(p) 72 | Expect(err).NotTo(HaveOccurred()) 73 | c.parameters = string(params) 74 | } 75 | } 76 | } 77 | 78 | func WithName(name string) Option { 79 | return func(c *config) { 80 | c.name = name 81 | } 82 | } 83 | 84 | func WithOptions(opts ...Option) Option { 85 | return func(c *config) { 86 | for _, o := range opts { 87 | o(c) 88 | } 89 | } 90 | } 91 | 92 | func defaultConfig(offering, plan string, opts ...Option) config { 93 | var cfg config 94 | WithOptions(append([]Option{ 95 | WithDefaultBroker(), 96 | WithName(random.Name(random.WithPrefix(offering, plan))), 97 | }, opts...)...)(&cfg) 98 | return cfg 99 | } 100 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/createservicekey.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "csbbrokerpakazure/acceptance-tests/helpers/servicekeys" 4 | 5 | func (s *ServiceInstance) CreateServiceKey() *servicekeys.ServiceKey { 6 | return servicekeys.Create(s.Name) 7 | } 8 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/delete.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 5 | 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gexec" 8 | ) 9 | 10 | func (s *ServiceInstance) Delete() { 11 | Delete(s.Name) 12 | } 13 | 14 | func Delete(name string) { 15 | session := cf.Start("delete-service", "-f", name, "--wait") 16 | Eventually(session).WithTimeout(operationTimeout).Should(Exit(0)) 17 | } 18 | 19 | func (s *ServiceInstance) Purge() { 20 | cf.Run("purge-service-instance", "-f", s.Name) 21 | } 22 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/guid.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "strings" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 7 | ) 8 | 9 | func (s *ServiceInstance) GUID() string { 10 | if s.guid == "" { 11 | out, _ := cf.Run("service", s.Name, "--guid") 12 | s.guid = strings.TrimSpace(out) 13 | } 14 | 15 | return s.guid 16 | } 17 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/service_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestServices(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Services test Suite") 13 | } 14 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/services.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "time" 4 | 5 | const operationTimeout = time.Hour 6 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/update.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 5 | 6 | . "github.com/onsi/gomega" 7 | . "github.com/onsi/gomega/gexec" 8 | ) 9 | 10 | func (s *ServiceInstance) Update(parameters ...string) { 11 | args := append([]string{"update-service", s.Name, "--wait"}, parameters...) 12 | 13 | session := cf.Start(args...) 14 | Eventually(session).WithTimeout(operationTimeout).Should(Exit(0), func() string { 15 | out, _ := cf.Run("service", s.Name) 16 | return out 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/services/upgrade.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | . "github.com/onsi/gomega/gexec" 10 | 11 | "csbbrokerpakazure/acceptance-tests/helpers/cf" 12 | ) 13 | 14 | func (s *ServiceInstance) Upgrade() { 15 | if !s.UpgradeAvailable() { 16 | GinkgoWriter.Println("No Upgrade available for service instance") 17 | return 18 | } 19 | 20 | session := cf.Start("upgrade-service", s.Name, "--force", "--wait") 21 | Eventually(session).WithTimeout(operationTimeout).Should(Exit(0), func() string { 22 | out, _ := cf.Run("service", s.Name) 23 | return out 24 | }) 25 | 26 | out, _ := cf.Run("service", s.Name) 27 | Expect(out).To(MatchRegexp(`status:\s+update succeeded`)) 28 | 29 | Expect(s.UpgradeAvailable()).To(BeFalse(), "service instance has an upgrade available after upgrade") 30 | } 31 | 32 | func (s *ServiceInstance) UpgradeAvailable() bool { 33 | out, _ := cf.Run("curl", fmt.Sprintf("/v3/service_instances/%s", s.GUID())) 34 | 35 | var receiver struct { 36 | UpgradeAvailable bool `json:"upgrade_available"` 37 | } 38 | Expect(json.Unmarshal([]byte(out), &receiver)).NotTo(HaveOccurred()) 39 | return receiver.UpgradeAvailable 40 | } 41 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/testpath/brokerpak_file.go: -------------------------------------------------------------------------------- 1 | package testpath 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "path/filepath" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func BrokerpakFile(parts ...string) string { 13 | GinkgoHelper() 14 | 15 | r := BrokerpakRoot() 16 | p := filepath.Join(append([]string{r}, parts...)...) 17 | Expect(p).To(BeAnExistingFile(), func() string { 18 | return fmt.Sprintf("could not find file %q in brokerpak %q", path.Join(parts...), r) 19 | }) 20 | 21 | return p 22 | } 23 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/testpath/brokerpak_root.go: -------------------------------------------------------------------------------- 1 | package testpath 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/onsi/ginkgo/v2" 9 | ) 10 | 11 | // BrokerpakRoot searches upwards from the current working directory to find the root path of the brokerpak 12 | // Fails the test if not found 13 | func BrokerpakRoot() string { 14 | ginkgo.GinkgoHelper() 15 | 16 | cwd, err := os.Getwd() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | d, err := filepath.Abs(cwd) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | for { 27 | switch { 28 | case Exists(filepath.Join(d, "manifest.yml")) && Exists(filepath.Join(d, "acceptance-tests")): 29 | return d 30 | case d == "/": 31 | ginkgo.Fail(fmt.Sprintf("could not determine brokerpak root from %q", cwd)) 32 | default: 33 | d = filepath.Dir(d) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /acceptance-tests/helpers/testpath/exists.go: -------------------------------------------------------------------------------- 1 | // Package testpath provides path utilities for tests 2 | package testpath 3 | 4 | import ( 5 | "os" 6 | ) 7 | 8 | // Exists returns whether a path exists 9 | func Exists(path string) bool { 10 | _, err := os.Stat(path) 11 | return err == nil 12 | } 13 | -------------------------------------------------------------------------------- /acceptance-tests/mongodb_test.go: -------------------------------------------------------------------------------- 1 | package acceptance_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 7 | "csbbrokerpakazure/acceptance-tests/helpers/az" 8 | "csbbrokerpakazure/acceptance-tests/helpers/matchers" 9 | "csbbrokerpakazure/acceptance-tests/helpers/random" 10 | "csbbrokerpakazure/acceptance-tests/helpers/services" 11 | 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | // Tests the *csb-azure-mongodb* service offering 17 | // Uses the *default broker* 18 | var _ = Describe("MongoDB", Label("mongodb"), func() { 19 | It("can be accessed by an app", func() { 20 | By("creating a service instance") 21 | databaseName := random.Name(random.WithPrefix("database")) 22 | collectionName := random.Name(random.WithPrefix("collection")) 23 | serviceInstance := services.CreateInstance( 24 | "csb-azure-mongodb", 25 | "small", services.WithParameters(map[string]any{ 26 | "db_name": databaseName, 27 | "collection_name": collectionName, 28 | "shard_key": "_id", 29 | "indexes": "_id", 30 | "unique_indexes": "", 31 | "server_version": "4.0", 32 | }), 33 | ) 34 | defer serviceInstance.Delete() 35 | 36 | By("changing the firewall to allow comms") 37 | serviceName := fmt.Sprintf("csb%s", serviceInstance.GUID()) 38 | az.Run("cosmosdb", "update", "--ip-range-filter", metadata.PublicIP, "--name", serviceName, "--resource-group", metadata.ResourceGroup) 39 | 40 | By("pushing the unstarted app twice") 41 | appOne := apps.Push(apps.WithApp(apps.MongoDB)) 42 | appTwo := apps.Push(apps.WithApp(apps.MongoDB)) 43 | defer apps.Delete(appOne, appTwo) 44 | 45 | By("binding the apps to the MongoDB service instance") 46 | binding := serviceInstance.Bind(appOne) 47 | serviceInstance.Bind(appTwo) 48 | 49 | By("starting the apps") 50 | apps.Start(appOne, appTwo) 51 | 52 | By("checking that the app environment has a credhub reference for credentials") 53 | Expect(binding.Credential()).To(matchers.HaveCredHubRef) 54 | 55 | By("checking that the specified database has been created") 56 | databases := appOne.GET("") 57 | Expect(databases).To(MatchJSON(fmt.Sprintf(`["%s"]`, databaseName))) 58 | 59 | By("checking that the specified collection has been created") 60 | collections := appOne.GET(databaseName) 61 | Expect(collections).To(MatchJSON(fmt.Sprintf(`["%s"]`, collectionName))) 62 | 63 | By("creating a document using the first app") 64 | documentName := random.Hexadecimal() 65 | documentData := random.Hexadecimal() 66 | appOne.PUTf(documentData, "%s/%s/%s", databaseName, collectionName, documentName) 67 | 68 | By("getting the document using the second app") 69 | got := appTwo.GETf("%s/%s/%s", databaseName, collectionName, documentName) 70 | Expect(got).To(Equal(documentData)) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /acceptance-tests/mssql_fog_existing_test.go: -------------------------------------------------------------------------------- 1 | package acceptance_test 2 | 3 | import ( 4 | "context" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 7 | "csbbrokerpakazure/acceptance-tests/helpers/brokers" 8 | "csbbrokerpakazure/acceptance-tests/helpers/matchers" 9 | "csbbrokerpakazure/acceptance-tests/helpers/mssqlserver" 10 | "csbbrokerpakazure/acceptance-tests/helpers/random" 11 | "csbbrokerpakazure/acceptance-tests/helpers/services" 12 | 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | // Tests the *csb-azure-mssql-db-failover-group* with the *existing* property that allows it to adopt and existing 18 | // failover group rather than creating a new one. 19 | // Does NOT use the default broker: deploys a custom-configured broker 20 | var _ = Describe("MSSQL Failover Group Existing", Label("mssql-db-failover-group-existing"), func() { 21 | It("can be accessed by an app", func() { 22 | ctx := context.Background() 23 | 24 | By("creating primary and secondary DB servers in their resource group") 25 | serversConfig, err := mssqlserver.CreateServerPair(ctx, metadata, subscriptionID) 26 | Expect(err).NotTo(HaveOccurred()) 27 | 28 | DeferCleanup(func() { 29 | By("deleting the created resource group and DB servers") 30 | _ = mssqlserver.Cleanup(ctx, serversConfig, subscriptionID) 31 | }) 32 | 33 | By("deploying the CSB") 34 | 35 | serviceBroker := brokers.Create( 36 | brokers.WithPrefix("csb-mssql-db-fog"), 37 | brokers.WithLatestEnv(), 38 | brokers.WithEnv(apps.EnvVar{Name: "MSSQL_DB_FOG_SERVER_PAIR_CREDS", Value: serversConfig.ServerPairsConfig()}), 39 | ) 40 | defer serviceBroker.Delete() 41 | 42 | By("creating a failover group service instance") 43 | fogConfig := map[string]string{ 44 | "instance_name": random.Name(random.WithPrefix("fog")), 45 | "db_name": random.Name(random.WithPrefix("db")), 46 | "server_pair": serversConfig.ServerPairTag, 47 | } 48 | 49 | const serviceOffering = "csb-azure-mssql-db-failover-group" 50 | const servicePlan = "medium" 51 | serviceName := random.Name(random.WithPrefix(serviceOffering, servicePlan)) 52 | // CreateInstance can fail and can leave a service record (albeit a failed one) lying around. 53 | // We can't delete service brokers that have serviceInstances, so we need to ensure the service instance 54 | // is cleaned up regardless as to whether it wa successful. This is important when we use our own service broker 55 | // (which can only have 5 instances at any time) to prevent subsequent test failures. 56 | defer services.Delete(serviceName) 57 | initialFogInstance := services.CreateInstance( 58 | serviceOffering, 59 | servicePlan, 60 | services.WithBroker(serviceBroker), 61 | services.WithParameters(fogConfig), 62 | services.WithName(serviceName), 63 | ) 64 | 65 | By("pushing an unstarted app") 66 | app := apps.Push(apps.WithApp(apps.MSSQL)) 67 | 68 | By("binding the app to the initial failover group service instance") 69 | initialFogInstance.Bind(app) 70 | 71 | By("starting the app") 72 | apps.Start(app) 73 | 74 | By("creating a schema") 75 | schema := random.Name(random.WithMaxLength(10)) 76 | app.PUTf("", "%s?dbo=false", schema) 77 | 78 | By("setting a key-value") 79 | key := random.Hexadecimal() 80 | value := random.Hexadecimal() 81 | app.PUTf(value, "%s/%s", schema, key) 82 | 83 | By("connecting to the existing failover group") 84 | const servicePlanExisting = "existing" 85 | serviceNameExisting := random.Name(random.WithPrefix(serviceOffering, servicePlanExisting)) 86 | defer services.Delete(serviceNameExisting) 87 | dbFogInstance := services.CreateInstance( 88 | serviceOffering, 89 | servicePlanExisting, 90 | services.WithBroker(serviceBroker), 91 | services.WithParameters(fogConfig), 92 | services.WithName(serviceNameExisting), 93 | ) 94 | 95 | By("purging the initial FOG instance") 96 | initialFogInstance.Purge() 97 | 98 | By("binding the app to the CSB service instance") 99 | bindingTwo := dbFogInstance.Bind(app) 100 | defer apps.Delete(app) // app needs to be deleted before service instance 101 | 102 | By("checking that the app environment has a credhub reference for credentials") 103 | Expect(bindingTwo.Credential()).To(matchers.HaveCredHubRef) 104 | 105 | By("getting the value set with the initial binding") 106 | got := app.GETf("%s/%s", schema, key) 107 | Expect(got).To(Equal(value)) 108 | 109 | By("dropping the schema using the app") 110 | app.DELETE(schema) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /acceptance-tests/passwordrotation_test.go: -------------------------------------------------------------------------------- 1 | package acceptance_test 2 | 3 | import ( 4 | "csbbrokerpakazure/acceptance-tests/helpers/brokers" 5 | "csbbrokerpakazure/acceptance-tests/helpers/random" 6 | "csbbrokerpakazure/acceptance-tests/helpers/services" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | ) 10 | 11 | // Tests rotation of the encryption password using the *csb-azure-mongodb* service offering 12 | // Does NOT use the default broker: deploys a custom-configured broker 13 | var _ = Describe("Password Rotation", Label("passwordrotation"), func() { 14 | It("should reencrypt the DB when keys are rotated", func() { 15 | By("creating a service broker with an encryption secret") 16 | firstEncryptionSecret := random.Password() 17 | serviceBroker := brokers.Create( 18 | brokers.WithPrefix("csb-rotation"), 19 | brokers.WithLatestEnv(), 20 | brokers.WithEncryptionSecrets(brokers.EncryptionSecret{ 21 | Password: firstEncryptionSecret, 22 | Label: "default", 23 | Primary: true, 24 | }), 25 | ) 26 | defer serviceBroker.Delete() 27 | 28 | By("creating a service") 29 | databaseName := random.Name(random.WithPrefix("database")) 30 | collectionName := random.Name(random.WithPrefix("collection")) 31 | 32 | const serviceOffering = "csb-azure-mongodb" 33 | const servicePlan = "small" 34 | serviceName := random.Name(random.WithPrefix(serviceOffering, servicePlan)) 35 | // CreateInstance can fail and can leave a service record (albeit a failed one) lying around. 36 | // We can't delete service brokers that have serviceInstances, so we need to ensure the service instance 37 | // is cleaned up regardless as to whether it wa successful. This is important when we use our own service broker 38 | // (which can only have 5 instances at any time) to prevent subsequent test failures. 39 | defer services.Delete(serviceName) 40 | serviceInstance := services.CreateInstance( 41 | serviceOffering, 42 | servicePlan, 43 | services.WithBroker(serviceBroker), 44 | services.WithParameters(map[string]any{ 45 | "db_name": databaseName, 46 | "collection_name": collectionName, 47 | "shard_key": "_id", 48 | "indexes": "_id", 49 | "unique_indexes": "", 50 | }), 51 | services.WithName(serviceName), 52 | ) 53 | 54 | By("adding a new encryption secret") 55 | secondEncryptionSecret := random.Password() 56 | serviceBroker.UpdateEncryptionSecrets( 57 | brokers.EncryptionSecret{ 58 | Password: firstEncryptionSecret, 59 | Label: "default", 60 | Primary: false, 61 | }, 62 | brokers.EncryptionSecret{ 63 | Password: secondEncryptionSecret, 64 | Label: "second-password", 65 | Primary: true, 66 | }, 67 | ) 68 | 69 | By("creating a service key") 70 | sk1 := serviceInstance.CreateServiceKey() 71 | defer sk1.Delete() 72 | 73 | By("removing the original encryption secret") 74 | serviceBroker.UpdateEncryptionSecrets( 75 | brokers.EncryptionSecret{ 76 | Password: secondEncryptionSecret, 77 | Label: "second-password", 78 | Primary: true, 79 | }, 80 | ) 81 | 82 | By("creating a new service key") 83 | sk2 := serviceInstance.CreateServiceKey() 84 | defer sk2.Delete() 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /acceptance-tests/redis_test.go: -------------------------------------------------------------------------------- 1 | package acceptance_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "csbbrokerpakazure/acceptance-tests/helpers/apps" 7 | "csbbrokerpakazure/acceptance-tests/helpers/az" 8 | "csbbrokerpakazure/acceptance-tests/helpers/matchers" 9 | "csbbrokerpakazure/acceptance-tests/helpers/random" 10 | "csbbrokerpakazure/acceptance-tests/helpers/services" 11 | 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | // Tests the *csb-azure-redis* service offering 17 | // Uses the *default broker* 18 | var _ = Describe("Redis", Label("redis"), func() { 19 | It("can be accessed by an app", func() { 20 | By("creating a service instance") 21 | serviceInstance := services.CreateInstance("csb-azure-redis", "deprecated-small") 22 | defer serviceInstance.Delete() 23 | 24 | By("updating the firewall to allow comms") 25 | serviceName := fmt.Sprintf("csb-redis-%s", serviceInstance.GUID()) 26 | az.Run("redis", 27 | "firewall-rules", 28 | "create", 29 | "--name", serviceName, 30 | "--resource-group", metadata.ResourceGroup, 31 | "--rule-name", "allowtestrule", 32 | "--start-ip", metadata.PublicIP, 33 | "--end-ip", metadata.PublicIP, 34 | ) 35 | 36 | By("pushing the unstarted app twice") 37 | appOne := apps.Push(apps.WithApp(apps.Redis)) 38 | appTwo := apps.Push(apps.WithApp(apps.Redis)) 39 | defer apps.Delete(appOne, appTwo) 40 | 41 | By("binding the apps to the Redis service instance") 42 | binding := serviceInstance.Bind(appOne) 43 | serviceInstance.Bind(appTwo) 44 | 45 | By("starting the apps") 46 | apps.Start(appOne, appTwo) 47 | 48 | By("checking that the app environment has a credhub reference for credentials") 49 | Expect(binding.Credential()).To(matchers.HaveCredHubRef) 50 | 51 | By("setting a key-value using the first app") 52 | key := random.Hexadecimal() 53 | value := random.Hexadecimal() 54 | appOne.PUT(value, key) 55 | 56 | By("getting the value using the second app") 57 | got := appTwo.GET(key) 58 | Expect(got).To(Equal(value)) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /acceptance-tests/upgrade/.gitignore: -------------------------------------------------------------------------------- 1 | versions 2 | -------------------------------------------------------------------------------- /acceptance-tests/upgrade/README.md: -------------------------------------------------------------------------------- 1 | ## Upgrade tests 2 | 3 | These tests perform the following flow: 4 | - Push version A of this brokerpak (with broker) 5 | - Create some resources 6 | - Push version B of this brokerpak (with broker) 7 | - Check that the resources are still accessible 8 | - Create some more resources 9 | 10 | ### Usage 11 | By default, version A will be the highest brokerpak version on GitHub and 12 | version B will be whatever has been built locally with `make build`. 13 | 14 | ### Specifying version A 15 | To specify version A, use the `-from-version` flag: 16 | ``` 17 | ginkgo -v --label-filter