From 9ba46e15784a241cac84d00e147d4bb71eca3f75 Mon Sep 17 00:00:00 2001 From: Ollie Hughes Date: Mon, 6 Aug 2018 18:04:42 +0100 Subject: [PATCH] Add configuration to stop and start a GCP host BOSH Lite in the CircleCI pipeline Template credentials required by BOSH from Circle CI environment variables Script to create CircleCI environments from file in LastPass Ensure only a single job runs at a time so that there is not a deadlock over BOSH lite environment (in future multiple envs will be enabled) Update Docker file to include bbl and bosh cli Ensure space and org created in CF before running test Run Unit Tests and ATs as seperate steps Reduce use of .block() in test set up check environment variables exist before creating in circleci use springcloudservices docker repo Use different property prefix for ATs to avoid clashing Fix deleteServiceInstance so it checks if SI present first to avoid error Ensure logging is configured to not leak secrets in CircleCI output Add script to wait for BOSH to release all it's locks before continuing [closes #45] --- .circleci/config.yml | 59 +-- .gitignore | 6 + build.gradle | 4 +- ci/Dockerfile | 13 + ci/README.md | 50 +++ .../bbl-state/bbl-state.json.tmpl | 42 ++ .../bbl-state/director-vars-file.yml.tmpl | 1 + .../bbl-state/director-vars-store.yml.tmpl | 13 + .../bbl-state/jumpbox-vars-store.yml.tmpl | 3 + .../bbl-state/vars/.gitkeep | 0 ci/config.yml | 108 +++++ {config => ci/config}/pmdRuleSet.xml | 0 {config => ci/config}/pmdTestRuleSet.xml | 0 ci/scripts/delete-all-envvars-in-circle.sh | 44 +++ ci/scripts/export-envvars-to-circle-ci.sh | 77 ++++ ci/scripts/fix-bbl-state-json-line-breaks.py | 12 + .../populate-secrets-from-environment.sh | 48 +++ ci/scripts/run-local-ci-job.sh | 27 ++ .../wait-for-bosh-locks-to-be-released.sh | 33 ++ ci/scripts/wait-for-other-builds-to-finish.sh | 40 ++ .../README.adoc | 32 +- .../AcceptanceTestProperties.java | 36 ++ .../CloudFoundryAcceptanceTest.java | 76 ++-- .../fixtures/cf/CloudFoundryProperties.java | 7 +- .../fixtures/cf/CloudFoundryService.java | 373 +++++++++++------- .../src/test/resources/application.yml | 3 +- .../{logback.xml => logback-test.xml} | 0 27 files changed, 847 insertions(+), 260 deletions(-) mode change 100644 => 120000 .circleci/config.yml create mode 100644 ci/Dockerfile create mode 100644 ci/README.md create mode 100644 ci/bbl-bosh-lite-environment/bbl-state/bbl-state.json.tmpl create mode 100644 ci/bbl-bosh-lite-environment/bbl-state/director-vars-file.yml.tmpl create mode 100644 ci/bbl-bosh-lite-environment/bbl-state/director-vars-store.yml.tmpl create mode 100644 ci/bbl-bosh-lite-environment/bbl-state/jumpbox-vars-store.yml.tmpl create mode 100644 ci/bbl-bosh-lite-environment/bbl-state/vars/.gitkeep create mode 100644 ci/config.yml rename {config => ci/config}/pmdRuleSet.xml (100%) rename {config => ci/config}/pmdTestRuleSet.xml (100%) create mode 100755 ci/scripts/delete-all-envvars-in-circle.sh create mode 100755 ci/scripts/export-envvars-to-circle-ci.sh create mode 100755 ci/scripts/fix-bbl-state-json-line-breaks.py create mode 100755 ci/scripts/populate-secrets-from-environment.sh create mode 100755 ci/scripts/run-local-ci-job.sh create mode 100755 ci/scripts/wait-for-bosh-locks-to-be-released.sh create mode 100755 ci/scripts/wait-for-other-builds-to-finish.sh create mode 100644 spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/AcceptanceTestProperties.java rename spring-cloud-app-broker-acceptance-tests/src/test/resources/{logback.xml => logback-test.xml} (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 45090e7..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,58 +0,0 @@ -# Java Gradle CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-java/ for more details -# -version: 2 -jobs: - build: - docker: - - image: circleci/openjdk:8-jdk - - working_directory: ~/repo - - environment: - JAVA_TOOL_OPTIONS: "-Xms512m -Xmx2048m" - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx512m -Xmx2048m" -Dorg.gradle.daemon=false' - TERM: dumb - - steps: - - checkout - - - restore_cache: - key: sc-app-broker-{{ .Branch }} - - - run: - name: "Download dependencies" - command: ./gradlew assemble - - - save_cache: - paths: - - ~/.gradle - key: sc-app-broker-{{ .Branch }} - - - run: - name: "Disable Gradle daemon" - command: touch ~/.gradle/gradle.properties && echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties - - - run: - name: "Run tests" - command: ./gradlew clean codeCoverageReport --stacktrace --info --continue - - - run: - name: "Aggregate test results" - when: always - command: | - mkdir -p ~/junit/ - find . -type f -regex ".*/build/test-results/.*" -exec cp {} ~/junit/ \; - - - run: - name: "Upload tests coverage" - command: bash <(curl -s https://codecov.io/bash) - - - store_artifacts: - path: ~/junit/ - destination: artifacts - - - store_test_results: - path: ~/junit/ - destination: testartifacts \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 120000 index 0000000..ec04fc7 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1 @@ +../ci/config.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8894061..251dd43 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ target bin .idea out +.envrc +bbl-state.json +director-vars-store.yml +jumpbox-vars-store.yml +director-vars-file.yml +bbl-env.sh \ No newline at end of file diff --git a/build.gradle b/build.gradle index 27e2851..183a396 100644 --- a/build.gradle +++ b/build.gradle @@ -306,11 +306,11 @@ configure(rootProject) { } pmd { - ruleSetFiles = files("${project.rootDir}/config/pmdRuleSet.xml") + ruleSetFiles = files("${project.rootDir}/ci/config/pmdRuleSet.xml") } pmdTest { - ruleSetFiles = files("${project.rootDir}/config/pmdTestRuleSet.xml") + ruleSetFiles = files("${project.rootDir}/ci/config/pmdTestRuleSet.xml") } task codeCoverageReport(type: JacocoReport) { diff --git a/ci/Dockerfile b/ci/Dockerfile new file mode 100644 index 0000000..115ba49 --- /dev/null +++ b/ci/Dockerfile @@ -0,0 +1,13 @@ +FROM circleci/openjdk:8-jdk + +RUN BOSH_URL="https://s3.amazonaws.com/bosh-cli-artifacts/bosh-cli-5.0.1-linux-amd64"&& \ + sudo curl --silent --show-error --location --fail --retry 3 --output /usr/bin/bosh $BOSH_URL && \ + sudo chmod +x /usr/bin/bosh && \ + bosh -v + +RUN BBL_URL="https://github.com/cloudfoundry/bosh-bootloader/releases/download/v6.8.4/bbl-v6.8.4_linux_x86-64" && \ + sudo curl --silent --show-error --location --fail --retry 3 --output /usr/bin/bbl $BBL_URL && \ + sudo chmod +x /usr/bin/bbl && \ + bbl -v + +RUN sudo apt-get install python3 \ No newline at end of file diff --git a/ci/README.md b/ci/README.md new file mode 100644 index 0000000..0693d81 --- /dev/null +++ b/ci/README.md @@ -0,0 +1,50 @@ +#App Broker CI Pipeline + +### Managing BOSH Lite on GCP + +Currently CircleCI is used to run the acceptance tests. The system under test is a BOSH Lite, provisioned using [BOSH Bootloader](https://github.com/cloudfoundry/bosh-bootloader) + +The BOSH Lite director can be targeted using following steps; + +* Install bosh-cli and bbl + ``` + $ brew tap cloudfoundry/tap + $ brew install bosh-cli + $ brew install bbl + ``` +* Setup BBL environment variables + ``` + lpass show --notes 7083089362665788436 > ci/.envrc + source ci/.envrc + ./scripts/populate-secrets-from-environment.sh + ./scripts/fix-bbl-state-json-line-breaks.py + cd bbl-bosh-lite-environment/bbl-state + bbl print-env > bbl-env.sh && source bbl-env.sh + bosh deployments + ``` + +### Running CI pipeline locally +CircleCI allows pipelines to be run locally, this is very useful development purposes + +* Install circle command line tool to run local builds: + ```$bash + curl -o /usr/local/bin/circleci https://circle-downloads.s3.amazonaws.com/releases/build_agent_wrapper/circleci && \ + chmod +x /usr/local/bin/circleci + ``` +* Run script to execute pipeline +`ci/scripts/run-local-ci-job.sh` + +### Credentials +CircleCI uses environment variables to store credentials (these are stored inside +a Vault instance within the CircleCI server). Environment variables need to +be uploaded to circle so they can be used by the build. + +#### Adding a new variable or credential +* Edit the note in LastPass, adding the new variable: +`lpass edit --notes 7083089362665788436 --sync=now` +* Source all the variables in your local environment + ``` + lpass show --notes 7083089362665788436 > ci/.envrc + source ci/.envrc + ``` +* Upload the variables to CircleCi: `ci/scripts/export-envvars-to-cirle.sh` diff --git a/ci/bbl-bosh-lite-environment/bbl-state/bbl-state.json.tmpl b/ci/bbl-bosh-lite-environment/bbl-state/bbl-state.json.tmpl new file mode 100644 index 0000000..fa3eb85 --- /dev/null +++ b/ci/bbl-bosh-lite-environment/bbl-state/bbl-state.json.tmpl @@ -0,0 +1,42 @@ +{ + "version": 14, + "bblVersion": "6.8.4", + "iaas": "gcp", + "id": "<%= bblEnvId %>", + "envID": "appbroker-env", + "noDirector": false, + "aws": {}, + "azure": {}, + "gcp": { + "zone": "europe-west2-a", + "region": "europe-west2", + "zones": [ + "europe-west2-a", + "europe-west2-b", + "europe-west2-c" + ] + }, + "vsphere": {}, + "openstack": {}, + "jumpbox": { + "url": "<%= jumpboxUrl %>" + }, + "bosh": { + "directorName": "bosh-appbroker-env", + "directorUsername": "<%= directorUsername %>", + "directorPassword": "<%= directorPassword %>", + "directorAddress": "https://<%= directorInternalIp %>:<%= directorPort %>", + "directorSSLCA": <%= directorSSLCA %>, + "directorSSLCertificate": <%= directorSSLCertificate %>, + "directorSSLPrivateKey": <%= directorSSLPrivateKey %> + }, + "tfState": "", + "lb": { + "type": "cf", + "cert": <%= lbCert %>, + "key": <%= lbKey %>, + "chain": "", + "domain": "<%= lbDomain %>" + }, + "latestTFOutput": "" +} \ No newline at end of file diff --git a/ci/bbl-bosh-lite-environment/bbl-state/director-vars-file.yml.tmpl b/ci/bbl-bosh-lite-environment/bbl-state/director-vars-file.yml.tmpl new file mode 100644 index 0000000..287f157 --- /dev/null +++ b/ci/bbl-bosh-lite-environment/bbl-state/director-vars-file.yml.tmpl @@ -0,0 +1 @@ +internal_ip: <%= directorInternalIp %> \ No newline at end of file diff --git a/ci/bbl-bosh-lite-environment/bbl-state/director-vars-store.yml.tmpl b/ci/bbl-bosh-lite-environment/bbl-state/director-vars-store.yml.tmpl new file mode 100644 index 0000000..8f29121 --- /dev/null +++ b/ci/bbl-bosh-lite-environment/bbl-state/director-vars-store.yml.tmpl @@ -0,0 +1,13 @@ +admin_password: <%= directorAdminPassword %> +credhub_admin_client_secret: <%= credhubAdminClientSecret %> +credhub_ca: + certificate: | + <%= credhubCA %> + +uaa_ssl: + certificate: | + <%= uaaCert %> + +jumpbox_ssh: + private_key: | + <%= directorJumpboxSshPrivateKey %> diff --git a/ci/bbl-bosh-lite-environment/bbl-state/jumpbox-vars-store.yml.tmpl b/ci/bbl-bosh-lite-environment/bbl-state/jumpbox-vars-store.yml.tmpl new file mode 100644 index 0000000..7ad06cf --- /dev/null +++ b/ci/bbl-bosh-lite-environment/bbl-state/jumpbox-vars-store.yml.tmpl @@ -0,0 +1,3 @@ +jumpbox_ssh: + private_key: | + <%= jumpboxPrivateKey %> diff --git a/ci/bbl-bosh-lite-environment/bbl-state/vars/.gitkeep b/ci/bbl-bosh-lite-environment/bbl-state/vars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ci/config.yml b/ci/config.yml new file mode 100644 index 0000000..09530a3 --- /dev/null +++ b/ci/config.yml @@ -0,0 +1,108 @@ +# Java Gradle CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-java/ for more details +# +version: 2 +jobs: + build: + docker: + - image: springcloudservices/spring-cloud-app-broker-ci:v1 + + working_directory: ~/repo + + environment: + JAVA_TOOL_OPTIONS: "-Xms512m -Xmx2048m" + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx512m -Xmx2048m" -Dorg.gradle.daemon=false' + TERM: dumb + + steps: + - checkout + + - restore_cache: + key: sc-app-broker-{{ .Branch }} + + - run: + name: "Queue until all other builds finished" + command: | + cd ~/repo/ci/scripts + ./wait-for-other-builds-to-finish.sh 60 + + - run: + name: "Download dependencies" + command: ./gradlew assemble + + - save_cache: + paths: + - ~/.gradle + key: sc-app-broker-{{ .Branch }} + + - run: + name: "Disable Gradle daemon" + command: touch ~/.gradle/gradle.properties && echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties + + - run: + name: "Run unit tests" + command: ./gradlew clean codeCoverageReport --stacktrace + + - run: + name: "Aggregate test results" + when: always + command: | + mkdir -p ~/junit/ + find . -type f -regex ".*/build/test-results/.*" -exec cp {} ~/junit/ \; + + - run: + name: "Upload tests coverage" + command: bash <(curl -s https://codecov.io/bash) + + - store_artifacts: + path: ~/junit/ + destination: artifacts + + - store_test_results: + path: ~/junit/ + destination: testartifacts + + - run: + name: "Populate Secrets from Environment" + command: | + if [[ ${CIRCLE_BUILD_NUM} = "" ]]; then + echo "populating from .envrc" + [[ -f "ci/.envrc" ]] || (printf "Download .envrc from LastPass using \"lpass show --notes 7083089362665788436\" in directory ${CI_DIR} before running script"; exit 1) + cd ~/repo/ci/ + cat .envrc >> $BASH_ENV + ./scripts/populate-secrets-from-environment.sh + ./scripts/fix-bbl-state-json-line-breaks.py + cd bbl-bosh-lite-environment/bbl-state + bbl print-env >> $BASH_ENV + else + echo "populating from bbl env" + cd ~/repo/ci/scripts + ./populate-secrets-from-environment.sh + cd ~/repo/ci/bbl-bosh-lite-environment/bbl-state + bbl print-env >> $BASH_ENV + fi + + - run: + name: "Start BOSH Lite" + command: | + cd ~/repo/ci + ./scripts/wait-for-bosh-locks-to-be-released.sh 30 + bosh -n -d cf start + + - run: + name: "Run acceptance tests" + command: | + ./gradlew -b spring-cloud-app-broker-sample/build.gradle assemble + ./gradlew clean assemble check -PacceptanceTests --stacktrace --continue -b spring-cloud-app-broker-acceptance-tests/build.gradle + + - store_test_results: + path: ~/repo/spring-cloud-app-broker-acceptance-tests/build/test-results + destination: acceptancetestartifacts + + - run: + name: "Stop BOSH Lite" + command: | + cd ~/repo/ci/bbl-bosh-lite-environment/bbl-state + bosh -n -d cf stop + when: always diff --git a/config/pmdRuleSet.xml b/ci/config/pmdRuleSet.xml similarity index 100% rename from config/pmdRuleSet.xml rename to ci/config/pmdRuleSet.xml diff --git a/config/pmdTestRuleSet.xml b/ci/config/pmdTestRuleSet.xml similarity index 100% rename from config/pmdTestRuleSet.xml rename to ci/config/pmdTestRuleSet.xml diff --git a/ci/scripts/delete-all-envvars-in-circle.sh b/ci/scripts/delete-all-envvars-in-circle.sh new file mode 100755 index 0000000..a056bc0 --- /dev/null +++ b/ci/scripts/delete-all-envvars-in-circle.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +#TODO: List all envvars in Circle then delete to avoid needing to explicitly name all vars +set -euo pipefail +readonly CIRCLE_API_BASE=https://circleci.com/api/v1.1/project/github/spring-cloud-incubator/spring-cloud-app-broker + +delete_envvars(){ + + delete_envvar_from_circle DIRECTOR_SSLCA + delete_envvar_from_circle JUMPBOX_PRIVATE_KEY + delete_envvar_from_circle UAA_CERT + delete_envvar_from_circle CREDHUB_CA + delete_envvar_from_circle LB_KEY + delete_envvar_from_circle LB_CERT + delete_envvar_from_circle DIRECTOR_SSLPRIVATE_KEY + delete_envvar_from_circle DIRECTOR_SSLCERTIFICATE + delete_envvar_from_circle DIRECTOR_JUMPBOX_SSH_PRIVATE_KEY + delete_envvar_from_circle DIRECTOR_USERNAME + delete_envvar_from_circle DIRECTOR_PASSWORD + delete_envvar_from_circle DIRECTOR_INTERNAL_IP + delete_envvar_from_circle DIRECTOR_PORT + delete_envvar_from_circle JUMPBOX_URL + delete_envvar_from_circle LB_DOMAIN + delete_envvar_from_circle DIRECTOR_ADMIN_PASSWORD + delete_envvar_from_circle CREDHUB_ADMIN_CLIENT_SECRET + delete_envvar_from_circle BBL_ENV_ID + delete_envvar_from_circle CIRCLECI_API_KEY + delete_envvar_from_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_API_HOST + delete_envvar_from_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_API_PORT + delete_envvar_from_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_USERNAME + delete_envvar_from_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_PASSWORD + delete_envvar_from_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_DEFAULT_ORG + delete_envvar_from_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_DEFAULT_SPACE + delete_envvar_from_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_SKIP_SSL_VALIDATION + delete_envvar_from_circle TESTS_SAMPLEBROKERAPPPATH + +} + +delete_envvar_from_circle() { + local envvar_name=$1 + curl -XDELETE "${CIRCLE_API_BASE}/envvar/${envvar_name}?circle-token=${CIRCLECI_API_KEY}" + +} + +delete_envvars \ No newline at end of file diff --git a/ci/scripts/export-envvars-to-circle-ci.sh b/ci/scripts/export-envvars-to-circle-ci.sh new file mode 100755 index 0000000..5e23fe7 --- /dev/null +++ b/ci/scripts/export-envvars-to-circle-ci.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail +readonly CIRCLE_API_BASE=https://circleci.com/api/v1.1/project/github/spring-cloud-incubator/spring-cloud-app-broker +readonly PROG_DIR=$(readlink -m $(dirname $0)) +readonly CI_DIR=$(dirname "${PROG_DIR}") + +export_variables_to_circle() { + [[ ! -f "${CI_DIR}/.envrc" ]] && printf "Download .envrc from LastPass using \"lpass show --notes 7083089362665788436\" in directory ${SCRIPTS_DIR} before running script" && exit 1 + + : ${DIRECTOR_SSLCA:?"Required Env Variable not found!"} + : ${JUMPBOX_PRIVATE_KEY:?"Required Env Variable not found!"} + : ${UAA_CERT:?"Required Env Variable not found!"} + : ${CREDHUB_CA:?"Required Env Variable not found!"} + : ${LB_KEY:?"Required Env Variable not found!"} + : ${LB_CERT:?"Required Env Variable not found!"} + : ${DIRECTOR_SSLPRIVATE_KEY:?"Required Env Variable not found!"} + : ${DIRECTOR_SSLCERTIFICATE:?"Required Env Variable not found!"} + : ${DIRECTOR_SSLCERTIFICATE:?"Required Env Variable not found!"} + : ${DIRECTOR_JUMPBOX_SSH_PRIVATE_KEY:?"Required Env Variable not found!"} + : ${DIRECTOR_USERNAME:?"Required Env Variable not found!"} + : ${DIRECTOR_PASSWORD:?"Required Env Variable not found!"} + : ${DIRECTOR_INTERNAL_IP:?"Required Env Variable not found!"} + : ${DIRECTOR_PORT:?"Required Env Variable not found!"} + : ${JUMPBOX_URL:?"Required Env Variable not found!"} + : ${LB_DOMAIN:?"Required Env Variable not found!"} + : ${DIRECTOR_ADMIN_PASSWORD:?"Required Env Variable not found!"} + : ${CREDHUB_ADMIN_CLIENT_SECRET:?"Required Env Variable not found!"} + : ${BBL_ENV_ID:?"Required Env Variable not found!"} + : ${CIRCLECI_API_KEY:?"Required Env Variable not found!"} + : ${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_API_HOST:?"Required Env Variable not found!"} + : ${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_API_PORT:?"Required Env Variable not found!"} + : ${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_USERNAME:?"Required Env Variable not found!"} + : ${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_PASSWORD:?"Required Env Variable not found!"} + : ${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_DEFAULT_ORG:?"Required Env Variable not found!"} + : ${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_DEFAULT_SPACE:?"Required Env Variable not found!"} + : ${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_SKIP_SSL_VALIDATION:?"Required Env Variable not found!"} + : ${TESTS_SAMPLEBROKERAPPPATH:?"Required Env Variable not found!"} + + create_envvar_in_circle DIRECTOR_SSLCA "${DIRECTOR_SSLCA}" + create_envvar_in_circle JUMPBOX_PRIVATE_KEY "${JUMPBOX_PRIVATE_KEY}" + create_envvar_in_circle UAA_CERT "${UAA_CERT}" + create_envvar_in_circle CREDHUB_CA "${CREDHUB_CA}" + create_envvar_in_circle LB_KEY "${LB_KEY}" + create_envvar_in_circle LB_CERT "${LB_CERT}" + create_envvar_in_circle DIRECTOR_SSLPRIVATE_KEY "${DIRECTOR_SSLPRIVATE_KEY}" + create_envvar_in_circle DIRECTOR_SSLCERTIFICATE "${DIRECTOR_SSLCERTIFICATE}" + create_envvar_in_circle DIRECTOR_JUMPBOX_SSH_PRIVATE_KEY "${DIRECTOR_JUMPBOX_SSH_PRIVATE_KEY}" + create_envvar_in_circle DIRECTOR_USERNAME "${DIRECTOR_USERNAME}" + create_envvar_in_circle DIRECTOR_PASSWORD "${DIRECTOR_PASSWORD}" + create_envvar_in_circle DIRECTOR_INTERNAL_IP "${DIRECTOR_INTERNAL_IP}" + create_envvar_in_circle DIRECTOR_PORT "${DIRECTOR_PORT}" + create_envvar_in_circle JUMPBOX_URL "${JUMPBOX_URL}" + create_envvar_in_circle LB_DOMAIN "${LB_DOMAIN}" + create_envvar_in_circle DIRECTOR_ADMIN_PASSWORD "${DIRECTOR_ADMIN_PASSWORD}" + create_envvar_in_circle CREDHUB_ADMIN_CLIENT_SECRET "${CREDHUB_ADMIN_CLIENT_SECRET}" + create_envvar_in_circle BBL_ENV_ID "${BBL_ENV_ID}" + create_envvar_in_circle CIRCLECI_API_KEY "${CIRCLECI_API_KEY}" + create_envvar_in_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_API_HOST "${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_API_HOST}" + create_envvar_in_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_API_PORT "${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_API_PORT}" + create_envvar_in_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_USERNAME "${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_USERNAME}" + create_envvar_in_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_PASSWORD "${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_PASSWORD}" + create_envvar_in_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_DEFAULT_ORG "${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_DEFAULT_ORG}" + create_envvar_in_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_DEFAULT_SPACE "${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_DEFAULT_SPACE}" + create_envvar_in_circle SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_SKIP_SSL_VALIDATION "${SPRING_CLOUD_APPBROKER_ACCEPTANCETEST_CLOUDFOUNDRY_SKIP_SSL_VALIDATION}" + create_envvar_in_circle TESTS_SAMPLEBROKERAPPPATH "${TESTS_SAMPLEBROKERAPPPATH}" + +} + +create_envvar_in_circle() { + local envvar_name=$1 + local envvar_value=$2 + + curl -XPOST "${CIRCLE_API_BASE}/envvar?circle-token=${CIRCLECI_API_KEY}" \ + -H "Content-Type: application/json" -d '{"name": "'"${envvar_name}"'", "value": "'"${envvar_value}"'"}' +} + +export_variables_to_circle \ No newline at end of file diff --git a/ci/scripts/fix-bbl-state-json-line-breaks.py b/ci/scripts/fix-bbl-state-json-line-breaks.py new file mode 100755 index 0000000..275a7ab --- /dev/null +++ b/ci/scripts/fix-bbl-state-json-line-breaks.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# I couldn't get this working with sed so python it is! +import fileinput +import os +import sys + +script_path = os.path.dirname(os.path.realpath(sys.argv[0])) +ci_dir = os.path.dirname(script_path) + +with fileinput.FileInput(os.path.join(ci_dir, 'bbl-bosh-lite-environment/bbl-state/bbl-state.json'), inplace=True) as file: + for line in file: + print(line.replace('\\\\n', '\\n'), end='') \ No newline at end of file diff --git a/ci/scripts/populate-secrets-from-environment.sh b/ci/scripts/populate-secrets-from-environment.sh new file mode 100755 index 0000000..4ef3113 --- /dev/null +++ b/ci/scripts/populate-secrets-from-environment.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -euo pipefail + +readonly PROG_DIR=$(readlink -m $(dirname $0)) +readonly SCRIPTS_DIR=$(dirname "${PROG_DIR}") +readonly CI_DIR=$(dirname "${SCRIPTS_DIR}") +cd "${SCRIPTS_DIR}/bbl-bosh-lite-environment/bbl-state/" + +substitute_bbl_state_file(){ + + perl -pe "s|<%= bblEnvId %>|${BBL_ENV_ID}|;" \ + -pe "s|<%= jumpboxUrl %>|${JUMPBOX_URL}|;" \ + -pe "s|<%= directorUsername %>|${DIRECTOR_USERNAME}|;" \ + -pe "s|<%= directorPassword %>|${DIRECTOR_PASSWORD}|;" \ + -pe "s|<%= directorInternalIp %>|${DIRECTOR_INTERNAL_IP}|;" \ + -pe "s|<%= directorPort %>|${DIRECTOR_PORT}|;" \ + -pe "s|<%= directorSSLCA %>|$(echo ${DIRECTOR_SSLCA} | jq -aR .)|;" \ + -pe "s|<%= directorSSLCertificate %>|$(echo ${DIRECTOR_SSLCERTIFICATE} | jq -aR .)|;" \ + -pe "s|<%= directorSSLPrivateKey %>|$(echo ${DIRECTOR_SSLPRIVATE_KEY} | jq -aR .)|;" \ + -pe "s|<%= lbCert %>|$(echo ${LB_CERT} | jq -aR .)|;" \ + -pe "s|<%= lbKey %>|$(echo ${LB_KEY} | jq -aR .)|;" \ + -pe "s|<%= lbDomain %>|${LB_DOMAIN}|;" \ + bbl-state.json.tmpl > bbl-state.json +} + +substitute_director_vars_store(){ + perl -pe "s|<%= directorAdminPassword %>|${DIRECTOR_ADMIN_PASSWORD}|;" \ + -pe "s|<%= credhubAdminClientSecret %>|${CREDHUB_ADMIN_CLIENT_SECRET}|;" \ + -pe "s|<%= credhubCA %>|${CREDHUB_CA}|;" \ + -pe "s|<%= uaaCert %>|${UAA_CERT}|;" \ + -pe "s|<%= directorJumpboxSshPrivateKey %>|${DIRECTOR_JUMPBOX_SSH_PRIVATE_KEY}|;" \ + director-vars-store.yml.tmpl > vars/director-vars-store.yml +} + +substitute_director_vars_file(){ + perl -pe "s|<%= directorInternalIp %>|${DIRECTOR_INTERNAL_IP}|;" \ + director-vars-file.yml.tmpl > vars/director-vars-file.yml +} + +substitute_jumpbox_vars_store(){ + perl -pe "s|<%= jumpboxPrivateKey %>|${JUMPBOX_PRIVATE_KEY}|;" \ + jumpbox-vars-store.yml.tmpl > vars/jumpbox-vars-store.yml +} + +substitute_bbl_state_file +substitute_director_vars_store +substitute_director_vars_file +substitute_jumpbox_vars_store \ No newline at end of file diff --git a/ci/scripts/run-local-ci-job.sh b/ci/scripts/run-local-ci-job.sh new file mode 100755 index 0000000..c906e5b --- /dev/null +++ b/ci/scripts/run-local-ci-job.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly PROG_DIR=$(readlink -m $(dirname $0)) +readonly CI_DIR=$(dirname "${PROG_DIR}") +readonly PROJECT_ROOT=$(dirname "${CI_DIR}") + +validate(){ + [[ $(type circleci) ]] || (echo "circleci tool not installed" >&2 ; circle_usage ; exit 2) + [[ -f "${CI_DIR}/.envrc" ]] || (printf "Download .envrc from LastPass using \"lpass show --notes 7083089362665788436\" in directory ${CI_DIR} before running script"; exit 1) +} + +circle_usage() { + cat <<- EOF +Install circle command line tool to run local builds: + "curl -o /usr/local/bin/circleci https://circle-downloads.s3.amazonaws.com/releases/build_agent_wrapper/circleci && \ + chmod +x /usr/local/bin/circleci" +EOF +} + +run_circle_command(){ + cd "${PROJECT_ROOT}" + circleci build +} + +validate +run_circle_command \ No newline at end of file diff --git a/ci/scripts/wait-for-bosh-locks-to-be-released.sh b/ci/scripts/wait-for-bosh-locks-to-be-released.sh new file mode 100755 index 0000000..5ac8f4d --- /dev/null +++ b/ci/scripts/wait-for-bosh-locks-to-be-released.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# The BOSH director might be in use by someone else. Check `bosh locks` command before running ATs +set -euo pipefail + +[[ -z "${1:-}" ]] && printf "Must provide Max Queue Time in *minutes* as script argument\n" && exit 1 + +readonly MAX_TIME=${1} + +wait_for_bosh_locks_to_be_released(){ + local max_time_seconds=$((MAX_TIME * 60)) + + until [[ $(bosh locks --json | jq '.Tables[].Rows | length') -eq 0 ]]; do + + sleep 10 + printf "." + if [[ "${SECONDS}" -gt "${max_time_seconds}" ]]; then + # After cancelling the build, wait some time to ensure the job has been cancelled otherwise + # the script will exit and the job will be incorrectly marked success or failed + printf "Max time exceeded waiting for bosh locks to be released, cancelling build\n" + cancel_current_build + sleep 20 + exit 1 + fi + done + printf "No BOSH locks found, continuing build\n" +} + +cancel_current_build() { + curl -s -X POST \ + "https://circleci.com/api/v1.1/project/github/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BUILD_NUM}/cancel?circle-token=${CIRCLECI_API_KEY}" > /dev/null +} + +wait_for_bosh_locks_to_be_released \ No newline at end of file diff --git a/ci/scripts/wait-for-other-builds-to-finish.sh b/ci/scripts/wait-for-other-builds-to-finish.sh new file mode 100755 index 0000000..7dbe890 --- /dev/null +++ b/ci/scripts/wait-for-other-builds-to-finish.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +[[ -z "${1:-}" ]] && printf "Must provide Max Queue Time in *minutes* as script argument\n" && exit 1 +[[ -z "${CIRCLE_BUILD_NUM:-}" ]] && printf "skipping, not running on remote circleci environment\n" && exit 0 +: ${CIRCLECI_API_KEY:?"Required Env Variable not found!"} +: ${CIRCLE_PROJECT_USERNAME:?"Required Env Variable not found!"} +: ${CIRCLE_PROJECT_REPONAME:?"Required Env Variable not found!"} +: ${CIRCLE_REPOSITORY_URL:?"Required Env Variable not found!"} + +readonly MAX_TIME=${1} + +wait_for_previous_builds_to_complete(){ + local max_time_seconds=$((MAX_TIME * 60)) + + until [[ $(curl -s "https://circleci.com/api/v1.1/project/github/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}?circle-token=${CIRCLECI_API_KEY}&filter=running" \ + | jq 'min_by(.build_num) | .build_num') \ + -eq ${CIRCLE_BUILD_NUM} ]]; do + + sleep 10 + printf "." + if [[ "${SECONDS}" -gt "${max_time_seconds}" ]]; then + # After cancelling the build, wait some time to ensure the job has been cancelled otherwise + # the script will exit and the job will be incorrectly marked success or failed + printf "Max time exceeded waiting to reach front of queue, cancelling build\n" + cancel_current_build + sleep 20 + exit 1 + fi + done + printf "Build is front of queue\n" + +} + +cancel_current_build() { + curl -s -X POST \ + "https://circleci.com/api/v1.1/project/github/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BUILD_NUM}/cancel?circle-token=${CIRCLECI_API_KEY}" > /dev/null +} + +wait_for_previous_builds_to_complete \ No newline at end of file diff --git a/spring-cloud-app-broker-acceptance-tests/README.adoc b/spring-cloud-app-broker-acceptance-tests/README.adoc index 3a2067d..d31bfb9 100644 --- a/spring-cloud-app-broker-acceptance-tests/README.adoc +++ b/spring-cloud-app-broker-acceptance-tests/README.adoc @@ -12,14 +12,14 @@ The tests require the following properties to be set: -* `tests.sampleAppPath` - The path to the sample broker jar (eg. /spring-cloud-app-broker-sample/build/libs/spring-cloud-app-broker-sample.jar). -* `spring.cloud.appbroker.deployer.cloudfoundry.api-host` - The CF API host where the tests are going to run. -* `spring.cloud.appbroker.deployer.cloudfoundry.api-port` - The CF API port where the tests are going to run. -* `spring.cloud.appbroker.deployer.cloudfoundry.username` - The CF API username where the tests are going to run. -* `spring.cloud.appbroker.deployer.cloudfoundry.password` - The CF API password where the tests are going to run. -* `spring.cloud.appbroker.deployer.cloudfoundry.default-org` - The CF organization where the tests are going to run. -* `spring.cloud.appbroker.deployer.cloudfoundry.default-space` - The CF space where the tests are going to run. -* `spring.cloud.appbroker.deployer.cloudfoundry.skip-ssl-validation` - If SSL validation should be skipped. +* `tests.sampleBrokerAppPath` - The path to the sample broker jar (eg. /spring-cloud-app-broker-sample/build/libs/spring-cloud-app-broker-sample.jar). +* `spring.cloud.appbroker.acceptance-test.cloudfoundry.api-host` - The CF API host where the tests are going to run. +* `spring.cloud.appbroker.acceptance-test.cloudfoundry.api-port` - The CF API port where the tests are going to run. +* `spring.cloud.appbroker.acceptance-test.cloudfoundry.username` - The CF API username where the tests are going to run. +* `spring.cloud.appbroker.acceptance-test.cloudfoundry.password` - The CF API password where the tests are going to run. +* `spring.cloud.appbroker.acceptance-test.cloudfoundry.default-org` - The CF organization where the tests are going to run. +* `spring.cloud.appbroker.acceptance-test.cloudfoundry.default-space` - The CF space where the tests are going to run. +* `spring.cloud.appbroker.acceptance-test.cloudfoundry.skip-ssl-validation` - If SSL validation should be skipped. These properties can be set with `-D` system properties on the gradle command line. @@ -28,11 +28,11 @@ Also, the flag acceptanceTests must be provided as `-PacceptanceTests`. [source,bash] ---- $ ./gradlew test -PacceptanceTests \ - -Dtests.sampleAppPath=/path/to/broker.jar \ - -Dspring.cloud.appbroker.deployer.cloudfoundry.api-host=api.cf.my.cf.io \ - -Dspring.cloud.appbroker.deployer.cloudfoundry.api-port=443 \ - -Dspring.cloud.appbroker.deployer.cloudfoundry.username=admin \ - -Dspring.cloud.appbroker.deployer.cloudfoundry.password=password \ - -Dspring.cloud.appbroker.deployer.cloudfoundry.default-org=test \ - -Dspring.cloud.appbroker.deployer.cloudfoundry.default-space=development \ - -Dspring.cloud.appbroker.deployer.cloudfoundry.skip-ssl-validation=true + -Dtests.sampleBrokerAppPath=/path/to/broker.jar \ + -Dspring.cloud.appbroker.acceptance-test.cloudfoundry.api-host=api.cf.my.cf.io \ + -Dspring.cloud.appbroker.acceptance-test.cloudfoundry.api-port=443 \ + -Dspring.cloud.appbroker.acceptance-test.cloudfoundry.username=admin \ + -Dspring.cloud.appbroker.acceptance-test.cloudfoundry.password=password \ + -Dspring.cloud.appbroker.acceptance-test.cloudfoundry.default-org=test \ + -Dspring.cloud.appbroker.acceptance-test.cloudfoundry.default-space=development \ + -Dspring.cloud.appbroker.acceptance-test.cloudfoundry.skip-ssl-validation=true diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/AcceptanceTestProperties.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/AcceptanceTestProperties.java new file mode 100644 index 0000000..fad4608 --- /dev/null +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/AcceptanceTestProperties.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package org.springframework.cloud.appbroker.acceptance; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(value = "tests") +public class AcceptanceTestProperties { + + private String sampleBrokerAppPath; + + public String getSampleBrokerAppPath() { + return sampleBrokerAppPath; + } + + public void setSampleBrokerAppPath(String sampleBrokerAppPath) { + this.sampleBrokerAppPath = sampleBrokerAppPath; + } + +} diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java index c814ee9..943e61c 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java @@ -18,15 +18,19 @@ package org.springframework.cloud.appbroker.acceptance; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; import java.util.Optional; +import java.util.concurrent.CountDownLatch; + import org.cloudfoundry.operations.applications.ApplicationEnvironments; import org.cloudfoundry.operations.applications.ApplicationSummary; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration; import org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryService; @@ -35,6 +39,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; @SpringBootTest(classes = {CloudFoundryClientConfiguration.class, CloudFoundryService.class}) @ExtendWith(SpringExtension.class) @ExtendWith(BrokerPropertiesParameterResolver.class) +@EnableConfigurationProperties(AcceptanceTestProperties.class) class CloudFoundryAcceptanceTest { @BeforeEach @@ -43,7 +48,7 @@ class CloudFoundryAcceptanceTest { } private static final String SAMPLE_BROKER_APP_NAME = "sample-broker"; - private static final String BROKER_NAME = "sample-broker-name"; + private static final String SERVICE_BROKER_NAME = "sample-broker-name"; private static final String SERVICE_NAME = "example"; private static final String PLAN_NAME = "standard"; private static final String SERVICE_INSTANCE_NAME = "my-service"; @@ -51,53 +56,68 @@ class CloudFoundryAcceptanceTest { @Autowired private CloudFoundryService cloudFoundryService; - @Value("${tests.sampleAppPath}") - private String sampleAppPath; + @Autowired + private AcceptanceTestProperties acceptanceTestProperties; @AfterEach void tearDown() { - cleanup(); + blockingSubscribe(cleanup()); } - void initializeBroker(String[] properties) { - cleanup(); + void initializeBroker(String[] backingAppProperties) { - cloudFoundryService.pushAppNoStart(SAMPLE_BROKER_APP_NAME, getSampleAppPath()); - cloudFoundryService.setBrokerAppEnvironment(properties); - cloudFoundryService.startApplication(SAMPLE_BROKER_APP_NAME); - - String backingAppURL = cloudFoundryService.getApplicationRoute(SAMPLE_BROKER_APP_NAME); - cloudFoundryService.createServiceBroker(BROKER_NAME, backingAppURL); - cloudFoundryService.enableServiceBrokerAccess(SERVICE_NAME); + blockingSubscribe(cloudFoundryService + .getOrCreateDefaultOrganization() + .then(cloudFoundryService.getOrCreateDefaultSpace()) + .then(cloudFoundryService.pushAppBroker(SAMPLE_BROKER_APP_NAME, getSampleBrokerAppPath(), backingAppProperties)) + .then(cloudFoundryService.createServiceBroker(SERVICE_BROKER_NAME, SAMPLE_BROKER_APP_NAME)) + .then(cloudFoundryService.enableServiceBrokerAccess(SERVICE_NAME))); } - private void cleanup() { - cloudFoundryService.deleteServiceInstance(SERVICE_INSTANCE_NAME); - cloudFoundryService.deleteServiceBroker(BROKER_NAME); - cloudFoundryService.deleteBackingApp(SAMPLE_BROKER_APP_NAME); + private Mono cleanup() { + return cloudFoundryService + .deleteServiceInstance(SERVICE_INSTANCE_NAME) + .then(cloudFoundryService.deleteServiceBroker(SERVICE_BROKER_NAME)) + .then(cloudFoundryService.deleteBackingApp(SAMPLE_BROKER_APP_NAME)); } void createServiceInstance() { - cloudFoundryService.createServiceInstance(PLAN_NAME, SERVICE_NAME, SERVICE_INSTANCE_NAME); + blockingSubscribe(cloudFoundryService.createServiceInstance(PLAN_NAME, SERVICE_NAME, SERVICE_INSTANCE_NAME)); } void deleteServiceInstance() { - cloudFoundryService.deleteServiceInstance(SERVICE_INSTANCE_NAME); + blockingSubscribe(cloudFoundryService.deleteServiceInstance(SERVICE_INSTANCE_NAME)); } Optional getApplicationSummaryByName(String appName) { - List applicationsAfterDeletion = cloudFoundryService.getApplications(); - - return applicationsAfterDeletion.stream() + return cloudFoundryService + .getApplications() + .flatMapMany(Flux::fromIterable) .filter(applicationSummary -> appName.equals(applicationSummary.getName())) - .findFirst(); + .next() + .blockOptional(); } ApplicationEnvironments getApplicationEnvironmentByName(String appName) { - return cloudFoundryService.getApplicationEnvironmentByAppName(appName); + return cloudFoundryService.getApplicationEnvironmentByAppName(appName).block(); } - private Path getSampleAppPath() { - return Paths.get(sampleAppPath, ""); + private Path getSampleBrokerAppPath() { + return Paths.get(acceptanceTestProperties.getSampleBrokerAppPath(), ""); } + + private void blockingSubscribe(Mono publisher){ + CountDownLatch latch = new CountDownLatch(1); + publisher.subscribe(System.out::println, t -> { + t.printStackTrace(); + latch.countDown(); + }, latch::countDown); + try { + latch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } \ No newline at end of file diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryProperties.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryProperties.java index 807ce11..3f4c838 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryProperties.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryProperties.java @@ -17,16 +17,17 @@ package org.springframework.cloud.appbroker.acceptance.fixtures.cf; import java.net.URI; -import org.cloudfoundry.reactor.ProxyConfiguration; -import org.springframework.boot.context.properties.ConfigurationProperties; +import org.cloudfoundry.reactor.ProxyConfiguration; + +import org.springframework.boot.context.properties.ConfigurationProperties; import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryProperties.PROPERTY_PREFIX; @ConfigurationProperties(PROPERTY_PREFIX) public class CloudFoundryProperties { - static final String PROPERTY_PREFIX = "spring.cloud.appbroker.deployer.cloudfoundry"; + static final String PROPERTY_PREFIX = "spring.cloud.appbroker.acceptancetest.cloudfoundry"; private String apiHost; private Integer apiPort; diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java index c98f9ef..910ea8b 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java @@ -18,194 +18,263 @@ package org.springframework.cloud.appbroker.acceptance.fixtures.cf; import java.nio.file.Path; import java.util.Arrays; +import java.util.HashMap; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; + +import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationEnvironments; +import org.cloudfoundry.operations.applications.ApplicationManifest; import org.cloudfoundry.operations.applications.ApplicationSummary; import org.cloudfoundry.operations.applications.DeleteApplicationRequest; import org.cloudfoundry.operations.applications.GetApplicationEnvironmentsRequest; import org.cloudfoundry.operations.applications.GetApplicationRequest; -import org.cloudfoundry.operations.applications.PushApplicationRequest; -import org.cloudfoundry.operations.applications.SetEnvironmentVariableApplicationRequest; -import org.cloudfoundry.operations.applications.StartApplicationRequest; +import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; +import org.cloudfoundry.operations.organizations.CreateOrganizationRequest; +import org.cloudfoundry.operations.organizations.DefaultOrganizations; +import org.cloudfoundry.operations.organizations.OrganizationSummary; import org.cloudfoundry.operations.serviceadmin.CreateServiceBrokerRequest; import org.cloudfoundry.operations.serviceadmin.DeleteServiceBrokerRequest; import org.cloudfoundry.operations.serviceadmin.EnableServiceAccessRequest; import org.cloudfoundry.operations.services.CreateServiceInstanceRequest; import org.cloudfoundry.operations.services.DeleteServiceInstanceRequest; +import org.cloudfoundry.operations.spaces.CreateSpaceRequest; +import org.cloudfoundry.operations.spaces.DefaultSpaces; +import org.cloudfoundry.operations.spaces.SpaceSummary; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.appbroker.acceptance.AcceptanceTestProperties; +import org.springframework.stereotype.Service; + +import static java.lang.String.format; + @Service public class CloudFoundryService { private static final Logger LOGGER = LoggerFactory.getLogger(CloudFoundryService.class); - @Autowired private CloudFoundryOperations cloudFoundryOperations; + private CloudFoundryProperties cloudFoundryProperties; + private CloudFoundryClient cloudFoundryClient; + private AcceptanceTestProperties acceptanceTestProperties; @Autowired - private CloudFoundryProperties cloudFoundryProperties; - - public void enableServiceBrokerAccess(String serviceName) { - cloudFoundryOperations - .serviceAdmin() - .enableServiceAccess(EnableServiceAccessRequest.builder().serviceName(serviceName).build()) - .block(); + public CloudFoundryService(CloudFoundryOperations cloudFoundryOperations, CloudFoundryProperties cloudFoundryProperties, + CloudFoundryClient cloudFoundryClient, AcceptanceTestProperties acceptanceTestProperties) { + this.cloudFoundryOperations = cloudFoundryOperations; + this.cloudFoundryProperties = cloudFoundryProperties; + this.cloudFoundryClient = cloudFoundryClient; + this.acceptanceTestProperties = acceptanceTestProperties; } - public void createServiceBroker(String brokerName, String backingAppURL) { - cloudFoundryOperations - .serviceAdmin() - .create(CreateServiceBrokerRequest.builder() - .name(brokerName) - .username("user") - .password("password") - .url(backingAppURL) - .build()) - .block(); - } - - public String getApplicationRoute(String appName) { - return "https://" + cloudFoundryOperations - .applications() - .get(GetApplicationRequest.builder().name(appName).build()) - .block().getUrls().get(0); - } - - public void startApplication(String appName) { - cloudFoundryOperations - .applications() - .start(StartApplicationRequest.builder().name(appName).build()) - .block(); - } - - public void pushAppNoStart(String appName, Path appPath) { - cloudFoundryOperations - .applications() - .push(PushApplicationRequest - .builder() - .noStart(true) - .path(appPath) - .name(appName) - .build()) - .block(); - } - - public void deleteBackingApp(String appName) { - try { - cloudFoundryOperations - .applications() - .delete(DeleteApplicationRequest.builder().name(appName).build()) - .block(); - } catch (Exception e) { - // Ignore - } - } - - public void deleteServiceBroker(String brokerName) { - try { + public Mono enableServiceBrokerAccess(String serviceName) { + return loggingMono( cloudFoundryOperations .serviceAdmin() - .delete(DeleteServiceBrokerRequest.builder().name(brokerName).build()) - .block(); - } catch (Exception e) { - // Ignore - } + .enableServiceAccess(EnableServiceAccessRequest.builder().serviceName(serviceName).build())); } - public void deleteServiceInstance(String serviceInstanceName) { - try { + public Mono createServiceBroker(String brokerName, String sampleBrokerAppName) { + return loggingMono( + getApplicationRoute(sampleBrokerAppName) + .flatMap(url -> cloudFoundryOperations + .serviceAdmin() + .create(CreateServiceBrokerRequest + .builder() + .name(brokerName) + .username("user") + .password("password") + .url(url) + .build()))); + } + + public Mono getApplicationRoute(String appName) { + return loggingMono( + cloudFoundryOperations + .applications() + .get(GetApplicationRequest.builder().name(appName).build()) + .map(ApplicationDetail::getUrls) + .flatMapMany(Flux::fromIterable) + .next() + .map(url -> "https://" + url)); + } + + public Mono pushAppBroker(String appName, Path appPath, String[] backingAppProperties) { + return loggingMono( + cloudFoundryOperations + .applications() + .pushManifest(PushApplicationManifestRequest + .builder() + .manifest(ApplicationManifest + .builder() + .putAllEnvironmentVariables(appBrokerDeployerEnvironmentVariables()) + .putAllEnvironmentVariables(appBrokerCatalogEnvironmentVariables()) + .putAllEnvironmentVariables(backingAppEnvironmentVariables(backingAppProperties)) + .name(appName) + .path(appPath) + .memory(1024) + .build()) + .build())); + } + + public Mono deleteBackingApp(String appName) { + return loggingMono( + cloudFoundryOperations + .applications() + .delete(DeleteApplicationRequest.builder().name(appName).build())); + } + + public Mono deleteServiceBroker(String brokerName) { + return loggingMono( + cloudFoundryOperations + .serviceAdmin() + .delete(DeleteServiceBrokerRequest.builder().name(brokerName).build())); + } + + public Mono deleteServiceInstance(String serviceInstanceName) { + return loggingMono( cloudFoundryOperations .services() - .deleteInstance(DeleteServiceInstanceRequest.builder().name(serviceInstanceName).build()) - .block(); - } catch (Exception e) { - // Ignore + .listInstances() + .filter(si -> si.getName().equals(serviceInstanceName)) + .next() + .flatMap(si -> + cloudFoundryOperations + .services() + .deleteInstance(DeleteServiceInstanceRequest.builder().name(si.getName()).build()))); + } + + public Mono createServiceInstance(String planName, String serviceName, String serviceInstanceName) { + return loggingMono( + cloudFoundryOperations + .services() + .createInstance(CreateServiceInstanceRequest + .builder() + .planName(planName) + .serviceName(serviceName) + .serviceInstanceName(serviceInstanceName) + .build())); + } + + public Mono> getApplications() { + return loggingMono( + cloudFoundryOperations.applications().list().collectList()); + } + + public Mono getApplicationEnvironmentByAppName(String appName) { + return loggingMono( + cloudFoundryOperations + .applications() + .getEnvironments(GetApplicationEnvironmentsRequest.builder().name(appName).build())); + } + + + public Mono getOrCreateDefaultSpace() { + final String defaultOrg = cloudFoundryProperties.getDefaultOrg(); + + DefaultSpaces spaceOperations = new DefaultSpaces( + Mono.just(cloudFoundryClient), + getOrCreateDefaultOrganization().map(OrganizationSummary::getId), + Mono.just(cloudFoundryProperties.getUsername())); + + final String defaultSpace = cloudFoundryProperties.getDefaultSpace(); + return loggingMono( + getDefaultSpace(spaceOperations) + .switchIfEmpty(spaceOperations + .create(CreateSpaceRequest + .builder() + .name(defaultSpace) + .organization(defaultOrg) + .build()) + .then(getDefaultSpace(spaceOperations)))); + } + + public Mono getOrCreateDefaultOrganization() { + DefaultOrganizations organizationOperations = new DefaultOrganizations( + Mono.just(cloudFoundryClient), + Mono.just(cloudFoundryProperties.getUsername())); + + final String defaultOrg = cloudFoundryProperties.getDefaultOrg(); + return loggingMono( + getDefaultOrg(organizationOperations) + .switchIfEmpty(organizationOperations + .create(CreateOrganizationRequest + .builder() + .organizationName(defaultOrg) + .build()) + .then(getDefaultOrg(organizationOperations)))); + } + + private Mono getDefaultOrg(DefaultOrganizations orgOperations) { + return orgOperations + .list() + .filter(r -> r + .getName() + .equals(cloudFoundryProperties.getDefaultOrg())) + .next(); + } + + private Mono getDefaultSpace(DefaultSpaces spaceOperations) { + return spaceOperations + .list() + .filter(r -> r + .getName() + .equals(cloudFoundryProperties.getDefaultSpace())) + .next(); + } + + private Map appBrokerDeployerEnvironmentVariables() { + + Map deployerVariables = new HashMap<>(); + deployerVariables.put("spring.cloud.appbroker.deployer.cloudfoundry.api-host", cloudFoundryProperties.getApiHost()); + deployerVariables.put("spring.cloud.appbroker.deployer.cloudfoundry.api-port", String.valueOf(cloudFoundryProperties.getApiPort())); + deployerVariables.put("spring.cloud.appbroker.deployer.cloudfoundry.username", cloudFoundryProperties.getUsername()); + deployerVariables.put("spring.cloud.appbroker.deployer.cloudfoundry.password", cloudFoundryProperties.getPassword()); + deployerVariables.put("spring.cloud.appbroker.deployer.cloudfoundry.default-org", cloudFoundryProperties.getDefaultOrg()); + deployerVariables.put("spring.cloud.appbroker.deployer.cloudfoundry.default-space", cloudFoundryProperties.getDefaultSpace()); + deployerVariables.put("spring.cloud.appbroker.deployer.cloudfoundry.skip-ssl-validation", String.valueOf(cloudFoundryProperties.isSkipSslValidation())); + deployerVariables.put("spring.cloud.appbroker.apps[0].cloudfoundry.skip-ssl-validation", String.valueOf(cloudFoundryProperties.isSkipSslValidation())); + return deployerVariables; + } + + private Map appBrokerCatalogEnvironmentVariables() { + Map catalogVariables = new HashMap<>(); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].id", "example-service"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].name", "example"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].description", "A simple example"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].bindable", "true"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].tags[0]", "example"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].plans[0].id", "simple-plan"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].plans[0].bindable", "true"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].plans[0].name", "standard"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].plans[0].description", "A simple plan"); + catalogVariables.put("spring.cloud.openservicebroker.catalog.services[0].plans[0].free", "true"); + return catalogVariables; + } + + private Map backingAppEnvironmentVariables(String[] backingAppProperties) { + Map backingAppVariables = new HashMap<>(); + for (String appProperty : backingAppProperties) { + final String[] appPropertyKeyValue = appProperty.split("="); + if (appPropertyKeyValue.length != 2) { + throw new RuntimeException(format("Backing app property '%s' is incorrectly formatted", Arrays.toString(appPropertyKeyValue))); + } + else backingAppVariables.put(appPropertyKeyValue[0], appPropertyKeyValue[1]); } + return backingAppVariables; } - public void createServiceInstance(String planName, String serviceName, String serviceInstanceName) { - cloudFoundryOperations - .services() - .createInstance(CreateServiceInstanceRequest - .builder() - .planName(planName) - .serviceName(serviceName) - .serviceInstanceName(serviceInstanceName) - .build()) - .block(); - } - - private static SetEnvironmentVariableApplicationRequest createEnvRequest(String appName, String key, String value) { - return SetEnvironmentVariableApplicationRequest - .builder() - .name(appName) - .variableName(key) - .variableValue(value) - .build(); - } - - public List getApplications() { - return cloudFoundryOperations.applications().list().collectList().block(); - } - - public ApplicationEnvironments getApplicationEnvironmentByAppName(String appName) { - return cloudFoundryOperations - .applications() - .getEnvironments(GetApplicationEnvironmentsRequest.builder().name(appName).build()) - .block(); - } - - public void setBrokerAppEnvironment(String[] properties) { - Flux catalogPublishers = getCatalogPublishers(); - Flux appBrokerCFPublishers = getAppBrokerCFPublishers(); - Flux appBrokerApplicationPublishers = Flux.concat(Arrays.stream(properties) - .filter(property -> property.contains("=")) - .map(property -> property.split("=")) - .map(property -> setEnvRequest(property[0], property[1])) - .collect(Collectors.toList())); - - Flux.concat(catalogPublishers, appBrokerCFPublishers, appBrokerApplicationPublishers).blockLast(); - } - - private Flux getAppBrokerCFPublishers() { - return Flux.concat( - setEnvRequest("spring.cloud.appbroker.deployer.cloudfoundry.api-host", cloudFoundryProperties.getApiHost()), - setEnvRequest("spring.cloud.appbroker.deployer.cloudfoundry.api-port", String.valueOf(cloudFoundryProperties.getApiPort())), - setEnvRequest("spring.cloud.appbroker.deployer.cloudfoundry.username", cloudFoundryProperties.getUsername()), - setEnvRequest("spring.cloud.appbroker.deployer.cloudfoundry.password", cloudFoundryProperties.getPassword()), - setEnvRequest("spring.cloud.appbroker.deployer.cloudfoundry.default-org", cloudFoundryProperties.getDefaultOrg()), - setEnvRequest("spring.cloud.appbroker.deployer.cloudfoundry.default-space", cloudFoundryProperties.getDefaultSpace()), - setEnvRequest("spring.cloud.appbroker.deployer.cloudfoundry.skip-ssl-validation", String.valueOf(cloudFoundryProperties.isSkipSslValidation())) - ); - } - - private Flux getCatalogPublishers() { - return Flux.concat( - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].id", "example-service"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].name", "example"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].description", "A simple example"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].bindable", "true"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].tags[0]", "example"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].plans[0].id", "simple-plan"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].plans[0].bindable", "true"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].plans[0].name", "standard"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].plans[0].description", "A simple plan"), - setEnvRequest("spring.cloud.openservicebroker.catalog.services[0].plans[0].free", "true") - ); - } - - private Mono setEnvRequest(String key, String value) { - return cloudFoundryOperations - .applications() - .setEnvironmentVariable(createEnvRequest("sample-broker", key, value)) - .doOnSuccess(v -> LOGGER.info("Environment with key {} set", key)); + private Mono loggingMono(Mono publisher) { + if (LOGGER.isDebugEnabled()) { + return publisher.log(); + } + else return publisher; } } diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/resources/application.yml b/spring-cloud-app-broker-acceptance-tests/src/test/resources/application.yml index 3474d8a..413c166 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/resources/application.yml +++ b/spring-cloud-app-broker-acceptance-tests/src/test/resources/application.yml @@ -1,3 +1,4 @@ logging: level: - cloudfoundry-client: DEBUG \ No newline at end of file + cloudfoundry-client: ERROR + org.springframework.cloud.appbroker: DEBUG \ No newline at end of file diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/resources/logback.xml b/spring-cloud-app-broker-acceptance-tests/src/test/resources/logback-test.xml similarity index 100% rename from spring-cloud-app-broker-acceptance-tests/src/test/resources/logback.xml rename to spring-cloud-app-broker-acceptance-tests/src/test/resources/logback-test.xml