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