From e877db0aee369a0a1100ebacdc3fe5e4519fe104 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 20 Jun 2023 15:32:51 -0400 Subject: [PATCH 01/46] terra-java-project-template add workflows Revert "rename to java pfb" This reverts commit 166b8c9b6910920a20de9156a4bbb304652fdd84. rename to java pfb --- .github/workflows/build-and-test.yml | 227 ++++++++++++++++++ .github/workflows/integration-tests.yml | 174 ++++++++++++++ .github/workflows/publish.yml | 111 +++++++++ .github/workflows/tag.yml | 22 ++ .github/workflows/trivy.yml | 51 ++++ LICENSE | 29 +++ buildSrc/build.gradle | 26 ++ ....terra.java-application-conventions.gradle | 6 + .../bio.terra.java-common-conventions.gradle | 99 ++++++++ .../bio.terra.java-library-conventions.gradle | 4 + .../bio.terra.java-spring-conventions.gradle | 6 + client/.swagger-codegen-ignore | 2 + client/artifactory.gradle | 49 ++++ client/build.gradle | 18 ++ client/swagger.gradle | 42 ++++ common/postgres-init.sql | 2 + docs/api_versioning.md | 7 + docs/controller_service_dao.md | 42 ++++ docs/transactions.md | 116 +++++++++ gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 ++++++++++++++ gradlew.bat | 89 +++++++ integration/build.gradle | 31 +++ .../scripts/client/JavatemplateClient.java | 48 ++++ .../java/scripts/testscripts/GetStatus.java | 20 ++ .../java/scripts/testscripts/GetVersion.java | 30 +++ .../configs/integration/GetStatus.json | 17 ++ .../configs/integration/GetVersion.json | 17 ++ .../src/main/resources/rendered/.gitignore | 2 + .../src/main/resources/servers/local.json | 11 + .../serviceaccounts/delegate-user-sa.json | 5 + .../serviceaccounts/testrunner-perf.json | 5 + .../suites/local/FullIntegration.json | 9 + .../src/main/resources/testusers/admin.json | 5 + .../src/main/resources/testusers/user.json | 5 + scripts/render_configs.sh | 22 ++ service/build.gradle | 64 +++++ service/generators.gradle | 44 ++++ service/publishing.gradle | 45 ++++ .../main/java/bio/terra/javatemplate/App.java | 65 +++++ .../javatemplate/config/SamConfiguration.java | 6 + .../config/StatusCheckConfiguration.java | 10 + .../config/VersionConfiguration.java | 7 + .../controller/ExampleController.java | 81 +++++++ .../controller/GlobalExceptionHandler.java | 16 ++ .../controller/PublicApiController.java | 53 ++++ .../terra/javatemplate/dao/ExampleDao.java | 48 ++++ .../bio/terra/javatemplate/iam/SamClient.java | 49 ++++ .../terra/javatemplate/iam/SamService.java | 61 +++++ .../bio/terra/javatemplate/model/Example.java | 15 ++ .../service/BaseStatusService.java | 90 +++++++ .../javatemplate/service/ExampleService.java | 28 +++ .../javatemplate/service/StatusService.java | 41 ++++ service/src/main/resources/api/openapi.yml | 200 +++++++++++++++ service/src/main/resources/application.yml | 84 +++++++ .../changelog/changesets/initial_schema.yaml | 26 ++ .../db/changelog/db.changelog-master.yaml | 7 + .../src/main/resources/rendered/.gitignore | 2 + .../src/main/resources/templates/index.html | 144 +++++++++++ .../javatemplate/BaseSpringBootTest.java | 8 + .../api/ExampleControllerTest.java | 103 ++++++++ .../api/PublicApiControllerTest.java | 74 ++++++ .../terra/javatemplate/dao/BaseDaoTest.java | 9 + .../javatemplate/dao/ExampleDaoTest.java | 54 +++++ .../terra/javatemplate/iam/SamClientTest.java | 30 +++ .../javatemplate/iam/SamServiceTest.java | 54 +++++ .../service/BaseStatusServiceTest.java | 26 ++ settings.gradle | 4 + 70 files changed, 3088 insertions(+) create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .github/workflows/integration-tests.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/tag.yml create mode 100644 .github/workflows/trivy.yml create mode 100644 LICENSE create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/groovy/bio.terra.java-application-conventions.gradle create mode 100644 buildSrc/src/main/groovy/bio.terra.java-common-conventions.gradle create mode 100644 buildSrc/src/main/groovy/bio.terra.java-library-conventions.gradle create mode 100644 buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle create mode 100644 client/.swagger-codegen-ignore create mode 100644 client/artifactory.gradle create mode 100644 client/build.gradle create mode 100644 client/swagger.gradle create mode 100644 common/postgres-init.sql create mode 100644 docs/api_versioning.md create mode 100644 docs/controller_service_dao.md create mode 100644 docs/transactions.md create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 integration/build.gradle create mode 100644 integration/src/main/java/scripts/client/JavatemplateClient.java create mode 100644 integration/src/main/java/scripts/testscripts/GetStatus.java create mode 100644 integration/src/main/java/scripts/testscripts/GetVersion.java create mode 100644 integration/src/main/resources/configs/integration/GetStatus.json create mode 100644 integration/src/main/resources/configs/integration/GetVersion.json create mode 100644 integration/src/main/resources/rendered/.gitignore create mode 100644 integration/src/main/resources/servers/local.json create mode 100644 integration/src/main/resources/serviceaccounts/delegate-user-sa.json create mode 100644 integration/src/main/resources/serviceaccounts/testrunner-perf.json create mode 100644 integration/src/main/resources/suites/local/FullIntegration.json create mode 100644 integration/src/main/resources/testusers/admin.json create mode 100644 integration/src/main/resources/testusers/user.json create mode 100755 scripts/render_configs.sh create mode 100644 service/build.gradle create mode 100644 service/generators.gradle create mode 100644 service/publishing.gradle create mode 100644 service/src/main/java/bio/terra/javatemplate/App.java create mode 100644 service/src/main/java/bio/terra/javatemplate/config/SamConfiguration.java create mode 100644 service/src/main/java/bio/terra/javatemplate/config/StatusCheckConfiguration.java create mode 100644 service/src/main/java/bio/terra/javatemplate/config/VersionConfiguration.java create mode 100644 service/src/main/java/bio/terra/javatemplate/controller/ExampleController.java create mode 100644 service/src/main/java/bio/terra/javatemplate/controller/GlobalExceptionHandler.java create mode 100644 service/src/main/java/bio/terra/javatemplate/controller/PublicApiController.java create mode 100644 service/src/main/java/bio/terra/javatemplate/dao/ExampleDao.java create mode 100644 service/src/main/java/bio/terra/javatemplate/iam/SamClient.java create mode 100644 service/src/main/java/bio/terra/javatemplate/iam/SamService.java create mode 100644 service/src/main/java/bio/terra/javatemplate/model/Example.java create mode 100644 service/src/main/java/bio/terra/javatemplate/service/BaseStatusService.java create mode 100644 service/src/main/java/bio/terra/javatemplate/service/ExampleService.java create mode 100644 service/src/main/java/bio/terra/javatemplate/service/StatusService.java create mode 100644 service/src/main/resources/api/openapi.yml create mode 100644 service/src/main/resources/application.yml create mode 100644 service/src/main/resources/db/changelog/changesets/initial_schema.yaml create mode 100644 service/src/main/resources/db/changelog/db.changelog-master.yaml create mode 100644 service/src/main/resources/rendered/.gitignore create mode 100644 service/src/main/resources/templates/index.html create mode 100644 service/src/test/java/bio/terra/javatemplate/BaseSpringBootTest.java create mode 100644 service/src/test/java/bio/terra/javatemplate/api/ExampleControllerTest.java create mode 100644 service/src/test/java/bio/terra/javatemplate/api/PublicApiControllerTest.java create mode 100644 service/src/test/java/bio/terra/javatemplate/dao/BaseDaoTest.java create mode 100644 service/src/test/java/bio/terra/javatemplate/dao/ExampleDaoTest.java create mode 100644 service/src/test/java/bio/terra/javatemplate/iam/SamClientTest.java create mode 100644 service/src/test/java/bio/terra/javatemplate/iam/SamServiceTest.java create mode 100644 service/src/test/java/bio/terra/javatemplate/service/BaseStatusServiceTest.java create mode 100644 settings.gradle diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..a98d0cb0 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,227 @@ +name: Build and Test + +on: + push: + branches: [ main ] + paths-ignore: [ '*.md' ] + pull_request: + branches: [ '**' ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install black and link shellcheck into expected location + run: | + pip install black --force-reinstall black==22.3.0 + sudo ln -s $(which shellcheck) /usr/local/bin/shellcheck + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Build all projects without running tests + run: ./gradlew --build-cache build -x test + + - name: Upload spotbugs results + uses: github/codeql-action/upload-sarif@main + with: + sarif_file: service/build/reports/spotbugs/main.sarif + + jib: + needs: [ build ] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Construct docker image name and tag + id: image-name + run: | + GITHUB_REPO=$(basename ${{ github.repository }}) + GIT_SHORT_HASH=$(git rev-parse --short HEAD) + echo "name=${GITHUB_REPO}:${GIT_SHORT_HASH}" >> $GITHUB_OUTPUT + + - name: Build image locally with jib + run: | + ./gradlew --build-cache :service:jibDockerBuild \ + --image=${{ steps.image-name.outputs.name }} \ + -Djib.console=plain + + dispatch-trivy: + needs: [ build ] + runs-on: ubuntu-latest + + if: github.event_name == 'pull_request' + + steps: + - name: Fire off Trivy action + uses: broadinstitute/workflow-dispatch@v1 + with: + workflow: Trivy + token: ${{ secrets.BROADBOT_TOKEN }} + ref: ${{ github.event.pull_request.head.ref }} + + source-clear: + needs: [ build ] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: SourceClear scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: ./gradlew --build-cache srcclr + + unit-tests-and-sonarqube: + needs: [ build ] + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:13 + env: + POSTGRES_PASSWORD: postgres + ports: [ "5432:5432" ] + + steps: + - uses: actions/checkout@v3 + # Needed by sonar to get the git history for the branch the PR will be merged into. + with: + fetch-depth: 0 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Make sure Postgres is ready and init + env: + PGPASSWORD: postgres + run: | + pg_isready -h localhost -t 10 + psql -h localhost -U postgres -f ./common/postgres-init.sql + + - name: Test with coverage + run: ./gradlew --build-cache test jacocoTestReport + + # The SonarQube scan is done here, so it can upload the coverage report generated by the tests. +# - name: SonarQube scan +# run: ./gradlew --build-cache sonarqube +# env: +# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + integration-tests: + needs: [ build ] + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:13 + env: + POSTGRES_PASSWORD: postgres + ports: [ "5432:5432" ] + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Make sure Postgres is ready and init + env: + PGPASSWORD: postgres + run: | + pg_isready -h localhost -t 10 + psql -h localhost -U postgres -f ./common/postgres-init.sql + + - name: Render GitHub Secrets + run: | + echo "${{ secrets.DEV_FIRECLOUD_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/user-delegated-sa.json" + echo "${{ secrets.PERF_TESTRUNNER_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/testrunner-perf.json" + + - name: Launch the background process for integration tests + run: ./gradlew --build-cache bootRun | tee application.log & + + - name: Wait for boot run to be ready + run: | + set +e + timeout 60 bash -c 'until echo > /dev/tcp/localhost/8080; do sleep 1; done' + resultStatus=$? + set -e + if [[ $resultStatus == 0 ]]; then + echo "Server started successfully" + else + echo "Server did not start successfully" + exit 1 + fi + + - name: Run the integration test suite + run: ./gradlew --build-cache runTest --args="suites/local/FullIntegration.json build/reports" + + - name: Archive logs + id: archive_logs + if: always() + uses: actions/upload-artifact@v3 + with: + name: application-logs + path: | + application.log + + notify-slack: + needs: [ build, unit-tests-and-sonarqube, source-clear, integration-tests ] + runs-on: ubuntu-latest + + if: failure() && github.ref == 'refs/heads/main' + + steps: + - name: Notify slack on failure + uses: broadinstitute/action-slack@v3.8.0 + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + with: + channel: '#jade-data-explorer' + status: failure + author_name: Build on dev + fields: job,message + text: 'Build failed :sadpanda:' + username: 'Data Explorer GitHub Action' + + dispatch-tag: + needs: [ build, unit-tests-and-sonarqube, source-clear, integration-tests ] + runs-on: ubuntu-latest + + if: success() && github.ref == 'refs/heads/main' + + steps: + - name: Fire off tag action + uses: broadinstitute/workflow-dispatch@v1 + with: + workflow: Tag + token: ${{ secrets.BROADBOT_TOKEN }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 00000000..84909b0d --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,174 @@ +name: Integration Tests + +on: + workflow_dispatch: + inputs: + environment: + type: choice + description: 'environment to run test in' + required: true + options: + - staging + - alpha + - dev + default: 'dev' + +env: + TEST_ENV: ${{ github.event.inputs.environment }} + TEST_DEFAULT: dev + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Gradle build service + run: ./gradlew --build-cache :service:build -x test + + jib: + needs: [ build ] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Construct docker image name and tag + id: image-name + run: | + GITHUB_REPO=$(basename ${{ github.repository }}) + GIT_SHORT_HASH=$(git rev-parse --short HEAD) + echo "name=${GITHUB_REPO}:${GIT_SHORT_HASH}" >> $GITHUB_OUTPUT + + - name: Build image locally with jib + run: | + ./gradlew --build-cache :service:jibDockerBuild \ + --image=${{ steps.image-name.outputs.name }} \ + -Djib.console=plain + + dispatch-trivy: + needs: [ build ] + runs-on: ubuntu-latest + + steps: + - name: Fire off Trivy action + uses: broadinstitute/workflow-dispatch@v1 + with: + workflow: Trivy + token: ${{ secrets.BROADBOT_TOKEN }} + + test-env: + runs-on: ubuntu-latest + outputs: + test-env: ${{ steps.test-env.outputs.test-env }} + + steps: + - name: Set default test env + id: test-env + run: | + echo "test-env=${{ env.TEST_ENV || env.TEST_DEFAULT }}" >> $GITHUB_OUTPUT + + test-runner: + needs: [ build, test-env ] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Get the helm chart versions for the test env + run: | + curl -H 'Authorization: token ${{ secrets.BROADBOT_TOKEN }}' \ + -H 'Accept: application/vnd.github.v3.raw' \ + -L https://api.github.com/repos/broadinstitute/terra-helmfile/contents/versions/app/dev.yaml \ + --create-dirs -o "integration/src/main/resources/rendered/dev.yaml" + curl -H 'Authorization: token ${{ secrets.BROADBOT_TOKEN }}' \ + -H 'Accept: application/vnd.github.v3.raw' \ + -L https://api.github.com/repos/broadinstitute/terra-helmfile/contents/environments/live/${{ needs.test-env.outputs.test-env }}.yaml \ + --create-dirs -o "integration/src/main/resources/rendered/${{ needs.test-env.outputs.test-env }}.yaml" + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Render GitHub Secrets + run: | + echo "${{ secrets.DEV_FIRECLOUD_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/user-delegated-sa.json" + echo "${{ secrets.PERF_TESTRUNNER_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/testrunner-perf.json" + + - name: Run integration test suite + run: | + ./gradlew --build-cache runTest --args="suites/${{ needs.test-env.outputs.test-env }}/FullIntegration.json build/reports" + + - name: Upload Test Reports for QA + if: always() + run: | + ./gradlew --build-cache uploadResults --args="CompressDirectoryToTerraKernelK8S.json build/reports" + + - name: Upload Test Reports for GitHub + if: always() + uses: actions/upload-artifact@v1 + with: + name: Test Reports + path: integration/build/reports + + notify-de-slack: + needs: [ build, jib, test-env, test-runner ] + runs-on: ubuntu-latest + if: failure() + + steps: + - name: "Notify #jade-data-explorer Slack on failure" + uses: broadinstitute/action-slack@v3.8.0 + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + STATUS: failure + with: + channel: '#jade-data-explorer' + status: ${{ env.STATUS }} + fields: job,ref + text: > + ${{ format('Catalog test *{0}* in *{1}* {2}', + env.STATUS, needs.test-env.outputs.test-env, + env.STATUS == 'success' && ':check_green:' || ':sadpanda:') }} + username: 'Data Explorer Tests' + + notify-qa-slack: + needs: [ build, jib, test-env, test-runner ] + runs-on: ubuntu-latest + if: always() && needs.test-env.outputs.test-env != 'dev' + + steps: + - name: "Always notify #dsde-qa Slack" + uses: broadinstitute/action-slack@v3.8.0 + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + STATUS: >- + ${{ needs.build.result == 'success' + && needs.jib.result == 'success' + && needs.test-runner.result == 'success' + && 'success' || 'failure' }} + with: + channel: '#dsde-qa' + status: ${{ env.STATUS }} + fields: job,ref + text: > + ${{ format('Catalog test *{0}* in *{1}* {2}', + env.STATUS, needs.test-env.outputs.test-env, + env.STATUS == 'success' && ':check_green:' || ':sadpanda:') }} + username: 'Data Explorer Tests' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..301acc9e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,111 @@ +name: Publish and deploy +on: create + +env: + SERVICE_NAME: ${{ github.event.repository.name }} + GOOGLE_PROJECT: broad-dsp-gcr-public + +jobs: + publish-job: + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: 'read' + id-token: 'write' + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag.outputs.tag }} + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Parse tag + id: tag + run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT + + - name: Publish to Artifactory + run: ./gradlew --build-cache :client:artifactoryPublish + env: + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + ARTIFACTORY_REPO_KEY: "libs-release-local" + + - name: Auth to Google + uses: google-github-actions/auth@v1 + with: + workload_identity_provider: projects/1038484894585/locations/global/workloadIdentityPools/github-wi-pool/providers/github-wi-provider + service_account: gcr-publish@broad-dsp-gcr-public.iam.gserviceaccount.com + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v1 + + - name: Explicitly auth Docker for GCR + run: gcloud auth configure-docker --quiet + + - name: Construct docker image name and tag + id: image-name + run: echo "name=gcr.io/${GOOGLE_PROJECT}/${SERVICE_NAME}:${{ steps.tag.outputs.tag }}" >> $GITHUB_OUTPUT + + - name: Build image locally with jib + run: | + ./gradlew --build-cache :service:jibDockerBuild \ + --image=${{ steps.image-name.outputs.name }} \ + -Djib.console=plain + + - name: Push GCR image + run: docker push ${{ steps.image-name.outputs.name }} + + # - name: Notify slack on failure + # uses: broadinstitute/action-slack@v3.8.0 + # if: failure() + # env: + # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + # with: + # channel: '#jade-data-explorer' + # status: failure + # author_name: Publish to dev + # fields: job + # text: 'Publish failed :sadpanda:' + # username: 'Terra Java Project Template GitHub Action' + + + # + # Hey! You'll probably want to adjust below this point: it deploys newly published versions to dev! + # + # You'll need to create a new chart entry in Beehive first, at https://broad.io/beehive/charts/new (chart + # names can't be changed, so be sure beforehand). Replace 'javatemplate' below with whatever name you choose. + # + # You'll also need to add some access to your new repo to allow it to run these steps. We have docs on the + # whole process here: https://docs.google.com/document/d/1lkUkN2KOpHKWufaqw_RIE7EN3vN4G2xMnYBU83gi8VA/edit#heading=h.ipfs1speial + # + # Lastly, the deployment part won't work until your app has an actual chart and is deployed in dev. We can + # help with that, ping #dsp-devops-champions and we can point you in the right direction. + # + + report-to-sherlock: + # Report new version to Broad DevOps + uses: broadinstitute/sherlock/.github/workflows/client-report-app-version.yaml@main + needs: publish-job + with: + new-version: ${{ needs.publish-job.outputs.tag }} + chart-name: 'javatemplate' + permissions: + contents: 'read' + id-token: 'write' + + set-version-in-dev: + # Put new version in Broad dev environment + uses: broadinstitute/sherlock/.github/workflows/client-set-environment-app-version.yaml@main + needs: [publish-job, report-to-sherlock] + with: + new-version: ${{ needs.publish-job.outputs.tag }} + chart-name: 'javatemplate' + environment-name: 'template-services' + secrets: + sync-git-token: ${{ secrets.BROADBOT_TOKEN }} + permissions: + id-token: 'write' diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 00000000..6a8a4f33 --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,22 @@ +name: Tag +on: workflow_dispatch + +jobs: + tag-job: + runs-on: ubuntu-latest + steps: + - name: Checkout current code + uses: actions/checkout@v3 + with: + token: ${{ secrets.BROADBOT_TOKEN }} # this allows the push to succeed later + - name: Bump the tag to a new version + # https://github.com/DataBiosphere/github-actions/tree/master/actions/bumper + uses: databiosphere/github-actions/actions/bumper@bumper-0.0.6 + id: tag + env: + GITHUB_TOKEN: ${{ secrets.BROADBOT_TOKEN }} + HOTFIX_BRANCHES: hotfix.* + DEFAULT_BUMP: minor + RELEASE_BRANCHES: main + VERSION_FILE_PATH: settings.gradle + VERSION_LINE_MATCH: "^\\s*gradle.ext.releaseVersion\\s*=\\s*'.*'" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 00000000..1010a86f --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,51 @@ +name: Trivy +on: workflow_dispatch + +jobs: + trivy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Build all projects without running tests + run: ./gradlew --build-cache build -x test -x spotlessCheck + + - name: Construct docker image name and tag + id: image-name + run: | + echo "name=trivy-local-testing-image" >> $GITHUB_OUTPUT + + - name: Build image locally with jib + run: | + ./gradlew --build-cache :service:jibDockerBuild \ + --image=${{ steps.image-name.outputs.name }} \ + -Djib.console=plain + + - name: Run Trivy vulnerability scanner + uses: broadinstitute/dsp-appsec-trivy-action@v1 + with: + image: ${{ steps.image-name.outputs.name }} + + notify-slack: + needs: [ trivy ] + runs-on: ubuntu-latest + if: failure() + steps: + - name: Notify slack on failure + uses: broadinstitute/action-slack@v3.8.0 + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + with: + channel: '#jade-data-explorer' + status: failure + author_name: Trivy action + fields: workflow,message + text: 'Trivy scan failure :sadpanda:' + username: 'Data Explorer GitHub Action' diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..46c2fd5d --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, Broad Institute +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 00000000..e7f9717a --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + maven { + url 'https://broadinstitute.jfrog.io/artifactory/plugins-snapshot' + } + gradlePluginPortal() +} + +dependencies { + implementation 'com.diffplug.spotless:spotless-plugin-gradle:6.11.0' + implementation 'com.felipefzdz.gradle.shellcheck:shellcheck:1.4.6' + implementation 'com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:3.3.0' + implementation 'com.srcclr.gradle:com.srcclr.gradle.gradle.plugin:3.1.12' + implementation 'de.undercouch.download:de.undercouch.download.gradle.plugin:5.2.0' + implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' + implementation 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.0.14.RELEASE' + implementation 'org.hidetake.swagger.generator:org.hidetake.swagger.generator.gradle.plugin:2.19.2' + implementation 'org.sonarqube:org.sonarqube.gradle.plugin:3.4.0.2513' + implementation 'org.springframework.boot:spring-boot-gradle-plugin:2.7.0' + implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' + // This is required due to a dependency conflict between jib and srcclr. Removing it will cause jib to fail. + implementation 'org.apache.commons:commons-compress:1.21' +} diff --git a/buildSrc/src/main/groovy/bio.terra.java-application-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-application-conventions.gradle new file mode 100644 index 00000000..329b117c --- /dev/null +++ b/buildSrc/src/main/groovy/bio.terra.java-application-conventions.gradle @@ -0,0 +1,6 @@ +plugins { + // Apply the common convention plugin for shared build configuration between library and application projects. + id 'bio.terra.java-common-conventions' + + id 'application' +} diff --git a/buildSrc/src/main/groovy/bio.terra.java-common-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-common-conventions.gradle new file mode 100644 index 00000000..d0f33742 --- /dev/null +++ b/buildSrc/src/main/groovy/bio.terra.java-common-conventions.gradle @@ -0,0 +1,99 @@ +plugins { + id 'idea' + id 'jacoco' + id 'java' + + id 'com.diffplug.spotless' + id 'com.github.spotbugs' + id 'org.hidetake.swagger.generator' +} + +boolean isCiServer = System.getenv().containsKey("CI") + +if (!isCiServer) { + tasks.withType(JavaExec).configureEach { + systemProperty 'spring.profiles.include', 'human-readable-logging' + } + tasks.withType(Test).configureEach { + systemProperty 'spring.profiles.include', 'human-readable-logging' + } +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + maven { + // Terra proxy for maven central + url 'https://broadinstitute.jfrog.io/broadinstitute/maven-central/' + } + mavenCentral() + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release/' + } + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' + } +} + +dependencies { + compileOnly 'com.github.spotbugs:spotbugs-annotations:4.7.2' + implementation 'io.swagger.core.v3:swagger-annotations:2.2.0' + swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.31' + + implementation 'org.slf4j:slf4j-api' + + testImplementation 'org.hamcrest:hamcrest:2.2' + + implementation 'bio.terra:terra-common-lib:0.0.75-SNAPSHOT' + implementation 'bio.terra:datarepo-client:1.349.0-SNAPSHOT' +} + +tasks.named('test') { + useJUnitPlatform() +} + +version = gradle.releaseVersion +group = 'bio.terra' + +spotless { + java { + targetExclude "${buildDir}/**" + targetExclude "**/swagger-code/**" + googleJavaFormat() + } +} + +// Run spotless check when running in github actions, otherwise run spotless apply. +compileJava { + if (isCiServer) { + dependsOn(spotlessCheck) + } else { + dependsOn(spotlessApply) + } +} + +// Spotbugs configuration +spotbugs { + reportLevel = 'high' + effort = 'max' +} +spotbugsMain { + reports { + if (isCiServer) { + sarif.enabled = true + } else { + html.enabled = true + } + } +} + +jacocoTestReport { + reports { + // sonarqube requires XML coverage output to upload coverage data + xml.required = true + } +} diff --git a/buildSrc/src/main/groovy/bio.terra.java-library-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-library-conventions.gradle new file mode 100644 index 00000000..81623b76 --- /dev/null +++ b/buildSrc/src/main/groovy/bio.terra.java-library-conventions.gradle @@ -0,0 +1,4 @@ +plugins { + id 'bio.terra.java-common-conventions' + id 'java-library' +} diff --git a/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle new file mode 100644 index 00000000..3bc0e9c7 --- /dev/null +++ b/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle @@ -0,0 +1,6 @@ +plugins { + // Apply the common convention plugin for shared build configuration between library and application projects. + id 'bio.terra.java-common-conventions' + + id 'org.springframework.boot' +} diff --git a/client/.swagger-codegen-ignore b/client/.swagger-codegen-ignore new file mode 100644 index 00000000..e5dfce6d --- /dev/null +++ b/client/.swagger-codegen-ignore @@ -0,0 +1,2 @@ +** +!**/src/main/java/** \ No newline at end of file diff --git a/client/artifactory.gradle b/client/artifactory.gradle new file mode 100644 index 00000000..6c0338a6 --- /dev/null +++ b/client/artifactory.gradle @@ -0,0 +1,49 @@ +// This and the test below makes sure the build will fail reasonably if you try +// to publish without the environment variables defined. +def artifactory_username = System.getenv("ARTIFACTORY_USERNAME") +def artifactory_password = System.getenv("ARTIFACTORY_PASSWORD") +def artifactory_repo_key = System.getenv("ARTIFACTORY_REPO_KEY") + +gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.hasTask(artifactoryPublish) && + (artifactory_username == null || artifactory_password == null)) { + throw new GradleException("Set env vars ARTIFACTORY_USERNAME and ARTIFACTORY_PASSWORD to publish") + } +} + +java { + // Builds sources into the published package as part of the 'assemble' task. + withSourcesJar() +} + +publishing { + publications { + javatemplateClientLibrary(MavenPublication) { + artifactId = "javatemplate-client" + from components.java + versionMapping { + usage("java-runtime") { + fromResolutionResult() + } + } + } + } +} + +artifactory { + publish { + contextUrl = "https://broadinstitute.jfrog.io/broadinstitute/" + repository { + repoKey = "${artifactory_repo_key}" // The Artifactory repository key to publish to + username = "${artifactory_username}" // The publisher user name + password = "${artifactory_password}" // The publisher password + } + defaults { + // This is how we tell the Artifactory Plugin which artifacts should be published to Artifactory. + // Reference to Gradle publications defined in the build script. + publications("javatemplateClientLibrary") + publishArtifacts = true + publishPom = true + } + } +} diff --git a/client/build.gradle b/client/build.gradle new file mode 100644 index 00000000..1ed4a117 --- /dev/null +++ b/client/build.gradle @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.plugin.SpringBootPlugin + +plugins { + id 'bio.terra.java-library-conventions' + id 'maven-publish' + id 'io.spring.dependency-management' + id 'com.jfrog.artifactory' version '4.18.2' + id 'org.hidetake.swagger.generator' +} + +dependencyManagement { + imports { + mavenBom(SpringBootPlugin.BOM_COORDINATES) + } +} + +apply from: 'artifactory.gradle' +apply from: 'swagger.gradle' diff --git a/client/swagger.gradle b/client/swagger.gradle new file mode 100644 index 00000000..b46dc5bc --- /dev/null +++ b/client/swagger.gradle @@ -0,0 +1,42 @@ +dependencies { + // Version controlled by dependency management plugin + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + implementation 'org.glassfish.jersey.core:jersey-client' + implementation 'org.glassfish.jersey.media:jersey-media-json-jackson' + implementation 'org.glassfish.jersey.media:jersey-media-multipart' + + implementation 'io.swagger.core.v3:swagger-annotations' + swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli' +} + +def artifactGroup = "${group}.javatemplate" + +generateSwaggerCode { + inputFile = file('../service/src/main/resources/api/openapi.yml') + language = 'java' + library = 'jersey2' + + // For Swagger Codegen v3 on Java 16+ + // See https://github.com/swagger-api/swagger-codegen/issues/10966 + jvmArgs = ['--add-opens=java.base/java.util=ALL-UNNAMED'] + + components = [ + apiDocs : false, apiTests: false, + modelDocs: false, modelTests: false + ] + + additionalProperties = [ + modelPackage : "${artifactGroup}.model", + apiPackage : "${artifactGroup}.api", + invokerPackage: "${artifactGroup}.client", + dateLibrary : 'java11', + java8 : true + ] + + rawOptions = ['--ignore-file-override', "${projectDir}/.swagger-codegen-ignore"] +} + +idea.module.generatedSourceDirs = [file("${generateSwaggerCode.outputDir}/src/main/java")] +sourceSets.main.java.srcDir "${generateSwaggerCode.outputDir}/src/main/java" +compileJava.dependsOn generateSwaggerCode +sourcesJar.dependsOn generateSwaggerCode diff --git a/common/postgres-init.sql b/common/postgres-init.sql new file mode 100644 index 00000000..e49ff37a --- /dev/null +++ b/common/postgres-init.sql @@ -0,0 +1,2 @@ +CREATE ROLE dbuser WITH LOGIN ENCRYPTED PASSWORD 'dbpwd'; +CREATE DATABASE javatemplate_db OWNER dbuser; diff --git a/docs/api_versioning.md b/docs/api_versioning.md new file mode 100644 index 00000000..0bdfbd03 --- /dev/null +++ b/docs/api_versioning.md @@ -0,0 +1,7 @@ +We have taken inspiration for our API versioning standards from the [Google cloud API versioning scheme](https://cloud.google.com/apis/design/versioning). While not exactly identical, for practical purposes it is. + +APIs shall be versioned per interface. An interface is a logical collection of endpoints which can be viewed as a singular entity. A service might implement multiple interfaces. In terms of the correct granularity for an interface, think of it as something which would make sense for a single service to implement. As an example, Cromwell has endpoints both for workflow submission & manipulation but also reading from the metadata store. These could be seen as two separate interfaces as one could imagine a service dedicated to reading from the metadata store. + +Versions shall be bumped on any breaking, non-backwards compatible change. In [Semantic Versioning](http://semver.org/) terms, these would be for major version changes. Unless we encounter reasons to diverge we shall follow the [Google definition of compatibility](https://cloud.google.com/apis/design/compatibility). Versions are expected to change infrequently, with every effort made to integrate required functionality in an additive fashion. + +URLs shall take the form INTERFACE/vVERSION/path. For example: `/api/example/v1/message` where `/api/example` is the interface, `v1` is the version and `message` is the path. diff --git a/docs/controller_service_dao.md b/docs/controller_service_dao.md new file mode 100644 index 00000000..3341e79c --- /dev/null +++ b/docs/controller_service_dao.md @@ -0,0 +1,42 @@ +Terra services are generally organized into 3 layers, controller, service and DAO (data access object). +Controllers handle api requests and responses. +Services implement all business logic. +DAOs implement data persistence and querying. + +# Controller +Controllers are the api entry point to the service. [OpenAPI](../service/src/main/resources/api/openapi.yml) is used to [generate](../service/generators.gradle) `*API` interfaces +which are implemented by controller classes. In this way we can craft a service's api and have the implementation +flow from there. Alternatively we could use java annotations on controller classes to generate OpenAPI +but that tends to lead to a poor API, we think about the api second or not at all. + +Controllers are generally responsible for +* Resolving the user if required +* Checking access control +* Any translation between API model objects and service model objects +* Making service calls +* Translating service responses and errors to API responses and status codes + +Controllers talk to services. It should be an exceptional situation that they talk directly to DAOs. + +Note on access control: There can be some debate on whether this could be elsewhere. Certainly +more complicated or cross resource access control checks can be elsewhere. Pushing access control +checks down to the service layer generally make services less reusable. The most important thing +about access control checks is that it are implemented consistently and readably to avoid mistakes. + +# Service +The service layer handles all business logic. +If it is interesting, the code for it probably lives here. +If it is coordinating anything, the code for it probably lives here. +If it is making a decision, the code for it probably lives here. + +The service layer is the transaction boundary (see [transactions](transactions.md) for more details). + +Services talk to other services and DAOs. + +# DAOs +DAOs or data access objects know how to do data things. Reading or writing to databases. +Accessing other Terra services. DAOs do not make decisions, they get information for the service +layer to use to make decisions. These should be stupid that unconditionally do what they are told +and return undigested information. + +DAOs are the bottom layer and should not call other DAOs or services. \ No newline at end of file diff --git a/docs/transactions.md b/docs/transactions.md new file mode 100644 index 00000000..08e56e79 --- /dev/null +++ b/docs/transactions.md @@ -0,0 +1,116 @@ +# Transaction Barrier +Transactions are tricky, it is best not to have to think about them all the time. +Therefore, it is beneficial to have a consistent place in your code flow where transactions +are handled. This is the transaction barrier. + +If transactions begin too early in the code flow there is risk of them running too long. For example, +making external api calls within a transaction. Long running transactions lead to scalability problems +because there is are greater chances for transactions to collide leading to rollbacks and retries. + +If transactions begin too late in the code flow then you might as well turn on auto-commit. +Essentially the value of transactions disappear because you can't do more than one thing atomically. + +#Transaction Implementation with Spring +Transaction management in Spring starts with an annotation on a method which tells Spring that the entire +method should be wrapped in a transaction. But how does Spring do this? From the caller's perspective, +it is just calling a method on a java class. From the method's code perspective there's no special code. +The answer is that Spring can wrap a Bean inside a Proxy. When the caller invokes a method of a Bean, +it is not on the java class that it appears to be but rather a Proxy that can add special sauce before and +after calling the target java class itself. See [this article](https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring) for more depth. + +This has one very important implication: it is critical that methods are invoked upon the Spring Bean and +not the underlying java class instance. Any injected dependency (e.g. via `@Autowired`) is ok. Using +`this` within a Bean to call other methods may be a problem if features from the Proxy are expected +and the error is not obvious_. Another implication is that transaction annotations only work on public +methods. + +### Examples +Assume a service that wants to perform a transaction, then do some stuff (such as an external api call), then +perform another transaction. + +#### Problematic (Intra-Bean) Code Example +``` +@Service +public class ProblematicService { + public void complexCode() { + doTransactionOne(); + + // code between transactions + + doTransactionTwo(); + } + + @Transactional + public void doTransactionOne() {...} + + @Transactional + public void doTransactionTwo() {...} +} +``` +This code is problematic because the calls from `complexCode` to `doTransactionOne` and `doTransactionTwo` +are internal to the class and do not go through the Proxy. The `@Transactional` annotations have no affect. + +#### Inter-Bean Code Solution +``` +@Service +public class OrchestratingService { + @Autowired LowerLevelService lls; + + public void complexCode() { + lls.doTransactionOne(); + + // code between transactions + + lls.doTransactionTwo(); + } +} + +@Service +public class LowerLevelService() { + @Transactional + public void doTransactionOne() {...} + + @Transactional + public void doTransactionTwo() {...} +} +``` +This code puts the transactions in a separate service. Sometimes this pattern feels like an arbitrary +division of code where it feels more natural for `OrchestratingService` and `LowerLevelService` to be +the same class. Need to be careful to put transactional code in the right place. + +#### Self-Reference Code Solution +``` +@Service +public class SelfReferencingService { + @Autowired SelfReferencingService self; + + public void complexCode() { + self.doTransactionOne(); + + // code between transactions + + self.doTransactionTwo(); + } + + @Transactional + public void doTransactionOne() {...} + + @Transactional + public void doTransactionTwo() {...} +} +``` +This code keeps one class but is written so Spring injects a self reference. `this` is different from `self`, +the former is naked and the latter is clothed in a Proxy. But it is easy to lose track of why one should +use `this` vs. `self` (not everyone is going to read this). + +# Transaction Barrier in the Service Layer +This all boils down to transactions are important and have pitfalls. One thought is to put the transaction +boundary at the DAO layer since they should be self-contained. But this can lead to packing too much +business logic in DAOs or DAOs that span subject areas. This can be ok for cases where the +state being stored is simple. More complicated state argues for transactions in the Service Layer. +But then care is required to structure Service classes so that it is easy to do the right thing. + +# Transactions with Terra Common Library (TCL) +TCL provides 2 handy annotations `@ReadTransaction` and `@WriteTransaction`. These go above +and beyond the Spring provided `@Transactional` annotation by setting the isolation level +to `SERIALIZABLE` and adding appropriate retries. This is tuned for Postgres. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..f97ebb7d --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 zcma&NbC71ylI~qywr$(CZQJHswz}-9F59+k+g;UV+cs{`J?GrGXYR~=-ydruB3JCa zB64N^cILAcWk5iofq)<(fq;O7{th4@;QxID0)qN`mJ?GIqLY#rX8-|G{5M0pdVW5^ zzXk$-2kQTAC?_N@B`&6-N-rmVFE=$QD?>*=4<|!MJu@}isLc4AW#{m2if&A5T5g&~ ziuMQeS*U5sL6J698wOd)K@oK@1{peP5&Esut<#VH^u)gp`9H4)`uE!2$>RTctN+^u z=ASkePDZA-X8)rp%D;p*~P?*a_=*Kwc<^>QSH|^<0>o37lt^+Mj1;4YvJ(JR-Y+?%Nu}JAYj5 z_Qc5%Ao#F?q32i?ZaN2OSNhWL;2oDEw_({7ZbgUjna!Fqn3NzLM@-EWFPZVmc>(fZ z0&bF-Ch#p9C{YJT9Rcr3+Y_uR^At1^BxZ#eo>$PLJF3=;t_$2|t+_6gg5(j{TmjYU zK12c&lE?Eh+2u2&6Gf*IdKS&6?rYbSEKBN!rv{YCm|Rt=UlPcW9j`0o6{66#y5t9C zruFA2iKd=H%jHf%ypOkxLnO8#H}#Zt{8p!oi6)7#NqoF({t6|J^?1e*oxqng9Q2Cc zg%5Vu!em)}Yuj?kaP!D?b?(C*w!1;>R=j90+RTkyEXz+9CufZ$C^umX^+4|JYaO<5 zmIM3#dv`DGM;@F6;(t!WngZSYzHx?9&$xEF70D1BvfVj<%+b#)vz)2iLCrTeYzUcL z(OBnNoG6Le%M+@2oo)&jdOg=iCszzv59e zDRCeaX8l1hC=8LbBt|k5?CXgep=3r9BXx1uR8!p%Z|0+4Xro=xi0G!e{c4U~1j6!) zH6adq0}#l{%*1U(Cb%4AJ}VLWKBPi0MoKFaQH6x?^hQ!6em@993xdtS%_dmevzeNl z(o?YlOI=jl(`L9^ z0O+H9k$_@`6L13eTT8ci-V0ljDMD|0ifUw|Q-Hep$xYj0hTO@0%IS^TD4b4n6EKDG z??uM;MEx`s98KYN(K0>c!C3HZdZ{+_53DO%9k5W%pr6yJusQAv_;IA}925Y%;+!tY z%2k!YQmLLOr{rF~!s<3-WEUs)`ix_mSU|cNRBIWxOox_Yb7Z=~Q45ZNe*u|m^|)d* zog=i>`=bTe!|;8F+#H>EjIMcgWcG2ORD`w0WD;YZAy5#s{65~qfI6o$+Ty&-hyMyJ z3Ra~t>R!p=5ZpxA;QkDAoPi4sYOP6>LT+}{xp}tk+<0k^CKCFdNYG(Es>p0gqD)jP zWOeX5G;9(m@?GOG7g;e74i_|SmE?`B2i;sLYwRWKLy0RLW!Hx`=!LH3&k=FuCsM=9M4|GqzA)anEHfxkB z?2iK-u(DC_T1};KaUT@3nP~LEcENT^UgPvp!QC@Dw&PVAhaEYrPey{nkcn(ro|r7XUz z%#(=$7D8uP_uU-oPHhd>>^adbCSQetgSG`e$U|7mr!`|bU0aHl_cmL)na-5x1#OsVE#m*+k84Y^+UMeSAa zbrVZHU=mFwXEaGHtXQq`2ZtjfS!B2H{5A<3(nb-6ARVV8kEmOkx6D2x7~-6hl;*-*}2Xz;J#a8Wn;_B5=m zl3dY;%krf?i-Ok^Pal-}4F`{F@TYPTwTEhxpZK5WCpfD^UmM_iYPe}wpE!Djai6_{ z*pGO=WB47#Xjb7!n2Ma)s^yeR*1rTxp`Mt4sfA+`HwZf%!7ZqGosPkw69`Ix5Ku6G z@Pa;pjzV&dn{M=QDx89t?p?d9gna*}jBly*#1!6}5K<*xDPJ{wv4& zM$17DFd~L*Te3A%yD;Dp9UGWTjRxAvMu!j^Tbc}2v~q^59d4bz zvu#!IJCy(BcWTc`;v$9tH;J%oiSJ_i7s;2`JXZF+qd4C)vY!hyCtl)sJIC{ebI*0> z@x>;EzyBv>AI-~{D6l6{ST=em*U( z(r$nuXY-#CCi^8Z2#v#UXOt`dbYN1z5jzNF2 z411?w)whZrfA20;nl&C1Gi+gk<`JSm+{|*2o<< zqM#@z_D`Cn|0H^9$|Tah)0M_X4c37|KQ*PmoT@%xHc3L1ZY6(p(sNXHa&49Frzto& zR`c~ClHpE~4Z=uKa5S(-?M8EJ$zt0&fJk~p$M#fGN1-y$7!37hld`Uw>Urri(DxLa;=#rK0g4J)pXMC zxzraOVw1+kNWpi#P=6(qxf`zSdUC?D$i`8ZI@F>k6k zz21?d+dw7b&i*>Kv5L(LH-?J%@WnqT7j#qZ9B>|Zl+=> z^U-pV@1y_ptHo4hl^cPRWewbLQ#g6XYQ@EkiP z;(=SU!yhjHp%1&MsU`FV1Z_#K1&(|5n(7IHbx&gG28HNT)*~-BQi372@|->2Aw5It z0CBpUcMA*QvsPy)#lr!lIdCi@1k4V2m!NH)%Px(vu-r(Q)HYc!p zJ^$|)j^E#q#QOgcb^pd74^JUi7fUmMiNP_o*lvx*q%_odv49Dsv$NV;6J z9GOXKomA{2Pb{w}&+yHtH?IkJJu~}Z?{Uk++2mB8zyvh*xhHKE``99>y#TdD z&(MH^^JHf;g(Tbb^&8P*;_i*2&fS$7${3WJtV7K&&(MBV2~)2KB3%cWg#1!VE~k#C z!;A;?p$s{ihyojEZz+$I1)L}&G~ml=udD9qh>Tu(ylv)?YcJT3ihapi!zgPtWb*CP zlLLJSRCj-^w?@;RU9aL2zDZY1`I3d<&OMuW=c3$o0#STpv_p3b9Wtbql>w^bBi~u4 z3D8KyF?YE?=HcKk!xcp@Cigvzy=lnFgc^9c%(^F22BWYNAYRSho@~*~S)4%AhEttv zvq>7X!!EWKG?mOd9&n>vvH1p4VzE?HCuxT-u+F&mnsfDI^}*-d00-KAauEaXqg3k@ zy#)MGX!X;&3&0s}F3q40ZmVM$(H3CLfpdL?hB6nVqMxX)q=1b}o_PG%r~hZ4gUfSp zOH4qlEOW4OMUc)_m)fMR_rl^pCfXc{$fQbI*E&mV77}kRF z&{<06AJyJ!e863o-V>FA1a9Eemx6>^F$~9ppt()ZbPGfg_NdRXBWoZnDy2;#ODgf! zgl?iOcF7Meo|{AF>KDwTgYrJLb$L2%%BEtO>T$C?|9bAB&}s;gI?lY#^tttY&hfr# zKhC+&b-rpg_?~uVK%S@mQleU#_xCsvIPK*<`E0fHE1&!J7!xD#IB|SSPW6-PyuqGn3^M^Rz%WT{e?OI^svARX&SAdU77V(C~ zM$H{Kg59op{<|8ry9ecfP%=kFm(-!W&?U0@<%z*+!*<e0XesMxRFu9QnGqun6R_%T+B%&9Dtk?*d$Q zb~>84jEAPi@&F@3wAa^Lzc(AJz5gsfZ7J53;@D<;Klpl?sK&u@gie`~vTsbOE~Cd4 z%kr56mI|#b(Jk&;p6plVwmNB0H@0SmgdmjIn5Ne@)}7Vty(yb2t3ev@22AE^s!KaN zyQ>j+F3w=wnx7w@FVCRe+`vUH)3gW%_72fxzqX!S&!dchdkRiHbXW1FMrIIBwjsai8`CB2r4mAbwp%rrO>3B$Zw;9=%fXI9B{d(UzVap7u z6piC-FQ)>}VOEuPpuqznpY`hN4dGa_1Xz9rVg(;H$5Te^F0dDv*gz9JS<|>>U0J^# z6)(4ICh+N_Q`Ft0hF|3fSHs*?a=XC;e`sJaU9&d>X4l?1W=|fr!5ShD|nv$GK;j46@BV6+{oRbWfqOBRb!ir88XD*SbC(LF}I1h#6@dvK%Toe%@ zhDyG$93H8Eu&gCYddP58iF3oQH*zLbNI;rN@E{T9%A8!=v#JLxKyUe}e}BJpB{~uN zqgxRgo0*-@-iaHPV8bTOH(rS(huwK1Xg0u+e!`(Irzu@Bld&s5&bWgVc@m7;JgELd zimVs`>vQ}B_1(2#rv#N9O`fJpVfPc7V2nv34PC);Dzbb;p!6pqHzvy?2pD&1NE)?A zt(t-ucqy@wn9`^MN5apa7K|L=9>ISC>xoc#>{@e}m#YAAa1*8-RUMKwbm|;5p>T`Z zNf*ph@tnF{gmDa3uwwN(g=`Rh)4!&)^oOy@VJaK4lMT&5#YbXkl`q?<*XtsqD z9PRK6bqb)fJw0g-^a@nu`^?71k|m3RPRjt;pIkCo1{*pdqbVs-Yl>4E>3fZx3Sv44grW=*qdSoiZ9?X0wWyO4`yDHh2E!9I!ZFi zVL8|VtW38}BOJHW(Ax#KL_KQzarbuE{(%TA)AY)@tY4%A%P%SqIU~8~-Lp3qY;U-} z`h_Gel7;K1h}7$_5ZZT0&%$Lxxr-<89V&&TCsu}LL#!xpQ1O31jaa{U34~^le*Y%L za?7$>Jk^k^pS^_M&cDs}NgXlR>16AHkSK-4TRaJSh#h&p!-!vQY%f+bmn6x`4fwTp z$727L^y`~!exvmE^W&#@uY!NxJi`g!i#(++!)?iJ(1)2Wk;RN zFK&O4eTkP$Xn~4bB|q8y(btx$R#D`O@epi4ofcETrx!IM(kWNEe42Qh(8*KqfP(c0 zouBl6>Fc_zM+V;F3znbo{x#%!?mH3`_ANJ?y7ppxS@glg#S9^MXu|FM&ynpz3o&Qh z2ujAHLF3($pH}0jXQsa#?t--TnF1P73b?4`KeJ9^qK-USHE)4!IYgMn-7z|=ALF5SNGkrtPG@Y~niUQV2?g$vzJN3nZ{7;HZHzWAeQ;5P|@Tl3YHpyznGG4-f4=XflwSJY+58-+wf?~Fg@1p1wkzuu-RF3j2JX37SQUc? zQ4v%`V8z9ZVZVqS8h|@@RpD?n0W<=hk=3Cf8R?d^9YK&e9ZybFY%jdnA)PeHvtBe- zhMLD+SSteHBq*q)d6x{)s1UrsO!byyLS$58WK;sqip$Mk{l)Y(_6hEIBsIjCr5t>( z7CdKUrJTrW%qZ#1z^n*Lb8#VdfzPw~OIL76aC+Rhr<~;4Tl!sw?Rj6hXj4XWa#6Tp z@)kJ~qOV)^Rh*-?aG>ic2*NlC2M7&LUzc9RT6WM%Cpe78`iAowe!>(T0jo&ivn8-7 zs{Qa@cGy$rE-3AY0V(l8wjI^uB8Lchj@?L}fYal^>T9z;8juH@?rG&g-t+R2dVDBe zq!K%{e-rT5jX19`(bP23LUN4+_zh2KD~EAYzhpEO3MUG8@}uBHH@4J zd`>_(K4q&>*k82(dDuC)X6JuPrBBubOg7qZ{?x!r@{%0);*`h*^F|%o?&1wX?Wr4b z1~&cy#PUuES{C#xJ84!z<1tp9sfrR(i%Tu^jnXy;4`Xk;AQCdFC@?V%|; zySdC7qS|uQRcH}EFZH%mMB~7gi}a0utE}ZE_}8PQH8f;H%PN41Cb9R%w5Oi5el^fd z$n{3SqLCnrF##x?4sa^r!O$7NX!}&}V;0ZGQ&K&i%6$3C_dR%I7%gdQ;KT6YZiQrW zk%q<74oVBV>@}CvJ4Wj!d^?#Zwq(b$E1ze4$99DuNg?6t9H}k_|D7KWD7i0-g*EO7 z;5{hSIYE4DMOK3H%|f5Edx+S0VI0Yw!tsaRS2&Il2)ea^8R5TG72BrJue|f_{2UHa z@w;^c|K3da#$TB0P3;MPlF7RuQeXT$ zS<<|C0OF(k)>fr&wOB=gP8!Qm>F41u;3esv7_0l%QHt(~+n; zf!G6%hp;Gfa9L9=AceiZs~tK+Tf*Wof=4!u{nIO90jH@iS0l+#%8=~%ASzFv7zqSB^?!@N7)kp0t&tCGLmzXSRMRyxCmCYUD2!B`? zhs$4%KO~m=VFk3Buv9osha{v+mAEq=ik3RdK@;WWTV_g&-$U4IM{1IhGX{pAu%Z&H zFfwCpUsX%RKg);B@7OUzZ{Hn{q6Vv!3#8fAg!P$IEx<0vAx;GU%}0{VIsmFBPq_mb zpe^BChDK>sc-WLKl<6 zwbW|e&d&dv9Wu0goueyu>(JyPx1mz0v4E?cJjFuKF71Q1)AL8jHO$!fYT3(;U3Re* zPPOe%*O+@JYt1bW`!W_1!mN&=w3G9ru1XsmwfS~BJ))PhD(+_J_^N6j)sx5VwbWK| zwRyC?W<`pOCY)b#AS?rluxuuGf-AJ=D!M36l{ua?@SJ5>e!IBr3CXIxWw5xUZ@Xrw z_R@%?{>d%Ld4p}nEsiA@v*nc6Ah!MUs?GA7e5Q5lPpp0@`%5xY$C;{%rz24$;vR#* zBP=a{)K#CwIY%p} zXVdxTQ^HS@O&~eIftU+Qt^~(DGxrdi3k}DdT^I7Iy5SMOp$QuD8s;+93YQ!OY{eB24%xY7ml@|M7I(Nb@K_-?F;2?et|CKkuZK_>+>Lvg!>JE~wN`BI|_h6$qi!P)+K-1Hh(1;a`os z55)4Q{oJiA(lQM#;w#Ta%T0jDNXIPM_bgESMCDEg6rM33anEr}=|Fn6)|jBP6Y}u{ zv9@%7*#RI9;fv;Yii5CI+KrRdr0DKh=L>)eO4q$1zmcSmglsV`*N(x=&Wx`*v!!hn6X-l0 zP_m;X??O(skcj+oS$cIdKhfT%ABAzz3w^la-Ucw?yBPEC+=Pe_vU8nd-HV5YX6X8r zZih&j^eLU=%*;VzhUyoLF;#8QsEfmByk+Y~caBqSvQaaWf2a{JKB9B>V&r?l^rXaC z8)6AdR@Qy_BxQrE2Fk?ewD!SwLuMj@&d_n5RZFf7=>O>hzVE*seW3U?_p|R^CfoY`?|#x9)-*yjv#lo&zP=uI`M?J zbzC<^3x7GfXA4{FZ72{PE*-mNHyy59Q;kYG@BB~NhTd6pm2Oj=_ zizmD?MKVRkT^KmXuhsk?eRQllPo2Ubk=uCKiZ&u3Xjj~<(!M94c)Tez@9M1Gfs5JV z->@II)CDJOXTtPrQudNjE}Eltbjq>6KiwAwqvAKd^|g!exgLG3;wP+#mZYr`cy3#39e653d=jrR-ulW|h#ddHu(m9mFoW~2yE zz5?dB%6vF}+`-&-W8vy^OCxm3_{02royjvmwjlp+eQDzFVEUiyO#gLv%QdDSI#3W* z?3!lL8clTaNo-DVJw@ynq?q!%6hTQi35&^>P85G$TqNt78%9_sSJt2RThO|JzM$iL zg|wjxdMC2|Icc5rX*qPL(coL!u>-xxz-rFiC!6hD1IR%|HSRsV3>Kq~&vJ=s3M5y8SG%YBQ|{^l#LGlg!D?E>2yR*eV%9m$_J6VGQ~AIh&P$_aFbh zULr0Z$QE!QpkP=aAeR4ny<#3Fwyw@rZf4?Ewq`;mCVv}xaz+3ni+}a=k~P+yaWt^L z@w67!DqVf7D%7XtXX5xBW;Co|HvQ8WR1k?r2cZD%U;2$bsM%u8{JUJ5Z0k= zZJARv^vFkmWx15CB=rb=D4${+#DVqy5$C%bf`!T0+epLJLnh1jwCdb*zuCL}eEFvE z{rO1%gxg>1!W(I!owu*mJZ0@6FM(?C+d*CeceZRW_4id*D9p5nzMY&{mWqrJomjIZ z97ZNnZ3_%Hx8dn;H>p8m7F#^2;T%yZ3H;a&N7tm=Lvs&lgJLW{V1@h&6Vy~!+Ffbb zv(n3+v)_D$}dqd!2>Y2B)#<+o}LH#%ogGi2-?xRIH)1!SD)u-L65B&bsJTC=LiaF+YOCif2dUX6uAA|#+vNR z>U+KQekVGon)Yi<93(d!(yw1h3&X0N(PxN2{%vn}cnV?rYw z$N^}_o!XUB!mckL`yO1rnUaI4wrOeQ(+&k?2mi47hzxSD`N#-byqd1IhEoh!PGq>t z_MRy{5B0eKY>;Ao3z$RUU7U+i?iX^&r739F)itdrTpAi-NN0=?^m%?{A9Ly2pVv>Lqs6moTP?T2-AHqFD-o_ znVr|7OAS#AEH}h8SRPQ@NGG47dO}l=t07__+iK8nHw^(AHx&Wb<%jPc$$jl6_p(b$ z)!pi(0fQodCHfM)KMEMUR&UID>}m^(!{C^U7sBDOA)$VThRCI0_+2=( zV8mMq0R(#z;C|7$m>$>`tX+T|xGt(+Y48@ZYu#z;0pCgYgmMVbFb!$?%yhZqP_nhn zy4<#3P1oQ#2b51NU1mGnHP$cf0j-YOgAA}A$QoL6JVLcmExs(kU{4z;PBHJD%_=0F z>+sQV`mzijSIT7xn%PiDKHOujX;n|M&qr1T@rOxTdxtZ!&u&3HHFLYD5$RLQ=heur zb>+AFokUVQeJy-#LP*^)spt{mb@Mqe=A~-4p0b+Bt|pZ+@CY+%x}9f}izU5;4&QFE zO1bhg&A4uC1)Zb67kuowWY4xbo&J=%yoXlFB)&$d*-}kjBu|w!^zbD1YPc0-#XTJr z)pm2RDy%J3jlqSMq|o%xGS$bPwn4AqitC6&e?pqWcjWPt{3I{>CBy;hg0Umh#c;hU3RhCUX=8aR>rmd` z7Orw(5tcM{|-^J?ZAA9KP|)X6n9$-kvr#j5YDecTM6n z&07(nD^qb8hpF0B^z^pQ*%5ePYkv&FabrlI61ntiVp!!C8y^}|<2xgAd#FY=8b*y( zuQOuvy2`Ii^`VBNJB&R!0{hABYX55ooCAJSSevl4RPqEGb)iy_0H}v@vFwFzD%>#I>)3PsouQ+_Kkbqy*kKdHdfkN7NBcq%V{x^fSxgXpg7$bF& zj!6AQbDY(1u#1_A#1UO9AxiZaCVN2F0wGXdY*g@x$ByvUA?ePdide0dmr#}udE%K| z3*k}Vv2Ew2u1FXBaVA6aerI36R&rzEZeDDCl5!t0J=ug6kuNZzH>3i_VN`%BsaVB3 zQYw|Xub_SGf{)F{$ZX5`Jc!X!;eybjP+o$I{Z^Hsj@D=E{MnnL+TbC@HEU2DjG{3-LDGIbq()U87x4eS;JXnSh;lRlJ z>EL3D>wHt-+wTjQF$fGyDO$>d+(fq@bPpLBS~xA~R=3JPbS{tzN(u~m#Po!?H;IYv zE;?8%^vle|%#oux(Lj!YzBKv+Fd}*Ur-dCBoX*t{KeNM*n~ZPYJ4NNKkI^MFbz9!v z4(Bvm*Kc!-$%VFEewYJKz-CQN{`2}KX4*CeJEs+Q(!kI%hN1!1P6iOq?ovz}X0IOi z)YfWpwW@pK08^69#wSyCZkX9?uZD?C^@rw^Y?gLS_xmFKkooyx$*^5#cPqntNTtSG zlP>XLMj2!VF^0k#ole7`-c~*~+_T5ls?x4)ah(j8vo_ zwb%S8qoaZqY0-$ZI+ViIA_1~~rAH7K_+yFS{0rT@eQtTAdz#8E5VpwnW!zJ_^{Utv zlW5Iar3V5t&H4D6A=>?mq;G92;1cg9a2sf;gY9pJDVKn$DYdQlvfXq}zz8#LyPGq@ z+`YUMD;^-6w&r-82JL7mA8&M~Pj@aK!m{0+^v<|t%APYf7`}jGEhdYLqsHW-Le9TL z_hZZ1gbrz7$f9^fAzVIP30^KIz!!#+DRLL+qMszvI_BpOSmjtl$hh;&UeM{ER@INV zcI}VbiVTPoN|iSna@=7XkP&-4#06C};8ajbxJ4Gcq8(vWv4*&X8bM^T$mBk75Q92j z1v&%a;OSKc8EIrodmIiw$lOES2hzGDcjjB`kEDfJe{r}yE6`eZL zEB`9u>Cl0IsQ+t}`-cx}{6jqcANucqIB>Qmga_&<+80E2Q|VHHQ$YlAt{6`Qu`HA3 z03s0-sSlwbvgi&_R8s={6<~M^pGvBNjKOa>tWenzS8s zR>L7R5aZ=mSU{f?ib4Grx$AeFvtO5N|D>9#)ChH#Fny2maHWHOf2G=#<9Myot#+4u zWVa6d^Vseq_0=#AYS(-m$Lp;*8nC_6jXIjEM`omUmtH@QDs3|G)i4j*#_?#UYVZvJ z?YjT-?!4Q{BNun;dKBWLEw2C-VeAz`%?A>p;)PL}TAZn5j~HK>v1W&anteARlE+~+ zj>c(F;?qO3pXBb|#OZdQnm<4xWmn~;DR5SDMxt0UK_F^&eD|KZ=O;tO3vy4@4h^;2 zUL~-z`-P1aOe?|ZC1BgVsL)2^J-&vIFI%q@40w0{jjEfeVl)i9(~bt2z#2Vm)p`V_ z1;6$Ae7=YXk#=Qkd24Y23t&GvRxaOoad~NbJ+6pxqzJ>FY#Td7@`N5xp!n(c!=RE& z&<<@^a$_Ys8jqz4|5Nk#FY$~|FPC0`*a5HH!|Gssa9=~66&xG9)|=pOOJ2KE5|YrR zw!w6K2aC=J$t?L-;}5hn6mHd%hC;p8P|Dgh6D>hGnXPgi;6r+eA=?f72y9(Cf_ho{ zH6#)uD&R=73^$$NE;5piWX2bzR67fQ)`b=85o0eOLGI4c-Tb@-KNi2pz=Ke@SDcPn za$AxXib84`!Sf;Z3B@TSo`Dz7GM5Kf(@PR>Ghzi=BBxK8wRp>YQoXm+iL>H*Jo9M3 z6w&E?BC8AFTFT&Tv8zf+m9<&S&%dIaZ)Aoqkak_$r-2{$d~0g2oLETx9Y`eOAf14QXEQw3tJne;fdzl@wV#TFXSLXM2428F-Q}t+n2g%vPRMUzYPvzQ9f# zu(liiJem9P*?0%V@RwA7F53r~|I!Ty)<*AsMX3J{_4&}{6pT%Tpw>)^|DJ)>gpS~1rNEh z0$D?uO8mG?H;2BwM5a*26^7YO$XjUm40XmBsb63MoR;bJh63J;OngS5sSI+o2HA;W zdZV#8pDpC9Oez&L8loZO)MClRz!_!WD&QRtQxnazhT%Vj6Wl4G11nUk8*vSeVab@N#oJ}`KyJv+8Mo@T1-pqZ1t|?cnaVOd;1(h9 z!$DrN=jcGsVYE-0-n?oCJ^4x)F}E;UaD-LZUIzcD?W^ficqJWM%QLy6QikrM1aKZC zi{?;oKwq^Vsr|&`i{jIphA8S6G4)$KGvpULjH%9u(Dq247;R#l&I0{IhcC|oBF*Al zvLo7Xte=C{aIt*otJD}BUq)|_pdR>{zBMT< z(^1RpZv*l*m*OV^8>9&asGBo8h*_4q*)-eCv*|Pq=XNGrZE)^(SF7^{QE_~4VDB(o zVcPA_!G+2CAtLbl+`=Q~9iW`4ZRLku!uB?;tWqVjB0lEOf}2RD7dJ=BExy=<9wkb- z9&7{XFA%n#JsHYN8t5d~=T~5DcW4$B%3M+nNvC2`0!#@sckqlzo5;hhGi(D9=*A4` z5ynobawSPRtWn&CDLEs3Xf`(8^zDP=NdF~F^s&={l7(aw&EG}KWpMjtmz7j_VLO;@ zM2NVLDxZ@GIv7*gzl1 zjq78tv*8#WSY`}Su0&C;2F$Ze(q>F(@Wm^Gw!)(j;dk9Ad{STaxn)IV9FZhm*n+U} zi;4y*3v%A`_c7a__DJ8D1b@dl0Std3F||4Wtvi)fCcBRh!X9$1x!_VzUh>*S5s!oq z;qd{J_r79EL2wIeiGAqFstWtkfIJpjVh%zFo*=55B9Zq~y0=^iqHWfQl@O!Ak;(o*m!pZqe9 z%U2oDOhR)BvW8&F70L;2TpkzIutIvNQaTjjs5V#8mV4!NQ}zN=i`i@WI1z0eN-iCS z;vL-Wxc^Vc_qK<5RPh(}*8dLT{~GzE{w2o$2kMFaEl&q zP{V=>&3kW7tWaK-Exy{~`v4J0U#OZBk{a9{&)&QG18L@6=bsZ1zC_d{{pKZ-Ey>I> z;8H0t4bwyQqgu4hmO`3|4K{R*5>qnQ&gOfdy?z`XD%e5+pTDzUt3`k^u~SaL&XMe= z9*h#kT(*Q9jO#w2Hd|Mr-%DV8i_1{J1MU~XJ3!WUplhXDYBpJH><0OU`**nIvPIof z|N8@I=wA)sf45SAvx||f?Z5uB$kz1qL3Ky_{%RPdP5iN-D2!p5scq}buuC00C@jom zhfGKm3|f?Z0iQ|K$Z~!`8{nmAS1r+fp6r#YDOS8V*;K&Gs7Lc&f^$RC66O|)28oh`NHy&vq zJh+hAw8+ybTB0@VhWN^0iiTnLsCWbS_y`^gs!LX!Lw{yE``!UVzrV24tP8o;I6-65 z1MUiHw^{bB15tmrVT*7-#sj6cs~z`wk52YQJ*TG{SE;KTm#Hf#a~|<(|ImHH17nNM z`Ub{+J3dMD!)mzC8b(2tZtokKW5pAwHa?NFiso~# z1*iaNh4lQ4TS)|@G)H4dZV@l*Vd;Rw;-;odDhW2&lJ%m@jz+Panv7LQm~2Js6rOW3 z0_&2cW^b^MYW3)@o;neZ<{B4c#m48dAl$GCc=$>ErDe|?y@z`$uq3xd(%aAsX)D%l z>y*SQ%My`yDP*zof|3@_w#cjaW_YW4BdA;#Glg1RQcJGY*CJ9`H{@|D+*e~*457kd z73p<%fB^PV!Ybw@)Dr%(ZJbX}xmCStCYv#K3O32ej{$9IzM^I{6FJ8!(=azt7RWf4 z7ib0UOPqN40X!wOnFOoddd8`!_IN~9O)#HRTyjfc#&MCZ zZAMzOVB=;qwt8gV?{Y2?b=iSZG~RF~uyx18K)IDFLl})G1v@$(s{O4@RJ%OTJyF+Cpcx4jmy|F3euCnMK!P2WTDu5j z{{gD$=M*pH!GGzL%P)V2*ROm>!$Y=z|D`!_yY6e7SU$~a5q8?hZGgaYqaiLnkK%?0 zs#oI%;zOxF@g*@(V4p!$7dS1rOr6GVs6uYCTt2h)eB4?(&w8{#o)s#%gN@BBosRUe z)@P@8_Zm89pr~)b>e{tbPC~&_MR--iB{=)y;INU5#)@Gix-YpgP<-c2Ms{9zuCX|3 z!p(?VaXww&(w&uBHzoT%!A2=3HAP>SDxcljrego7rY|%hxy3XlODWffO_%g|l+7Y_ zqV(xbu)s4lV=l7M;f>vJl{`6qBm>#ZeMA}kXb97Z)?R97EkoI?x6Lp0yu1Z>PS?2{ z0QQ(8D)|lc9CO3B~e(pQM&5(1y&y=e>C^X$`)_&XuaI!IgDTVqt31wX#n+@!a_A0ZQkA zCJ2@M_4Gb5MfCrm5UPggeyh)8 zO9?`B0J#rkoCx(R0I!ko_2?iO@|oRf1;3r+i)w-2&j?=;NVIdPFsB)`|IC0zk6r9c zRrkfxWsiJ(#8QndNJj@{@WP2Ackr|r1VxV{7S&rSU(^)-M8gV>@UzOLXu9K<{6e{T zXJ6b92r$!|lwjhmgqkdswY&}c)KW4A)-ac%sU;2^fvq7gfUW4Bw$b!i@duy1CAxSn z(pyh$^Z=&O-q<{bZUP+$U}=*#M9uVc>CQVgDs4swy5&8RAHZ~$)hrTF4W zPsSa~qYv_0mJnF89RnnJTH`3}w4?~epFl=D(35$ zWa07ON$`OMBOHgCmfO(9RFc<)?$x)N}Jd2A(<*Ll7+4jrRt9w zwGxExUXd9VB#I|DwfxvJ;HZ8Q{37^wDhaZ%O!oO(HpcqfLH%#a#!~;Jl7F5>EX_=8 z{()l2NqPz>La3qJR;_v+wlK>GsHl;uRA8%j`A|yH@k5r%55S9{*Cp%uw6t`qc1!*T za2OeqtQj7sAp#Q~=5Fs&aCR9v>5V+s&RdNvo&H~6FJOjvaj--2sYYBvMq;55%z8^o z|BJDA4vzfow#DO#ZQHh;Oq_{r+qP{R9ox2TOgwQiv7Ow!zjN+A@BN;0tA2lUb#+zO z(^b89eV)D7UVE+h{mcNc6&GtpOqDn_?VAQ)Vob$hlFwW%xh>D#wml{t&Ofmm_d_+; zKDxzdr}`n2Rw`DtyIjrG)eD0vut$}dJAZ0AohZ+ZQdWXn_Z@dI_y=7t3q8x#pDI-K z2VVc&EGq445Rq-j0=U=Zx`oBaBjsefY;%)Co>J3v4l8V(T8H?49_@;K6q#r~Wwppc z4XW0(4k}cP=5ex>-Xt3oATZ~bBWKv)aw|I|Lx=9C1s~&b77idz({&q3T(Y(KbWO?+ zmcZ6?WeUsGk6>km*~234YC+2e6Zxdl~<_g2J|IE`GH%n<%PRv-50; zH{tnVts*S5*_RxFT9eM0z-pksIb^drUq4>QSww=u;UFCv2AhOuXE*V4z?MM`|ABOC4P;OfhS(M{1|c%QZ=!%rQTDFx`+}?Kdx$&FU?Y<$x;j7z=(;Lyz+?EE>ov!8vvMtSzG!nMie zsBa9t8as#2nH}n8xzN%W%U$#MHNXmDUVr@GX{?(=yI=4vks|V)!-W5jHsU|h_&+kY zS_8^kd3jlYqOoiI`ZqBVY!(UfnAGny!FowZWY_@YR0z!nG7m{{)4OS$q&YDyw6vC$ zm4!$h>*|!2LbMbxS+VM6&DIrL*X4DeMO!@#EzMVfr)e4Tagn~AQHIU8?e61TuhcKD zr!F4(kEebk(Wdk-?4oXM(rJwanS>Jc%<>R(siF+>+5*CqJLecP_we33iTFTXr6W^G z7M?LPC-qFHK;E!fxCP)`8rkxZyFk{EV;G-|kwf4b$c1k0atD?85+|4V%YATWMG|?K zLyLrws36p%Qz6{}>7b>)$pe>mR+=IWuGrX{3ZPZXF3plvuv5Huax86}KX*lbPVr}L z{C#lDjdDeHr~?l|)Vp_}T|%$qF&q#U;ClHEPVuS+Jg~NjC1RP=17=aQKGOcJ6B3mp z8?4*-fAD~}sX*=E6!}^u8)+m2j<&FSW%pYr_d|p_{28DZ#Cz0@NF=gC-o$MY?8Ca8 zr5Y8DSR^*urS~rhpX^05r30Ik#2>*dIOGxRm0#0YX@YQ%Mg5b6dXlS!4{7O_kdaW8PFSdj1=ryI-=5$fiieGK{LZ+SX(1b=MNL!q#lN zv98?fqqTUH8r8C7v(cx#BQ5P9W>- zmW93;eH6T`vuJ~rqtIBg%A6>q>gnWb3X!r0wh_q;211+Om&?nvYzL1hhtjB zK_7G3!n7PL>d!kj){HQE zE8(%J%dWLh1_k%gVXTZt zEdT09XSKAx27Ncaq|(vzL3gm83q>6CAw<$fTnMU05*xAe&rDfCiu`u^1)CD<>sx0i z*hr^N_TeN89G(nunZoLBf^81#pmM}>JgD@Nn1l*lN#a=B=9pN%tmvYFjFIoKe_(GF z-26x{(KXdfsQL7Uv6UtDuYwV`;8V3w>oT_I<`Ccz3QqK9tYT5ZQzbop{=I=!pMOCb zCU68`n?^DT%^&m>A%+-~#lvF!7`L7a{z<3JqIlk1$<||_J}vW1U9Y&eX<}l8##6i( zZcTT@2`9(Mecptm@{3A_Y(X`w9K0EwtPq~O!16bq{7c0f7#(3wn-^)h zxV&M~iiF!{-6A@>o;$RzQ5A50kxXYj!tcgme=Qjrbje~;5X2xryU;vH|6bE(8z^<7 zQ>BG7_c*JG8~K7Oe68i#0~C$v?-t@~@r3t2inUnLT(c=URpA9kA8uq9PKU(Ps(LVH zqgcqW>Gm?6oV#AldDPKVRcEyQIdTT`Qa1j~vS{<;SwyTdr&3*t?J)y=M7q*CzucZ&B0M=joT zBbj@*SY;o2^_h*>R0e({!QHF0=)0hOj^B^d*m>SnRrwq>MolNSgl^~r8GR#mDWGYEIJA8B<|{{j?-7p zVnV$zancW3&JVDtVpIlI|5djKq0(w$KxEFzEiiL=h5Jw~4Le23@s(mYyXWL9SX6Ot zmb)sZaly_P%BeX_9 zw&{yBef8tFm+%=--m*J|o~+Xg3N+$IH)t)=fqD+|fEk4AAZ&!wcN5=mi~Vvo^i`}> z#_3ahR}Ju)(Px7kev#JGcSwPXJ2id9%Qd2A#Uc@t8~egZ8;iC{e! z%=CGJOD1}j!HW_sgbi_8suYnn4#Ou}%9u)dXd3huFIb!ytlX>Denx@pCS-Nj$`VO&j@(z!kKSP0hE4;YIP#w9ta=3DO$7f*x zc9M4&NK%IrVmZAe=r@skWD`AEWH=g+r|*13Ss$+{c_R!b?>?UaGXlw*8qDmY#xlR= z<0XFbs2t?8i^G~m?b|!Hal^ZjRjt<@a? z%({Gn14b4-a|#uY^=@iiKH+k?~~wTj5K1A&hU z2^9-HTC)7zpoWK|$JXaBL6C z#qSNYtY>65T@Zs&-0cHeu|RX(Pxz6vTITdzJdYippF zC-EB+n4}#lM7`2Ry~SO>FxhKboIAF#Z{1wqxaCb{#yEFhLuX;Rx(Lz%T`Xo1+a2M}7D+@wol2)OJs$TwtRNJ={( zD@#zTUEE}#Fz#&(EoD|SV#bayvr&E0vzmb%H?o~46|FAcx?r4$N z&67W3mdip-T1RIxwSm_&(%U|+WvtGBj*}t69XVd&ebn>KOuL(7Y8cV?THd-(+9>G7*Nt%T zcH;`p={`SOjaf7hNd(=37Lz3-51;58JffzIPgGs_7xIOsB5p2t&@v1mKS$2D$*GQ6 zM(IR*j4{nri7NMK9xlDy-hJW6sW|ZiDRaFiayj%;(%51DN!ZCCCXz+0Vm#};70nOx zJ#yA0P3p^1DED;jGdPbQWo0WATN=&2(QybbVdhd=Vq*liDk`c7iZ?*AKEYC#SY&2g z&Q(Ci)MJ{mEat$ZdSwTjf6h~roanYh2?9j$CF@4hjj_f35kTKuGHvIs9}Re@iKMxS-OI*`0S z6s)fOtz}O$T?PLFVSeOjSO26$@u`e<>k(OSP!&YstH3ANh>)mzmKGNOwOawq-MPXe zy4xbeUAl6tamnx))-`Gi2uV5>9n(73yS)Ukma4*7fI8PaEwa)dWHs6QA6>$}7?(L8 ztN8M}?{Tf!Zu22J5?2@95&rQ|F7=FK-hihT-vDp!5JCcWrVogEnp;CHenAZ)+E+K5 z$Cffk5sNwD_?4+ymgcHR(5xgt20Z8M`2*;MzOM#>yhk{r3x=EyM226wb&!+j`W<%* zSc&|`8!>dn9D@!pYow~(DsY_naSx7(Z4i>cu#hA5=;IuI88}7f%)bRkuY2B;+9Uep zpXcvFWkJ!mQai63BgNXG26$5kyhZ2&*3Q_tk)Ii4M>@p~_~q_cE!|^A;_MHB;7s#9 zKzMzK{lIxotjc};k67^Xsl-gS!^*m*m6kn|sbdun`O?dUkJ{0cmI0-_2y=lTAfn*Y zKg*A-2sJq)CCJgY0LF-VQvl&6HIXZyxo2#!O&6fOhbHXC?%1cMc6y^*dOS{f$=137Ds1m01qs`>iUQ49JijsaQ( zksqV9@&?il$|4Ua%4!O15>Zy&%gBY&wgqB>XA3!EldQ%1CRSM(pp#k~-pkcCg4LAT zXE=puHbgsw)!xtc@P4r~Z}nTF=D2~j(6D%gTBw$(`Fc=OOQ0kiW$_RDd=hcO0t97h zb86S5r=>(@VGy1&#S$Kg_H@7G^;8Ue)X5Y+IWUi`o;mpvoV)`fcVk4FpcT|;EG!;? zHG^zrVVZOm>1KFaHlaogcWj(v!S)O(Aa|Vo?S|P z5|6b{qkH(USa*Z7-y_Uvty_Z1|B{rTS^qmEMLEYUSk03_Fg&!O3BMo{b^*`3SHvl0 zhnLTe^_vVIdcSHe)SQE}r~2dq)VZJ!aSKR?RS<(9lzkYo&dQ?mubnWmgMM37Nudwo z3Vz@R{=m2gENUE3V4NbIzAA$H1z0pagz94-PTJyX{b$yndsdKptmlKQKaaHj@3=ED zc7L?p@%ui|RegVYutK$64q4pe9+5sv34QUpo)u{1ci?)_7gXQd{PL>b0l(LI#rJmN zGuO+%GO`xneFOOr4EU(Wg}_%bhzUf;d@TU+V*2#}!2OLwg~%D;1FAu=Un>OgjPb3S z7l(riiCwgghC=Lm5hWGf5NdGp#01xQ59`HJcLXbUR3&n%P(+W2q$h2Qd z*6+-QXJ*&Kvk9ht0f0*rO_|FMBALen{j7T1l%=Q>gf#kma zQlg#I9+HB+z*5BMxdesMND`_W;q5|FaEURFk|~&{@qY32N$G$2B=&Po{=!)x5b!#n zxLzblkq{yj05#O7(GRuT39(06FJlalyv<#K4m}+vs>9@q-&31@1(QBv82{}Zkns~K ze{eHC_RDX0#^A*JQTwF`a=IkE6Ze@j#-8Q`tTT?k9`^ZhA~3eCZJ-Jr{~7Cx;H4A3 zcZ+Zj{mzFZbVvQ6U~n>$U2ZotGsERZ@}VKrgGh0xM;Jzt29%TX6_&CWzg+YYMozrM z`nutuS)_0dCM8UVaKRj804J4i%z2BA_8A4OJRQ$N(P9Mfn-gF;4#q788C@9XR0O3< zsoS4wIoyt046d+LnSCJOy@B@Uz*#GGd#+Ln1ek5Dv>(ZtD@tgZlPnZZJGBLr^JK+!$$?A_fA3LOrkoDRH&l7 zcMcD$Hsjko3`-{bn)jPL6E9Ds{WskMrivsUu5apD z?grQO@W7i5+%X&E&p|RBaEZ(sGLR@~(y^BI@lDMot^Ll?!`90KT!JXUhYS`ZgX3jnu@Ja^seA*M5R@f`=`ynQV4rc$uT1mvE?@tz)TN<=&H1%Z?5yjxcpO+6y_R z6EPuPKM5uxKpmZfT(WKjRRNHs@ib)F5WAP7QCADvmCSD#hPz$V10wiD&{NXyEwx5S z6NE`3z!IS^$s7m}PCwQutVQ#~w+V z=+~->DI*bR2j0^@dMr9`p>q^Ny~NrAVxrJtX2DUveic5vM%#N*XO|?YAWwNI$Q)_) zvE|L(L1jP@F%gOGtnlXtIv2&1i8q<)Xfz8O3G^Ea~e*HJsQgBxWL(yuLY+jqUK zRE~`-zklrGog(X}$9@ZVUw!8*=l`6mzYLtsg`AvBYz(cxmAhr^j0~(rzXdiOEeu_p zE$sf2(w(BPAvO5DlaN&uQ$4@p-b?fRs}d7&2UQ4Fh?1Hzu*YVjcndqJLw0#q@fR4u zJCJ}>_7-|QbvOfylj+e^_L`5Ep9gqd>XI3-O?Wp z-gt*P29f$Tx(mtS`0d05nHH=gm~Po_^OxxUwV294BDKT>PHVlC5bndncxGR!n(OOm znsNt@Q&N{TLrmsoKFw0&_M9$&+C24`sIXGWgQaz=kY;S{?w`z^Q0JXXBKFLj0w0U6P*+jPKyZHX9F#b0D1$&(- zrm8PJd?+SrVf^JlfTM^qGDK&-p2Kdfg?f>^%>1n8bu&byH(huaocL>l@f%c*QkX2i znl}VZ4R1en4S&Bcqw?$=Zi7ohqB$Jw9x`aM#>pHc0x z0$!q7iFu zZ`tryM70qBI6JWWTF9EjgG@>6SRzsd}3h+4D8d~@CR07P$LJ}MFsYi-*O%XVvD@yT|rJ+Mk zDllJ7$n0V&A!0flbOf)HE6P_afPWZmbhpliqJuw=-h+r;WGk|ntkWN(8tKlYpq5Ow z(@%s>IN8nHRaYb*^d;M(D$zGCv5C|uqmsDjwy4g=Lz>*OhO3z=)VD}C<65;`89Ye} zSCxrv#ILzIpEx1KdLPlM&%Cctf@FqTKvNPXC&`*H9=l=D3r!GLM?UV zOxa(8ZsB`&+76S-_xuj?G#wXBfDY@Z_tMpXJS7^mp z@YX&u0jYw2A+Z+bD#6sgVK5ZgdPSJV3>{K^4~%HV?rn~4D)*2H!67Y>0aOmzup`{D zzDp3c9yEbGCY$U<8biJ_gB*`jluz1ShUd!QUIQJ$*1;MXCMApJ^m*Fiv88RZ zFopLViw}{$Tyhh_{MLGIE2~sZ)t0VvoW%=8qKZ>h=adTe3QM$&$PO2lfqH@brt!9j ziePM8$!CgE9iz6B<6_wyTQj?qYa;eC^{x_0wuwV~W+^fZmFco-o%wsKSnjXFEx02V zF5C2t)T6Gw$Kf^_c;Ei3G~uC8SM-xyycmXyC2hAVi-IfXqhu$$-C=*|X?R0~hu z8`J6TdgflslhrmDZq1f?GXF7*ALeMmOEpRDg(s*H`4>_NAr`2uqF;k;JQ+8>A|_6ZNsNLECC%NNEb1Y1dP zbIEmNpK)#XagtL4R6BC{C5T(+=yA-(Z|Ap}U-AfZM#gwVpus3(gPn}Q$CExObJ5AC z)ff9Yk?wZ}dZ-^)?cbb9Fw#EjqQ8jxF4G3=L?Ra zg_)0QDMV1y^A^>HRI$x?Op@t;oj&H@1xt4SZ9(kifQ zb59B*`M99Td7@aZ3UWvj1rD0sE)d=BsBuW*KwkCds7ay(7*01_+L}b~7)VHI>F_!{ zyxg-&nCO?v#KOUec0{OOKy+sjWA;8rTE|Lv6I9H?CI?H(mUm8VXGwU$49LGpz&{nQp2}dinE1@lZ1iox6{ghN&v^GZv9J${7WaXj)<0S4g_uiJ&JCZ zr8-hsu`U%N;+9N^@&Q0^kVPB3)wY(rr}p7{p0qFHb3NUUHJb672+wRZs`gd1UjKPX z4o6zljKKA+Kkj?H>Ew63o%QjyBk&1!P22;MkD>sM0=z_s-G{mTixJCT9@_|*(p^bz zJ8?ZZ&;pzV+7#6Mn`_U-)k8Pjg?a;|Oe^us^PoPY$Va~yi8|?+&=y$f+lABT<*pZr zP}D{~Pq1Qyni+@|aP;ixO~mbEW9#c0OU#YbDZIaw=_&$K%Ep2f%hO^&P67hApZe`x zv8b`Mz@?M_7-)b!lkQKk)JXXUuT|B8kJlvqRmRpxtQDgvrHMXC1B$M@Y%Me!BSx3P z#2Eawl$HleZhhTS6Txm>lN_+I`>eV$&v9fOg)%zVn3O5mI*lAl>QcHuW6!Kixmq`X zBCZ*Ck6OYtDiK!N47>jxI&O2a9x7M|i^IagRr-fmrmikEQGgw%J7bO|)*$2FW95O4 zeBs>KR)izRG1gRVL;F*sr8A}aRHO0gc$$j&ds8CIO1=Gwq1%_~E)CWNn9pCtBE}+`Jelk4{>S)M)`Ll=!~gnn1yq^EX(+y*ik@3Ou0qU`IgYi3*doM+5&dU!cho$pZ zn%lhKeZkS72P?Cf68<#kll_6OAO26bIbueZx**j6o;I0cS^XiL`y+>{cD}gd%lux} z)3N>MaE24WBZ}s0ApfdM;5J_Ny}rfUyxfkC``Awo2#sgLnGPewK};dORuT?@I6(5~ z?kE)Qh$L&fwJXzK){iYx!l5$Tt|^D~MkGZPA}(o6f7w~O2G6Vvzdo*a;iXzk$B66$ zwF#;wM7A+(;uFG4+UAY(2`*3XXx|V$K8AYu#ECJYSl@S=uZW$ksfC$~qrrbQj4??z-)uz0QL}>k^?fPnJTPw% zGz)~?B4}u0CzOf@l^um}HZzbaIwPmb<)< zi_3@E9lc)Qe2_`*Z^HH;1CXOceL=CHpHS{HySy3T%<^NrWQ}G0i4e1xm_K3(+~oi$ zoHl9wzb?Z4j#90DtURtjtgvi7uw8DzHYmtPb;?%8vb9n@bszT=1qr)V_>R%s!92_` zfnHQPANx z<#hIjIMm#*(v*!OXtF+w8kLu`o?VZ5k7{`vw{Yc^qYclpUGIM_PBN1+c{#Vxv&E*@ zxg=W2W~JuV{IuRYw3>LSI1)a!thID@R=bU+cU@DbR^_SXY`MC7HOsCN z!dO4OKV7(E_Z8T#8MA1H`99?Z!r0)qKW_#|29X3#Jb+5+>qUidbeP1NJ@)(qi2S-X zao|f0_tl(O+$R|Qwd$H{_ig|~I1fbp_$NkI!0E;Y z6JrnU{1Ra6^on{9gUUB0mwzP3S%B#h0fjo>JvV~#+X0P~JV=IG=yHG$O+p5O3NUgG zEQ}z6BTp^Fie)Sg<){Z&I8NwPR(=mO4joTLHkJ>|Tnk23E(Bo`FSbPc05lF2-+)X? z6vV3*m~IBHTy*^E!<0nA(tCOJW2G4DsH7)BxLV8kICn5lu6@U*R`w)o9;Ro$i8=Q^V%uH8n3q=+Yf;SFRZu z!+F&PKcH#8cG?aSK_Tl@K9P#8o+jry@gdexz&d(Q=47<7nw@e@FFfIRNL9^)1i@;A z28+$Z#rjv-wj#heI|<&J_DiJ*s}xd-f!{J8jfqOHE`TiHHZVIA8CjkNQ_u;Ery^^t zl1I75&u^`1_q)crO+JT4rx|z2ToSC>)Or@-D zy3S>jW*sNIZR-EBsfyaJ+Jq4BQE4?SePtD2+jY8*%FsSLZ9MY>+wk?}}}AFAw)vr{ml)8LUG-y9>^t!{~|sgpxYc0Gnkg`&~R z-pilJZjr@y5$>B=VMdZ73svct%##v%wdX~9fz6i3Q-zOKJ9wso+h?VME7}SjL=!NUG{J?M&i!>ma`eoEa@IX`5G>B1(7;%}M*%-# zfhJ(W{y;>MRz!Ic8=S}VaBKqh;~7KdnGEHxcL$kA-6E~=!hrN*zw9N+_=odt<$_H_8dbo;0=42wcAETPCVGUr~v(`Uai zb{=D!Qc!dOEU6v)2eHSZq%5iqK?B(JlCq%T6av$Cb4Rko6onlG&?CqaX7Y_C_cOC3 zYZ;_oI(}=>_07}Oep&Ws7x7-R)cc8zfe!SYxJYP``pi$FDS)4Fvw5HH=FiU6xfVqIM!hJ;Rx8c0cB7~aPtNH(Nmm5Vh{ibAoU#J6 zImRCr?(iyu_4W_6AWo3*vxTPUw@vPwy@E0`(>1Qi=%>5eSIrp^`` zK*Y?fK_6F1W>-7UsB)RPC4>>Ps9)f+^MqM}8AUm@tZ->j%&h1M8s*s!LX5&WxQcAh z8mciQej@RPm?660%>{_D+7er>%zX_{s|$Z+;G7_sfNfBgY(zLB4Ey}J9F>zX#K0f6 z?dVNIeEh?EIShmP6>M+d|0wMM85Sa4diw1hrg|ITJ}JDg@o8y>(rF9mXk5M z2@D|NA)-7>wD&wF;S_$KS=eE84`BGw3g0?6wGxu8ys4rwI?9U=*^VF22t3%mbGeOh z`!O-OpF7#Vceu~F`${bW0nYVU9ecmk31V{tF%iv&5hWofC>I~cqAt@u6|R+|HLMMX zVxuSlMFOK_EQ86#E8&KwxIr8S9tj_goWtLv4f@!&h8;Ov41{J~496vp9vX=(LK#j! zAwi*21RAV-LD>9Cw3bV_9X(X3)Kr0-UaB*7Y>t82EQ%!)(&(XuAYtTsYy-dz+w=$ir)VJpe!_$ z6SGpX^i(af3{o=VlFPC);|J8#(=_8#vdxDe|Cok+ANhYwbE*FO`Su2m1~w+&9<_9~ z-|tTU_ACGN`~CNW5WYYBn^B#SwZ(t4%3aPp z;o)|L6Rk569KGxFLUPx@!6OOa+5OjQLK5w&nAmwxkC5rZ|m&HT8G%GVZxB_@ME z>>{rnXUqyiJrT(8GMj_ap#yN_!9-lO5e8mR3cJiK3NE{_UM&=*vIU`YkiL$1%kf+1 z4=jk@7EEj`u(jy$HnzE33ZVW_J4bj}K;vT?T91YlO(|Y0FU4r+VdbmQ97%(J5 zkK*Bed8+C}FcZ@HIgdCMioV%A<*4pw_n}l*{Cr4}a(lq|injK#O?$tyvyE`S%(1`H z_wwRvk#13ElkZvij2MFGOj`fhy?nC^8`Zyo%yVcUAfEr8x&J#A{|moUBAV_^f$hpaUuyQeY3da^ zS9iRgf87YBwfe}>BO+T&Fl%rfpZh#+AM?Dq-k$Bq`vG6G_b4z%Kbd&v>qFjow*mBl z-OylnqOpLg}or7_VNwRg2za3VBK6FUfFX{|TD z`Wt0Vm2H$vdlRWYQJqDmM?JUbVqL*ZQY|5&sY*?!&%P8qhA~5+Af<{MaGo(dl&C5t zE%t!J0 zh6jqANt4ABdPxSTrVV}fLsRQal*)l&_*rFq(Ez}ClEH6LHv{J#v?+H-BZ2)Wy{K@9 z+ovXHq~DiDvm>O~r$LJo!cOuwL+Oa--6;UFE2q@g3N8Qkw5E>ytz^(&($!O47+i~$ zKM+tkAd-RbmP{s_rh+ugTD;lriL~`Xwkad#;_aM?nQ7L_muEFI}U_4$phjvYgleK~`Fo`;GiC07&Hq1F<%p;9Q;tv5b?*QnR%8DYJH3P>Svmv47Y>*LPZJy8_{9H`g6kQpyZU{oJ`m%&p~D=K#KpfoJ@ zn-3cqmHsdtN!f?~w+(t+I`*7GQA#EQC^lUA9(i6=i1PqSAc|ha91I%X&nXzjYaM{8$s&wEx@aVkQ6M{E2 zfzId#&r(XwUNtPcq4Ngze^+XaJA1EK-%&C9j>^9(secqe{}z>hR5CFNveMsVA)m#S zk)_%SidkY-XmMWlVnQ(mNJ>)ooszQ#vaK;!rPmGKXV7am^_F!Lz>;~{VrIO$;!#30XRhE1QqO_~#+Ux;B_D{Nk=grn z8Y0oR^4RqtcYM)7a%@B(XdbZCOqnX#fD{BQTeLvRHd(irHKq=4*jq34`6@VAQR8WG z^%)@5CXnD_T#f%@-l${>y$tfb>2LPmc{~5A82|16mH)R?&r#KKLs7xpN-D`=&Cm^R zvMA6#Ahr<3X>Q7|-qfTY)}32HkAz$_mibYV!I)u>bmjK`qwBe(>za^0Kt*HnFbSdO z1>+ryKCNxmm^)*$XfiDOF2|{-v3KKB?&!(S_Y=Ht@|ir^hLd978xuI&N{k>?(*f8H z=ClxVJK_%_z1TH0eUwm2J+2To7FK4o+n_na)&#VLn1m;!+CX+~WC+qg1?PA~KdOlC zW)C@pw75_xoe=w7i|r9KGIvQ$+3K?L{7TGHwrQM{dCp=Z*D}3kX7E-@sZnup!BImw z*T#a=+WcTwL78exTgBn|iNE3#EsOorO z*kt)gDzHiPt07fmisA2LWN?AymkdqTgr?=loT7z@d`wnlr6oN}@o|&JX!yPzC*Y8d zu6kWlTzE1)ckyBn+0Y^HMN+GA$wUO_LN6W>mxCo!0?oiQvT`z$jbSEu&{UHRU0E8# z%B^wOc@S!yhMT49Y)ww(Xta^8pmPCe@eI5C*ed96)AX9<>))nKx0(sci8gwob_1}4 z0DIL&vsJ1_s%<@y%U*-eX z5rN&(zef-5G~?@r79oZGW1d!WaTqQn0F6RIOa9tJ=0(kdd{d1{<*tHT#cCvl*i>YY zH+L7jq8xZNcTUBqj(S)ztTU!TM!RQ}In*n&Gn<>(60G7}4%WQL!o>hbJqNDSGwl#H z`4k+twp0cj%PsS+NKaxslAEu9!#U3xT1|_KB6`h=PI0SW`P9GTa7caD1}vKEglV8# zjKZR`pluCW19c2fM&ZG)c3T3Um;ir3y(tSCJ7Agl6|b524dy5El{^EQBG?E61H0XY z`bqg!;zhGhyMFl&(o=JWEJ8n~z)xI}A@C0d2hQGvw7nGv)?POU@(kS1m=%`|+^ika zXl8zjS?xqW$WlO?Ewa;vF~XbybHBor$f<%I&*t$F5fynwZlTGj|IjZtVfGa7l&tK} zW>I<69w(cZLu)QIVG|M2xzW@S+70NinQzk&Y0+3WT*cC)rx~04O-^<{JohU_&HL5XdUKW!uFy|i$FB|EMu0eUyW;gsf`XfIc!Z0V zeK&*hPL}f_cX=@iv>K%S5kL;cl_$v?n(Q9f_cChk8Lq$glT|=e+T*8O4H2n<=NGmn z+2*h+v;kBvF>}&0RDS>)B{1!_*XuE8A$Y=G8w^qGMtfudDBsD5>T5SB;Qo}fSkkiV ze^K^M(UthkwrD!&*tTsu>Dacdj_q`~V%r_twr$(Ct&_dKeeXE?fA&4&yASJWJ*}~- zel=@W)tusynfC_YqH4ll>4Eg`Xjs5F7Tj>tTLz<0N3)X<1px_d2yUY>X~y>>93*$) z5PuNMQLf9Bu?AAGO~a_|J2akO1M*@VYN^VxvP0F$2>;Zb9;d5Yfd8P%oFCCoZE$ z4#N$^J8rxYjUE_6{T%Y>MmWfHgScpuGv59#4u6fpTF%~KB^Ae`t1TD_^Ud#DhL+Dm zbY^VAM#MrAmFj{3-BpVSWph2b_Y6gCnCAombVa|1S@DU)2r9W<> zT5L8BB^er3zxKt1v(y&OYk!^aoQisqU zH(g@_o)D~BufUXcPt!Ydom)e|aW{XiMnes2z&rE?og>7|G+tp7&^;q?Qz5S5^yd$i z8lWr4g5nctBHtigX%0%XzIAB8U|T6&JsC4&^hZBw^*aIcuNO47de?|pGXJ4t}BB`L^d8tD`H`i zqrP8?#J@8T#;{^B!KO6J=@OWKhAerih(phML`(Rg7N1XWf1TN>=Z3Do{l_!d~DND&)O)D>ta20}@Lt77qSnVsA7>)uZAaT9bsB>u&aUQl+7GiY2|dAEg@%Al3i316y;&IhQL^8fw_nwS>f60M_-m+!5)S_6EPM7Y)(Nq^8gL7(3 zOiot`6Wy6%vw~a_H?1hLVzIT^i1;HedHgW9-P#)}Y6vF%C=P70X0Tk^z9Te@kPILI z_(gk!k+0%CG)%!WnBjjw*kAKs_lf#=5HXC00s-}oM-Q1aXYLj)(1d!_a7 z*Gg4Fe6F$*ujVjI|79Z5+Pr`us%zW@ln++2l+0hsngv<{mJ%?OfSo_3HJXOCys{Ug z00*YR-(fv<=&%Q!j%b-_ppA$JsTm^_L4x`$k{VpfLI(FMCap%LFAyq;#ns5bR7V+x zO!o;c5y~DyBPqdVQX)8G^G&jWkBy2|oWTw>)?5u}SAsI$RjT#)lTV&Rf8;>u*qXnb z8F%Xb=7#$m)83z%`E;49)t3fHInhtc#kx4wSLLms!*~Z$V?bTyUGiS&m>1P(952(H zuHdv=;o*{;5#X-uAyon`hP}d#U{uDlV?W?_5UjJvf%11hKwe&(&9_~{W)*y1nR5f_ z!N(R74nNK`y8>B!0Bt_Vr!;nc3W>~RiKtGSBkNlsR#-t^&;$W#)f9tTlZz>n*+Fjz z3zXZ;jf(sTM(oDzJt4FJS*8c&;PLTW(IQDFs_5QPy+7yhi1syPCarvqrHFcf&yTy)^O<1EBx;Ir`5W{TIM>{8w&PB>ro4;YD<5LF^TjTb0!zAP|QijA+1Vg>{Afv^% zmrkc4o6rvBI;Q8rj4*=AZacy*n8B{&G3VJc)so4$XUoie0)vr;qzPZVbb<#Fc=j+8CGBWe$n|3K& z_@%?{l|TzKSlUEO{U{{%Fz_pVDxs7i9H#bnbCw7@4DR=}r_qV!Zo~CvD4ZI*+j3kO zW6_=|S`)(*gM0Z;;}nj`73OigF4p6_NPZQ-Od~e$c_);;4-7sR>+2u$6m$Gf%T{aq zle>e3(*Rt(TPD}03n5)!Ca8Pu!V}m6v0o1;5<1h$*|7z|^(3$Y&;KHKTT}hV056wuF0Xo@mK-52~r=6^SI1NC%c~CC?n>yX6wPTgiWYVz!Sx^atLby9YNn1Rk{g?|pJaxD4|9cUf|V1_I*w zzxK)hRh9%zOl=*$?XUjly5z8?jPMy%vEN)f%T*|WO|bp5NWv@B(K3D6LMl!-6dQg0 zXNE&O>Oyf%K@`ngCvbGPR>HRg5!1IV$_}m@3dWB7x3t&KFyOJn9pxRXCAzFr&%37wXG;z^xaO$ekR=LJG ztIHpY8F5xBP{mtQidqNRoz= z@){+N3(VO5bD+VrmS^YjG@+JO{EOIW)9=F4v_$Ed8rZtHvjpiEp{r^c4F6Ic#ChlC zJX^DtSK+v(YdCW)^EFcs=XP7S>Y!4=xgmv>{S$~@h=xW-G4FF9?I@zYN$e5oF9g$# zb!eVU#J+NjLyX;yb)%SY)xJdvGhsnE*JEkuOVo^k5PyS=o#vq!KD46UTW_%R=Y&0G zFj6bV{`Y6)YoKgqnir2&+sl+i6foAn-**Zd1{_;Zb7Ki=u394C5J{l^H@XN`_6XTKY%X1AgQM6KycJ+= zYO=&t#5oSKB^pYhNdzPgH~aEGW2=ec1O#s-KG z71}LOg@4UEFtp3GY1PBemXpNs6UK-ax*)#$J^pC_me;Z$Je(OqLoh|ZrW*mAMBFn< zHttjwC&fkVfMnQeen8`Rvy^$pNRFVaiEN4Pih*Y3@jo!T0nsClN)pdrr9AYLcZxZ| zJ5Wlj+4q~($hbtuY zVQ7hl>4-+@6g1i`1a)rvtp-;b0>^`Dloy(#{z~ytgv=j4q^Kl}wD>K_Y!l~ zp(_&7sh`vfO(1*MO!B%<6E_bx1)&s+Ae`O)a|X=J9y~XDa@UB`m)`tSG4AUhoM=5& znWoHlA-(z@3n0=l{E)R-p8sB9XkV zZ#D8wietfHL?J5X0%&fGg@MH~(rNS2`GHS4xTo7L$>TPme+Is~!|79=^}QbPF>m%J zFMkGzSndiPO|E~hrhCeo@&Ea{M(ieIgRWMf)E}qeTxT8Q#g-!Lu*x$v8W^M^>?-g= zwMJ$dThI|~M06rG$Sv@C@tWR>_YgaG&!BAbkGggVQa#KdtDB)lMLNVLN|51C@F^y8 zCRvMB^{GO@j=cHfmy}_pCGbP%xb{pNN>? z?7tBz$1^zVaP|uaatYaIN+#xEN4jBzwZ|YI_)p(4CUAz1ZEbDk>J~Y|63SZaak~#0 zoYKruYsWHoOlC1(MhTnsdUOwQfz5p6-D0}4;DO$B;7#M{3lSE^jnTT;ns`>!G%i*F?@pR1JO{QTuD0U+~SlZxcc8~>IB{)@8p`P&+nDxNj`*gh|u?yrv$phpQcW)Us)bi`kT%qLj(fi{dWRZ%Es2!=3mI~UxiW0$-v3vUl?#g{p6eF zMEUAqo5-L0Ar(s{VlR9g=j7+lt!gP!UN2ICMokAZ5(Agd>})#gkA2w|5+<%-CuEP# zqgcM}u@3(QIC^Gx<2dbLj?cFSws_f3e%f4jeR?4M^M3cx1f+Qr6ydQ>n)kz1s##2w zk}UyQc+Z5G-d-1}{WzjkLXgS-2P7auWSJ%pSnD|Uivj5u!xk0 z_^-N9r9o;(rFDt~q1PvE#iJZ_f>J3gcP$)SOqhE~pD2|$=GvpL^d!r z6u=sp-CrMoF7;)}Zd7XO4XihC4ji?>V&(t^?@3Q&t9Mx=qex6C9d%{FE6dvU6%d94 zIE;hJ1J)cCqjv?F``7I*6bc#X)JW2b4f$L^>j{*$R`%5VHFi*+Q$2;nyieduE}qdS{L8y8F08yLs?w}{>8>$3236T-VMh@B zq-nujsb_1aUv_7g#)*rf9h%sFj*^mIcImRV*k~Vmw;%;YH(&ylYpy!&UjUVqqtfG` zox3esju?`unJJA_zKXRJP)rA3nXc$m^{S&-p|v|-0x9LHJm;XIww7C#R$?00l&Yyj z=e}gKUOpsImwW?N)+E(awoF@HyP^EhL+GlNB#k?R<2>95hz!h9sF@U20DHSB3~WMa zk90+858r@-+vWwkawJ)8ougd(i#1m3GLN{iSTylYz$brAsP%=&m$mQQrH$g%3-^VR zE%B`Vi&m8f3T~&myTEK28BDWCVzfWir1I?03;pX))|kY5ClO^+bae z*7E?g=3g7EiisYOrE+lA)2?Ln6q2*HLNpZEWMB|O-JI_oaHZB%CvYB(%=tU= zE*OY%QY58fW#RG5=gm0NR#iMB=EuNF@)%oZJ}nmm=tsJ?eGjia{e{yuU0l3{d^D@)kVDt=1PE)&tf_hHC%0MB znL|CRCPC}SeuVTdf>-QV70`0(EHizc21s^sU>y%hW0t!0&y<7}Wi-wGy>m%(-jsDj zP?mF|>p_K>liZ6ZP(w5(|9Ga%>tLgb$|doDDfkdW>Z z`)>V2XC?NJT26mL^@ zf+IKr27TfM!UbZ@?zRddC7#6ss1sw%CXJ4FWC+t3lHZupzM77m^=9 z&(a?-LxIq}*nvv)y?27lZ{j zifdl9hyJudyP2LpU$-kXctshbJDKS{WfulP5Dk~xU4Le4c#h^(YjJit4#R8_khheS z|8(>2ibaHES4+J|DBM7I#QF5u-*EdN{n=Kt@4Zt?@Tv{JZA{`4 zU#kYOv{#A&gGPwT+$Ud}AXlK3K7hYzo$(fBSFjrP{QQ zeaKg--L&jh$9N}`pu{Bs>?eDFPaWY4|9|foN%}i;3%;@4{dc+iw>m}{3rELqH21G! z`8@;w-zsJ1H(N3%|1B@#ioLOjib)j`EiJqPQVSbPSPVHCj6t5J&(NcWzBrzCiDt{4 zdlPAUKldz%6x5II1H_+jv)(xVL+a;P+-1hv_pM>gMRr%04@k;DTokASSKKhU1Qms| zrWh3a!b(J3n0>-tipg{a?UaKsP7?+|@A+1WPDiQIW1Sf@qDU~M_P65_s}7(gjTn0X zucyEm)o;f8UyshMy&>^SC3I|C6jR*R_GFwGranWZe*I>K+0k}pBuET&M~ z;Odo*ZcT?ZpduHyrf8E%IBFtv;JQ!N_m>!sV6ly$_1D{(&nO~w)G~Y`7sD3#hQk%^ zp}ucDF_$!6DAz*PM8yE(&~;%|=+h(Rn-=1Wykas_-@d&z#=S}rDf`4w(rVlcF&lF! z=1)M3YVz7orwk^BXhslJ8jR);sh^knJW(Qmm(QdSgIAIdlN4Te5KJisifjr?eB{FjAX1a0AB>d?qY4Wx>BZ8&}5K0fA+d{l8 z?^s&l8#j7pR&ijD?0b%;lL9l$P_mi2^*_OL+b}4kuLR$GAf85sOo02?Y#90}CCDiS zZ%rbCw>=H~CBO=C_JVV=xgDe%b4FaEFtuS7Q1##y686r%F6I)s-~2(}PWK|Z8M+Gu zl$y~5@#0Ka%$M<&Cv%L`a8X^@tY&T7<0|(6dNT=EsRe0%kp1Qyq!^43VAKYnr*A5~ zsI%lK1ewqO;0TpLrT9v}!@vJK{QoVa_+N4FYT#h?Y8rS1S&-G+m$FNMP?(8N`MZP zels(*?kK{{^g9DOzkuZXJ2;SrOQsp9T$hwRB1(phw1c7`!Q!by?Q#YsSM#I12RhU{$Q+{xj83axHcftEc$mNJ8_T7A-BQc*k(sZ+~NsO~xAA zxnbb%dam_fZlHvW7fKXrB~F&jS<4FD2FqY?VG?ix*r~MDXCE^WQ|W|WM;gsIA4lQP zJ2hAK@CF*3*VqPr2eeg6GzWFlICi8S>nO>5HvWzyZTE)hlkdC_>pBej*>o0EOHR|) z$?};&I4+_?wvL*g#PJ9)!bc#9BJu1(*RdNEn>#Oxta(VWeM40ola<0aOe2kSS~{^P zDJBd}0L-P#O-CzX*%+$#v;(x%<*SPgAje=F{Zh-@ucd2DA(yC|N_|ocs*|-!H%wEw z@Q!>siv2W;C^^j^59OAX03&}&D*W4EjCvfi(ygcL#~t8XGa#|NPO+*M@Y-)ctFA@I z-p7npT1#5zOLo>7q?aZpCZ=iecn3QYklP;gF0bq@>oyBq94f6C=;Csw3PkZ|5q=(c zfs`aw?II0e(h=|7o&T+hq&m$; zBrE09Twxd9BJ2P+QPN}*OdZ-JZV7%av@OM7v!!NL8R;%WFq*?{9T3{ct@2EKgc8h) zMxoM$SaF#p<`65BwIDfmXG6+OiK0e)`I=!A3E`+K@61f}0e z!2a*FOaDrOe>U`q%K!QN`&=&0C~)CaL3R4VY(NDt{Xz(Xpqru5=r#uQN1L$Je1*dkdqQ*=lofQaN%lO!<5z9ZlHgxt|`THd>2 zsWfU$9=p;yLyJyM^t zS2w9w?Bpto`@H^xJpZDKR1@~^30Il6oFGfk5%g6w*C+VM)+%R@gfIwNprOV5{F^M2 zO?n3DEzpT+EoSV-%OdvZvNF+pDd-ZVZ&d8 zKeIyrrfPN=EcFRCPEDCVflX#3-)Ik_HCkL(ejmY8vzcf-MTA{oHk!R2*36`O68$7J zf}zJC+bbQk--9Xm!u#lgLvx8TXx2J258E5^*IZ(FXMpq$2LUUvhWQPs((z1+2{Op% z?J}9k5^N=z;7ja~zi8a_-exIqWUBJwohe#4QJ`|FF*$C{lM18z^#hX6!5B8KAkLUX ziP=oti-gpV(BsLD{0(3*dw}4JxK23Y7M{BeFPucw!sHpY&l%Ws4pSm`+~V7;bZ%Dx zeI)MK=4vC&5#;2MT7fS?^ch9?2;%<8Jlu-IB&N~gg8t;6S-#C@!NU{`p7M8@2iGc& zg|JPg%@gCoCQ&s6JvDU&`X2S<57f(k8nJ1wvBu{8r?;q3_kpZZ${?|( z+^)UvR33sjSd)aT!UPkA;ylO6{aE3MQa{g%Mcf$1KONcjO@&g5zPHWtzM1rYC{_K> zgQNcs<{&X{OA=cEWw5JGqpr0O>x*Tfak2PE9?FuWtz^DDNI}rwAaT0(bdo-<+SJ6A z&}S%boGMWIS0L}=S>|-#kRX;e^sUsotry(MjE|3_9duvfc|nwF#NHuM-w7ZU!5ei8 z6Mkf>2)WunY2eU@C-Uj-A zG(z0Tz2YoBk>zCz_9-)4a>T46$(~kF+Y{#sA9MWH%5z#zNoz)sdXq7ZR_+`RZ%0(q zC7&GyS_|BGHNFl8Xa%@>iWh%Gr?=J5<(!OEjauj5jyrA-QXBjn0OAhJJ9+v=!LK`` z@g(`^*84Q4jcDL`OA&ZV60djgwG`|bcD*i50O}Q{9_noRg|~?dj%VtKOnyRs$Uzqg z191aWoR^rDX#@iSq0n z?9Sg$WSRPqSeI<}&n1T3!6%Wj@5iw5`*`Btni~G=&;J+4`7g#OQTa>u`{4ZZ(c@s$ zK0y;ySOGD-UTjREKbru{QaS>HjN<2)R%Nn-TZiQ(Twe4p@-saNa3~p{?^V9Nixz@a zykPv~<@lu6-Ng9i$Lrk(xi2Tri3q=RW`BJYOPC;S0Yly%77c727Yj-d1vF!Fuk{Xh z)lMbA69y7*5ufET>P*gXQrxsW+ zz)*MbHZv*eJPEXYE<6g6_M7N%#%mR{#awV3i^PafNv(zyI)&bH?F}2s8_rR(6%!V4SOWlup`TKAb@ee>!9JKPM=&8g#BeYRH9FpFybxBXQI2|g}FGJfJ+ zY-*2hB?o{TVL;Wt_ek;AP5PBqfDR4@Z->_182W z{P@Mc27j6jE*9xG{R$>6_;i=y{qf(c`5w9fa*`rEzX6t!KJ(p1H|>J1pC-2zqWENF zmm=Z5B4u{cY2XYl(PfrInB*~WGWik3@1oRhiMOS|D;acnf-Bs(QCm#wR;@Vf!hOPJ zgjhDCfDj$HcyVLJ=AaTbQ{@vIv14LWWF$=i-BDoC11}V;2V8A`S>_x)vIq44-VB-v z*w-d}$G+Ql?En8j!~ZkCpQ$|cA0|+rrY>tiCeWxkRGPoarxlGU2?7%k#F693RHT24 z-?JsiXlT2PTqZqNb&sSc>$d;O4V@|b6VKSWQb~bUaWn1Cf0+K%`Q&Wc<>mQ>*iEGB zbZ;aYOotBZ{vH3y<0A*L0QVM|#rf*LIsGx(O*-7)r@yyBIzJnBFSKBUSl1e|8lxU* zzFL+YDVVkIuzFWeJ8AbgN&w(4-7zbiaMn{5!JQXu)SELk*CNL+Fro|2v|YO)1l15t zs(0^&EB6DPMyaqvY>=KL>)tEpsn;N5Q#yJj<9}ImL((SqErWN3Q=;tBO~ExTCs9hB z2E$7eN#5wX4<3m^5pdjm#5o>s#eS_Q^P)tm$@SawTqF*1dj_i#)3};JslbLKHXl_N z)Fxzf>FN)EK&Rz&*|6&%Hs-^f{V|+_vL1S;-1K-l$5xiC@}%uDuwHYhmsV?YcOUlk zOYkG5v2+`+UWqpn0aaaqrD3lYdh0*!L`3FAsNKu=Q!vJu?Yc8n|CoYyDo_`r0mPoo z8>XCo$W4>l(==h?2~PoRR*kEe)&IH{1sM41mO#-36`02m#nTX{r*r`Q5rZ2-sE|nA zhnn5T#s#v`52T5|?GNS`%HgS2;R(*|^egNPDzzH_z^W)-Q98~$#YAe)cEZ%vge965AS_am#DK#pjPRr-!^za8>`kksCAUj(Xr*1NW5~e zpypt_eJpD&4_bl_y?G%>^L}=>xAaV>KR6;^aBytqpiHe%!j;&MzI_>Sx7O%F%D*8s zSN}cS^<{iiK)=Ji`FpO#^zY!_|D)qeRNAtgmH)m;qC|mq^j(|hL`7uBz+ULUj37gj zksdbnU+LSVo35riSX_4z{UX=%n&}7s0{WuZYoSfwAP`8aKN9P@%e=~1`~1ASL-z%# zw>DO&ixr}c9%4InGc*_y42bdEk)ZdG7-mTu0bD@_vGAr*NcFoMW;@r?@LUhRI zCUJgHb`O?M3!w)|CPu~ej%fddw20lod?Ufp8Dmt0PbnA0J%KE^2~AIcnKP()025V> zG>noSM3$5Btmc$GZoyP^v1@Poz0FD(6YSTH@aD0}BXva?LphAiSz9f&Y(aDAzBnUh z?d2m``~{z;{}kZJ>a^wYI?ry(V9hIoh;|EFc0*-#*`$T0DRQ1;WsqInG;YPS+I4{g zJGpKk%%Sdc5xBa$Q^_I~(F97eqDO7AN3EN0u)PNBAb+n+ zWBTxQx^;O9o0`=g+Zrt_{lP!sgWZHW?8bLYS$;1a@&7w9rD9|Ge;Gb?sEjFoF9-6v z#!2)t{DMHZ2@0W*fCx;62d#;jouz`R5Y(t{BT=$N4yr^^o$ON8d{PQ=!O zX17^CrdM~7D-;ZrC!||<+FEOxI_WI3CA<35va%4v>gc zEX-@h8esj=a4szW7x{0g$hwoWRQG$yK{@3mqd-jYiVofJE!Wok1* znV7Gm&Ssq#hFuvj1sRyHg(6PFA5U*Q8Rx>-blOs=lb`qa{zFy&n4xY;sd$fE+<3EI z##W$P9M{B3c3Si9gw^jlPU-JqD~Cye;wr=XkV7BSv#6}DrsXWFJ3eUNrc%7{=^sP> zrp)BWKA9<}^R9g!0q7yWlh;gr_TEOD|#BmGq<@IV;ueg+D2}cjpp+dPf&Q(36sFU&K8}hA85U61faW&{ zlB`9HUl-WWCG|<1XANN3JVAkRYvr5U4q6;!G*MTdSUt*Mi=z_y3B1A9j-@aK{lNvx zK%p23>M&=KTCgR!Ee8c?DAO2_R?B zkaqr6^BSP!8dHXxj%N1l+V$_%vzHjqvu7p@%Nl6;>y*S}M!B=pz=aqUV#`;h%M0rU zHfcog>kv3UZAEB*g7Er@t6CF8kHDmKTjO@rejA^ULqn!`LwrEwOVmHx^;g|5PHm#B zZ+jjWgjJ!043F+&#_;D*mz%Q60=L9Ove|$gU&~As5^uz@2-BfQ!bW)Khn}G+Wyjw- z19qI#oB(RSNydn0t~;tAmK!P-d{b-@@E5|cdgOS#!>%#Rj6ynkMvaW@37E>@hJP^8 z2zk8VXx|>#R^JCcWdBCy{0nPmYFOxN55#^-rlqobe0#L6)bi?E?SPymF*a5oDDeSd zO0gx?#KMoOd&G(2O@*W)HgX6y_aa6iMCl^~`{@UR`nMQE`>n_{_aY5nA}vqU8mt8H z`oa=g0SyiLd~BxAj2~l$zRSDHxvDs;I4>+M$W`HbJ|g&P+$!U7-PHX4RAcR0szJ*( ze-417=bO2q{492SWrqDK+L3#ChUHtz*@MP)e^%@>_&#Yk^1|tv@j4%3T)diEX zATx4K*hcO`sY$jk#jN5WD<=C3nvuVsRh||qDHnc~;Kf59zr0;c7VkVSUPD%NnnJC_ zl3F^#f_rDu8l}l8qcAz0FFa)EAt32IUy_JLIhU_J^l~FRH&6-ivSpG2PRqzDdMWft>Zc(c)#tb%wgmWN%>IOPm zZi-noqS!^Ftb81pRcQi`X#UhWK70hy4tGW1mz|+vI8c*h@ zfFGJtW3r>qV>1Z0r|L>7I3un^gcep$AAWfZHRvB|E*kktY$qQP_$YG60C@X~tTQjB3%@`uz!qxtxF+LE!+=nrS^07hn` zEgAp!h|r03h7B!$#OZW#ACD+M;-5J!W+{h|6I;5cNnE(Y863%1(oH}_FTW})8zYb$7czP zg~Szk1+_NTm6SJ0MS_|oSz%e(S~P-&SFp;!k?uFayytV$8HPwuyELSXOs^27XvK-D zOx-Dl!P|28DK6iX>p#Yb%3`A&CG0X2S43FjN%IB}q(!hC$fG}yl1y9W&W&I@KTg6@ zK^kpH8=yFuP+vI^+59|3%Zqnb5lTDAykf z9S#X`3N(X^SpdMyWQGOQRjhiwlj!0W-yD<3aEj^&X%=?`6lCy~?`&WSWt z?U~EKFcCG_RJ(Qp7j=$I%H8t)Z@6VjA#>1f@EYiS8MRHZphp zMA_5`znM=pzUpBPO)pXGYpQ6gkine{6u_o!P@Q+NKJ}k!_X7u|qfpAyIJb$_#3@wJ z<1SE2Edkfk9C!0t%}8Yio09^F`YGzpaJHGk*-ffsn85@)%4@`;Fv^8q(-Wk7r=Q8p zT&hD`5(f?M{gfzGbbwh8(}G#|#fDuk7v1W)5H9wkorE0ZZjL0Q1=NRGY>zwgfm81DdoaVwNH;or{{eSyybt)m<=zXoA^RALYG-2t zouH|L*BLvmm9cdMmn+KGopyR@4*=&0&4g|FLoreZOhRmh=)R0bg~ zT2(8V_q7~42-zvb)+y959OAv!V$u(O3)%Es0M@CRFmG{5sovIq4%8Ahjk#*5w{+)+ zMWQoJI_r$HxL5km1#6(e@{lK3Udc~n0@g`g$s?VrnQJ$!oPnb?IHh-1qA`Rz$)Ai< z6w$-MJW-gKNvOhL+XMbE7&mFt`x1KY>k4(!KbbpZ`>`K@1J<(#vVbjx@Z@(6Q}MF# zMnbr-f55(cTa^q4+#)=s+ThMaV~E`B8V=|W_fZWDwiso8tNMTNse)RNBGi=gVwgg% zbOg8>mbRN%7^Um-7oj4=6`$|(K7!+t^90a{$18Z>}<#!bm%ZEFQ{X(yBZMc>lCz0f1I2w9Sq zuGh<9<=AO&g6BZte6hn>Qmvv;Rt)*cJfTr2=~EnGD8P$v3R|&1RCl&7)b+`=QGapi zPbLg_pxm`+HZurtFZ;wZ=`Vk*do~$wB zxoW&=j0OTbQ=Q%S8XJ%~qoa3Ea|au5o}_(P;=!y-AjFrERh%8la!z6Fn@lR?^E~H12D?8#ht=1F;7@o4$Q8GDj;sSC%Jfn01xgL&%F2 zwG1|5ikb^qHv&9hT8w83+yv&BQXOQyMVJSBL(Ky~p)gU3#%|blG?IR9rP^zUbs7rOA0X52Ao=GRt@C&zlyjNLv-} z9?*x{y(`509qhCV*B47f2hLrGl^<@SuRGR!KwHei?!CM10Tq*YDIoBNyRuO*>3FU? zHjipIE#B~y3FSfOsMfj~F9PNr*H?0oHyYB^G(YyNh{SxcE(Y-`x5jFMKb~HO*m+R% zrq|ic4fzJ#USpTm;X7K+E%xsT_3VHKe?*uc4-FsILUH;kL>_okY(w`VU*8+l>o>Jm ziU#?2^`>arnsl#)*R&nf_%>A+qwl%o{l(u)M?DK1^mf260_oteV3#E_>6Y4!_hhVD zM8AI6MM2V*^_M^sQ0dmHu11fy^kOqXqzpr?K$`}BKWG`=Es(9&S@K@)ZjA{lj3ea7_MBP zk(|hBFRjHVMN!sNUkrB;(cTP)T97M$0Dtc&UXSec<+q?y>5=)}S~{Z@ua;1xt@=T5 zI7{`Z=z_X*no8s>mY;>BvEXK%b`a6(DTS6t&b!vf_z#HM{Uoy_5fiB(zpkF{})ruka$iX*~pq1ZxD?q68dIo zIZSVls9kFGsTwvr4{T_LidcWtt$u{kJlW7moRaH6+A5hW&;;2O#$oKyEN8kx`LmG)Wfq4ykh+q{I3|RfVpkR&QH_x;t41Uw z`P+tft^E2B$domKT@|nNW`EHwyj>&}K;eDpe z1bNOh=fvIfk`&B61+S8ND<(KC%>y&?>opCnY*r5M+!UrWKxv0_QvTlJc>X#AaI^xo zaRXL}t5Ej_Z$y*|w*$6D+A?Lw-CO-$itm^{2Ct82-<0IW)0KMNvJHgBrdsIR0v~=H z?n6^}l{D``Me90`^o|q!olsF?UX3YSq^6Vu>Ijm>>PaZI8G@<^NGw{Cx&%|PwYrfw zR!gX_%AR=L3BFsf8LxI|K^J}deh0ZdV?$3r--FEX`#INxsOG6_=!v)DI>0q|BxT)z z-G6kzA01M?rba+G_mwNMQD1mbVbNTWmBi*{s_v_Ft9m2Avg!^78(QFu&n6mbRJ2bA zv!b;%yo{g*9l2)>tsZJOOp}U~8VUH`}$ z8p_}t*XIOehezolNa-a2x0BS})Y9}&*TPgua{Ewn-=wVrmJUeU39EKx+%w%=ixQWK zDLpwaNJs65#6o7Ln7~~X+p_o2BR1g~VCfxLzxA{HlWAI6^H;`juI=&r1jQrUv_q0Z z1Ja-tjdktrrP>GOC*#p?*xfQU5MqjMsBe!9lh(u8)w$e@Z|>aUHI5o;MGw*|Myiz3 z-f0;pHg~Q#%*Kx8MxH%AluVXjG2C$)WL-K63@Q`#y9_k_+}eR(x4~dp7oV-ek0H>I zgy8p#i4GN{>#v=pFYUQT(g&b$OeTy-X_#FDgNF8XyfGY6R!>inYn8IR2RDa&O!(6< znXs{W!bkP|s_YI*Yx%4stI`=ZO45IK6rBs`g7sP40ic}GZ58s?Mc$&i`kq_tfci>N zIHrC0H+Qpam1bNa=(`SRKjixBTtm&e`j9porEci!zdlg1RI0Jw#b(_Tb@RQK1Zxr_ z%7SUeH6=TrXt3J@js`4iDD0=IoHhK~I7^W8^Rcp~Yaf>2wVe|Hh1bUpX9ATD#moByY57-f2Ef1TP^lBi&p5_s7WGG9|0T}dlfxOx zXvScJO1Cnq`c`~{Dp;{;l<-KkCDE+pmexJkd}zCgE{eF=)K``-qC~IT6GcRog_)!X z?fK^F8UDz$(zFUrwuR$qro5>qqn>+Z%<5>;_*3pZ8QM|yv9CAtrAx;($>4l^_$_-L z*&?(77!-=zvnCVW&kUcZMb6;2!83si518Y%R*A3JZ8Is|kUCMu`!vxDgaWjs7^0j( ziTaS4HhQ)ldR=r)_7vYFUr%THE}cPF{0H45FJ5MQW^+W>P+eEX2kLp3zzFe*-pFVA zdDZRybv?H|>`9f$AKVjFWJ=wegO7hOOIYCtd?Vj{EYLT*^gl35|HQ`R=ti+ADm{jyQE7K@kdjuqJhWVSks>b^ zxha88-h3s;%3_5b1TqFCPTxVjvuB5U>v=HyZ$?JSk+&I%)M7KE*wOg<)1-Iy)8-K! z^XpIt|0ibmk9RtMmlUd7#Ap3Q!q9N4atQy)TmrhrFhfx1DAN`^vq@Q_SRl|V z#lU<~n67$mT)NvHh`%als+G-)x1`Y%4Bp*6Un5Ri9h=_Db zA-AdP!f>f0m@~>7X#uBM?diI@)Egjuz@jXKvm zJo+==juc9_<;CqeRaU9_Mz@;3e=E4=6TK+c`|uu#pIqhSyNm`G(X)&)B`8q0RBv#> z`gGlw(Q=1Xmf55VHj%C#^1lpc>LY8kfA@|rlC1EA<1#`iuyNO z(=;irt{_&K=i4)^x%;U(Xv<)+o=dczC5H3W~+e|f~{*ucxj@{Yi-cw^MqYr3fN zF5D+~!wd$#al?UfMnz(@K#wn`_5na@rRr8XqN@&M&FGEC@`+OEv}sI1hw>Up0qAWf zL#e4~&oM;TVfjRE+10B_gFlLEP9?Q-dARr3xi6nQqnw>k-S;~b z;!0s2VS4}W8b&pGuK=7im+t(`nz@FnT#VD|!)eQNp-W6)@>aA+j~K*H{$G`y2|QHY z|Hmy+CR@#jWY4~)lr1qBJB_RfHJFfP<}pK5(#ZZGSqcpyS&}01LnTWk5fzmXMGHkJ zTP6L^B+uj;lmB_W<~4=${+v0>z31M!-_O@o-O9GyW)j_mjx}!0@br_LE-7SIuPP84 z;5=O(U*g_um0tyG|61N@d9lEuOeiRd+#NY^{nd5;-CVlw&Ap7J?qwM^?E29wvS}2d zbzar4Fz&RSR(-|s!Z6+za&Z zY#D<5q_JUktIzvL0)yq_kLWG6DO{ri=?c!y!f(Dk%G{8)k`Gym%j#!OgXVDD3;$&v@qy#ISJfp=Vm>pls@9-mapVQChAHHd-x+OGx)(*Yr zC1qDUTZ6mM(b_hi!TuFF2k#8uI2;kD70AQ&di$L*4P*Y-@p`jdm%_c3f)XhYD^6M8&#Y$ZpzQMcR|6nsH>b=*R_Von!$BTRj7yGCXokoAQ z&ANvx0-Epw`QIEPgI(^cS2f(Y85yV@ygI{ewyv5Frng)e}KCZF7JbR(&W618_dcEh(#+^zZFY;o<815<5sOHQdeax9_!PyM&;{P zkBa5xymca0#)c#tke@3KNEM8a_mT&1gm;p&&JlMGH(cL(b)BckgMQ^9&vRwj!~3@l zY?L5}=Jzr080OGKb|y`ee(+`flQg|!lo6>=H)X4`$Gz~hLmu2a%kYW_Uu8x09Pa0J zKZ`E$BKJ=2GPj_3l*TEcZ*uYRr<*J^#5pILTT;k_cgto1ZL-%slyc16J~OH-(RgDA z%;EjEnoUkZ&acS{Q8`{i6T5^nywgqQI5bDIymoa7CSZG|WWVk>GM9)zy*bNih|QIm z%0+(Nnc*a_xo;$=!HQYaapLms>J1ToyjtFByY`C2H1wT#178#4+|{H0BBqtCdd$L% z_3Hc60j@{t9~MjM@LBalR&6@>B;9?r<7J~F+WXyYu*y3?px*=8MAK@EA+jRX8{CG?GI-< z54?Dc9CAh>QTAvyOEm0^+x;r2BWX|{3$Y7)L5l*qVE*y0`7J>l2wCmW zL1?|a`pJ-l{fb_N;R(Z9UMiSj6pQjOvQ^%DvhIJF!+Th7jO2~1f1N+(-TyCFYQZYw z4)>7caf^Ki_KJ^Zx2JUb z&$3zJy!*+rCV4%jqwyuNY3j1ZEiltS0xTzd+=itTb;IPYpaf?8Y+RSdVdpacB(bVQ zC(JupLfFp8y43%PMj2}T|VS@%LVp>hv4Y!RPMF?pp8U_$xCJ)S zQx!69>bphNTIb9yn*_yfj{N%bY)t{L1cs8<8|!f$;UQ*}IN=2<6lA;x^(`8t?;+ST zh)z4qeYYgZkIy{$4x28O-pugO&gauRh3;lti9)9Pvw+^)0!h~%m&8Q!AKX%urEMnl z?yEz?g#ODn$UM`+Q#$Q!6|zsq_`dLO5YK-6bJM6ya>}H+vnW^h?o$z;V&wvuM$dR& zeEq;uUUh$XR`TWeC$$c&Jjau2it3#%J-y}Qm>nW*s?En?R&6w@sDXMEr#8~$=b(gk zwDC3)NtAP;M2BW_lL^5ShpK$D%@|BnD{=!Tq)o(5@z3i7Z){} zGr}Exom_qDO{kAVkZ*MbLNHE666Kina#D{&>Jy%~w7yX$oj;cYCd^p9zy z8*+wgSEcj$4{WxKmCF(5o7U4jqwEvO&dm1H#7z}%VXAbW&W24v-tS6N3}qrm1OnE)fUkoE8yMMn9S$?IswS88tQWm4#Oid#ckgr6 zRtHm!mfNl-`d>O*1~d7%;~n+{Rph6BBy^95zqI{K((E!iFQ+h*C3EsbxNo_aRm5gj zKYug($r*Q#W9`p%Bf{bi6;IY0v`pB^^qu)gbg9QHQ7 zWBj(a1YSu)~2RK8Pi#C>{DMlrqFb9e_RehEHyI{n?e3vL_}L>kYJC z_ly$$)zFi*SFyNrnOt(B*7E$??s67EO%DgoZL2XNk8iVx~X_)o++4oaK1M|ou73vA0K^503j@uuVmLcHH4ya-kOIDfM%5%(E z+Xpt~#7y2!KB&)PoyCA+$~DXqxPxxALy!g-O?<9+9KTk4Pgq4AIdUkl`1<1#j^cJg zgU3`0hkHj_jxV>`Y~%LAZl^3o0}`Sm@iw7kwff{M%VwtN)|~!p{AsfA6vB5UolF~d zHWS%*uBDt<9y!9v2Xe|au&1j&iR1HXCdyCjxSgG*L{wmTD4(NQ=mFjpa~xooc6kju z`~+d{j7$h-;HAB04H!Zscu^hZffL#9!p$)9>sRI|Yovm)g@F>ZnosF2EgkU3ln0bR zTA}|+E(tt)!SG)-bEJi_0m{l+(cAz^pi}`9=~n?y&;2eG;d9{M6nj>BHGn(KA2n|O zt}$=FPq!j`p&kQ8>cirSzkU0c08%8{^Qyqi-w2LoO8)^E7;;I1;HQ6B$u0nNaX2CY zSmfi)F`m94zL8>#zu;8|{aBui@RzRKBlP1&mfFxEC@%cjl?NBs`cr^nm){>;$g?rhKr$AO&6qV_Wbn^}5tfFBry^e1`%du2~o zs$~dN;S_#%iwwA_QvmMjh%Qo?0?rR~6liyN5Xmej8(*V9ym*T`xAhHih-v$7U}8=dfXi2i*aAB!xM(Xekg*ix@r|ymDw*{*s0?dlVys2e)z62u1 z+k3esbJE=-P5S$&KdFp+2H7_2e=}OKDrf( z9-207?6$@f4m4B+9E*e((Y89!q?zH|mz_vM>kp*HGXldO0Hg#!EtFhRuOm$u8e~a9 z5(roy7m$Kh+zjW6@zw{&20u?1f2uP&boD}$#Zy)4o&T;vyBoqFiF2t;*g=|1=)PxB z8eM3Mp=l_obbc?I^xyLz?4Y1YDWPa+nm;O<$Cn;@ane616`J9OO2r=rZr{I_Kizyc zP#^^WCdIEp*()rRT+*YZK>V@^Zs=ht32x>Kwe zab)@ZEffz;VM4{XA6e421^h~`ji5r%)B{wZu#hD}f3$y@L0JV9f3g{-RK!A?vBUA}${YF(vO4)@`6f1 z-A|}e#LN{)(eXloDnX4Vs7eH|<@{r#LodP@Nz--$Dg_Par%DCpu2>2jUnqy~|J?eZ zBG4FVsz_A+ibdwv>mLp>P!(t}E>$JGaK$R~;fb{O3($y1ssQQo|5M;^JqC?7qe|hg zu0ZOqeFcp?qVn&Qu7FQJ4hcFi&|nR!*j)MF#b}QO^lN%5)4p*D^H+B){n8%VPUzi! zDihoGcP71a6!ab`l^hK&*dYrVYzJ0)#}xVrp!e;lI!+x+bfCN0KXwUAPU9@#l7@0& QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..d7e66b5c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..744e882e --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/integration/build.gradle b/integration/build.gradle new file mode 100644 index 00000000..394a48f2 --- /dev/null +++ b/integration/build.gradle @@ -0,0 +1,31 @@ +import org.springframework.boot.gradle.plugin.SpringBootPlugin + +plugins { + id 'bio.terra.java-application-conventions' + id 'io.spring.dependency-management' + id 'bio.terra.test-runner-plugin' +} + +dependencyManagement { + imports { + mavenBom(SpringBootPlugin.BOM_COORDINATES) + } +} + +dependencies { + implementation 'org.slf4j:slf4j-api' + implementation 'org.glassfish.jersey.inject:jersey-hk2' + implementation 'org.junit.jupiter:junit-jupiter-api' + implementation 'org.hamcrest:hamcrest' + + implementation 'org.glassfish.jersey.connectors:jersey-jdk-connector' + + // Google Dependencies + implementation 'com.google.auth:google-auth-library-oauth2-http:1.4.0' + + // Terra Test Runner Library + implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' + + // Requires client libraries + implementation project(':client') +} diff --git a/integration/src/main/java/scripts/client/JavatemplateClient.java b/integration/src/main/java/scripts/client/JavatemplateClient.java new file mode 100644 index 00000000..3c640156 --- /dev/null +++ b/integration/src/main/java/scripts/client/JavatemplateClient.java @@ -0,0 +1,48 @@ +package scripts.client; + +import bio.terra.javatemplate.client.ApiClient; +import bio.terra.testrunner.common.utils.AuthenticationUtils; +import bio.terra.testrunner.runner.config.ServerSpecification; +import bio.terra.testrunner.runner.config.TestUserSpecification; +import com.google.auth.oauth2.GoogleCredentials; +import java.io.IOException; +import java.util.Objects; + +public class JavatemplateClient extends ApiClient { + + /** + * Build a no-auth API client object for the service. No access token is needed for this API + * client. + * + * @param server the server we are testing against + */ + public JavatemplateClient(ServerSpecification server) throws IOException { + this(server, null); + } + + /** + * Build an API client object for the given test user for the service. The test user's token is + * always refreshed. If a test user isn't configured (e.g. when running locally), return an + * un-authenticated client. + * + * @param server the server we are testing against + * @param testUser the test user whose credentials are supplied to the API client object + */ + public JavatemplateClient(ServerSpecification server, TestUserSpecification testUser) + throws IOException { + // note that this uses server.catalogUri. Typically a uri for a new service needs to be added to + // https://github.com/DataBiosphere/terra-test-runner/blob/main/src/main/java/bio/terra/testrunner/runner/config/ServerSpecification.java + // but for this template we will stick with catalog + setBasePath(Objects.requireNonNull(server.catalogUri, "Catalog URI required")); + + if (testUser != null) { + GoogleCredentials userCredential = + AuthenticationUtils.getDelegatedUserCredential( + testUser, AuthenticationUtils.userLoginScopes); + var accessToken = AuthenticationUtils.getAccessToken(userCredential); + if (accessToken != null) { + setAccessToken(accessToken.getTokenValue()); + } + } + } +} diff --git a/integration/src/main/java/scripts/testscripts/GetStatus.java b/integration/src/main/java/scripts/testscripts/GetStatus.java new file mode 100644 index 00000000..1e3154b2 --- /dev/null +++ b/integration/src/main/java/scripts/testscripts/GetStatus.java @@ -0,0 +1,20 @@ +package scripts.testscripts; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import bio.terra.javatemplate.api.PublicApi; +import bio.terra.testrunner.runner.TestScript; +import bio.terra.testrunner.runner.config.TestUserSpecification; +import com.google.api.client.http.HttpStatusCodes; +import scripts.client.JavatemplateClient; + +public class GetStatus extends TestScript { + @Override + public void userJourney(TestUserSpecification testUser) throws Exception { + var client = new JavatemplateClient(server); + var publicApi = new PublicApi(client); + publicApi.getStatus(); + assertThat(client.getStatusCode(), is(HttpStatusCodes.STATUS_CODE_OK)); + } +} diff --git a/integration/src/main/java/scripts/testscripts/GetVersion.java b/integration/src/main/java/scripts/testscripts/GetVersion.java new file mode 100644 index 00000000..97cad602 --- /dev/null +++ b/integration/src/main/java/scripts/testscripts/GetVersion.java @@ -0,0 +1,30 @@ +package scripts.testscripts; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import bio.terra.javatemplate.api.PublicApi; +import bio.terra.testrunner.runner.TestScript; +import bio.terra.testrunner.runner.config.TestUserSpecification; +import com.google.api.client.http.HttpStatusCodes; +import scripts.client.JavatemplateClient; + +public class GetVersion extends TestScript { + @Override + public void userJourney(TestUserSpecification testUser) throws Exception { + JavatemplateClient client = new JavatemplateClient(server); + var publicApi = new PublicApi(client); + + var versionProperties = publicApi.getVersion(); + + // check the response code + assertThat(client.getStatusCode(), is(HttpStatusCodes.STATUS_CODE_OK)); + + // check the response body + assertThat(versionProperties.getGitHash(), notNullValue()); + assertThat(versionProperties.getGitTag(), notNullValue()); + assertThat(versionProperties.getGithub(), notNullValue()); + assertThat(versionProperties.getBuild(), notNullValue()); + } +} diff --git a/integration/src/main/resources/configs/integration/GetStatus.json b/integration/src/main/resources/configs/integration/GetStatus.json new file mode 100644 index 00000000..18cc4673 --- /dev/null +++ b/integration/src/main/resources/configs/integration/GetStatus.json @@ -0,0 +1,17 @@ +{ + "name": "GetStatus", + "description": "Check the service status once. No authentication required.", + "serverSpecificationFile": "local.json", + "kubernetes": {}, + "application": {}, + "testScripts": [ + { + "name": "GetStatus", + "numberOfUserJourneyThreadsToRun": 1, + "userJourneyThreadPoolSize": 2, + "expectedTimeForEach": 5, + "expectedTimeForEachUnit": "SECONDS" + } + ], + "testUserFiles": [] +} diff --git a/integration/src/main/resources/configs/integration/GetVersion.json b/integration/src/main/resources/configs/integration/GetVersion.json new file mode 100644 index 00000000..96302865 --- /dev/null +++ b/integration/src/main/resources/configs/integration/GetVersion.json @@ -0,0 +1,17 @@ +{ + "name": "GetVersion", + "description": "Check the version endpoint once. No authentication required.", + "serverSpecificationFile": "local.json", + "kubernetes": {}, + "application": {}, + "testScripts": [ + { + "name": "GetVersion", + "numberOfUserJourneyThreadsToRun": 1, + "userJourneyThreadPoolSize": 2, + "expectedTimeForEach": 5, + "expectedTimeForEachUnit": "SECONDS" + } + ], + "testUserFiles": [] +} diff --git a/integration/src/main/resources/rendered/.gitignore b/integration/src/main/resources/rendered/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/integration/src/main/resources/rendered/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/integration/src/main/resources/servers/local.json b/integration/src/main/resources/servers/local.json new file mode 100644 index 00000000..9b1190f9 --- /dev/null +++ b/integration/src/main/resources/servers/local.json @@ -0,0 +1,11 @@ +{ + "name": "local", + "description": "Local env.", + + "catalogUri": "http://localhost:8080", + + "testRunnerServiceAccountFile": "testrunner-perf.json", + + "skipDeployment": true, + "skipKubernetes": true +} diff --git a/integration/src/main/resources/serviceaccounts/delegate-user-sa.json b/integration/src/main/resources/serviceaccounts/delegate-user-sa.json new file mode 100644 index 00000000..629fd0a6 --- /dev/null +++ b/integration/src/main/resources/serviceaccounts/delegate-user-sa.json @@ -0,0 +1,5 @@ +{ + "name": "firecloud-dev@broad-dsde-dev.iam.gserviceaccount.com", + "jsonKeyFilename": "user-delegated-sa.json", + "jsonKeyDirectoryPath": "src/main/resources/rendered" +} diff --git a/integration/src/main/resources/serviceaccounts/testrunner-perf.json b/integration/src/main/resources/serviceaccounts/testrunner-perf.json new file mode 100644 index 00000000..bec13a2a --- /dev/null +++ b/integration/src/main/resources/serviceaccounts/testrunner-perf.json @@ -0,0 +1,5 @@ +{ + "name": "testrunner-perf@broad-dsde-perf.iam.gserviceaccount.com", + "jsonKeyFilename": "testrunner-perf.json", + "jsonKeyDirectoryPath": "src/main/resources/rendered" +} diff --git a/integration/src/main/resources/suites/local/FullIntegration.json b/integration/src/main/resources/suites/local/FullIntegration.json new file mode 100644 index 00000000..1406547f --- /dev/null +++ b/integration/src/main/resources/suites/local/FullIntegration.json @@ -0,0 +1,9 @@ +{ + "name": "FullIntegration", + "description": "All integration tests", + "serverSpecificationFile": "local.json", + "testConfigurationFiles": [ + "integration/GetStatus.json", + "integration/GetVersion.json" + ] +} diff --git a/integration/src/main/resources/testusers/admin.json b/integration/src/main/resources/testusers/admin.json new file mode 100644 index 00000000..8d259c75 --- /dev/null +++ b/integration/src/main/resources/testusers/admin.json @@ -0,0 +1,5 @@ +{ + "name": "admin", + "userEmail": "datacatalogadmin@test.firecloud.org", + "delegatorServiceAccountFile": "delegate-user-sa.json" +} diff --git a/integration/src/main/resources/testusers/user.json b/integration/src/main/resources/testusers/user.json new file mode 100644 index 00000000..8f0d7633 --- /dev/null +++ b/integration/src/main/resources/testusers/user.json @@ -0,0 +1,5 @@ +{ + "name": "user", + "userEmail": "datacataloguser@test.firecloud.org", + "delegatorServiceAccountFile": "delegate-user-sa.json" +} diff --git a/scripts/render_configs.sh b/scripts/render_configs.sh new file mode 100755 index 00000000..a573408f --- /dev/null +++ b/scripts/render_configs.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +ENV=${1:-dev} +VAULT_TOKEN=${2:-$(cat "$HOME"/.vault-token)} + +VAULT_ADDR="https://clotho.broadinstitute.org:8200" + +VAULT_COMMAND="vault read" + +# use SERVICE_OUTPUT_LOCATION to add any service specific secrets +SERVICE_OUTPUT_LOCATION="$(dirname "$0")/../service/src/main/resources/rendered" +INTEGRATION_OUTPUT_LOCATION="$(dirname "$0")/../integration/src/main/resources/rendered" + +if ! [ -x "$(command -v vault)" ]; then + VAULT_COMMAND="docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN -e VAULT_ADDR=$VAULT_ADDR vault:1.7.3 $VAULT_COMMAND" +fi + +$VAULT_COMMAND -field=data -format=json "secret/dsde/firecloud/$ENV/common/firecloud-account.json" >"$INTEGRATION_OUTPUT_LOCATION/user-delegated-sa.json" + +# We use the perf testrunner account in all environments. +PERF_VAULT_PATH="secret/dsde/terra/kernel/perf/common" +$VAULT_COMMAND -field=key "$PERF_VAULT_PATH/testrunner/testrunner-sa" | base64 -d > "$INTEGRATION_OUTPUT_LOCATION/testrunner-perf.json" diff --git a/service/build.gradle b/service/build.gradle new file mode 100644 index 00000000..12c77cb4 --- /dev/null +++ b/service/build.gradle @@ -0,0 +1,64 @@ +plugins { + id 'bio.terra.java-spring-conventions' + id 'de.undercouch.download' + id 'com.google.cloud.tools.jib' + id 'com.srcclr.gradle' + id 'org.sonarqube' + + id 'com.gorylenko.gradle-git-properties' version '2.3.1' + id 'org.liquibase.gradle' version '2.1.0' +} + +apply from: 'generators.gradle' +apply from: 'publishing.gradle' + +dependencies { + implementation 'bio.terra:terra-common-lib' + implementation 'org.apache.commons:commons-dbcp2' + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.retry:spring-retry' + implementation 'org.broadinstitute.dsde.workbench:sam-client_2.13:0.1-9867891-SNAP' + implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' + implementation 'org.postgresql:postgresql' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-actuator:3.0.2' + implementation 'io.micrometer:micrometer-registry-prometheus:1.10.4' + + liquibaseRuntime 'org.liquibase:liquibase-core' + liquibaseRuntime 'info.picocli:picocli:4.6.1' + liquibaseRuntime 'org.postgresql:postgresql' + liquibaseRuntime 'ch.qos.logback:logback-classic' + + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + // Fixes warning about multiple occurrences of JSONObject on the classpath + exclude group: 'com.vaadin.external.google', module: 'android-json' + } + testImplementation 'org.mockito:mockito-inline' +} + +test { + useJUnitPlatform () +} + +sonarqube { + properties { + property 'sonar.projectName', 'terra-java-project-template' + property 'sonar.projectKey', 'terra-java-project-template' + property 'sonar.organization', 'broad-databiosphere' + property 'sonar.host.url', 'https://sonarcloud.io' + } +} + +liquibase { + activities { + catalog { + changeLogFile 'src/main/resources/db/changelog.xml' + url 'jdbc:postgresql://localhost:5432/javatemplate_db' + username 'dbuser' + password 'dbpwd' + logLevel 'info' + } + } +} diff --git a/service/generators.gradle b/service/generators.gradle new file mode 100644 index 00000000..e9df9fbf --- /dev/null +++ b/service/generators.gradle @@ -0,0 +1,44 @@ +dependencies { + implementation 'io.swagger.core.v3:swagger-annotations' + runtimeOnly 'org.webjars.npm:swagger-ui-dist:4.9.0' + swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli' + + // Versioned by Spring: + implementation 'javax.validation:validation-api' + implementation 'org.webjars:webjars-locator-core' + + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' +} + +def artifactGroup = "${group}.javatemplate" + +generateSwaggerCode { + inputFile = file('src/main/resources/api/openapi.yml') + language = 'spring' + components = ['models', 'apis'] + jvmArgs = ['--add-opens=java.base/java.util=ALL-UNNAMED'] + additionalProperties = [ + modelPackage : "${artifactGroup}.model", + apiPackage : "${artifactGroup}.api", + dateLibrary : 'java11', + java8 : true, + interfaceOnly : 'true', + useTags : 'true', + springBootVersion: dependencyManagement.managedVersions['org.springframework.boot:spring-boot'] + ] +} + +String swaggerOutputSrc = "${generateSwaggerCode.outputDir}/src/main/java" + +idea.module.generatedSourceDirs = [file(swaggerOutputSrc)] +sourceSets.main.java.srcDir swaggerOutputSrc +compileJava.dependsOn generateSwaggerCode + +// see https://github.com/n0mer/gradle-git-properties +gitProperties { + keys = [] + customProperty('javatemplate.version.gitTag', { it.describe(tags: true) }) + customProperty('javatemplate.version.gitHash', { it.head().abbreviatedId }) + customProperty('javatemplate.version.github', { "https://github.com/DataBiosphere/terra-java-project-template/tree/${it.describe(tags: true)}" }) + customProperty('javatemplate.version.build', version) +} diff --git a/service/publishing.gradle b/service/publishing.gradle new file mode 100644 index 00000000..2564e39d --- /dev/null +++ b/service/publishing.gradle @@ -0,0 +1,45 @@ +import java.time.ZonedDateTime + +// Download and extract the Cloud Profiler Java Agent +ext { + // where to place the Cloud Profiler agent in the container + cloudProfilerLocation = "/opt/cprof" + + // location for jib extras, including the Java agent + jibExtraDirectory = "${buildDir}/jib-agents" +} +task downloadProfilerAgent(type: Download) { + // where to download the Cloud Profiler agent https://cloud.google.com/profiler/docs/profiling-java + src "https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent.tar.gz" + dest "${buildDir}/cprof_java_agent_gce.tar.gz" +} +task extractProfilerAgent(dependsOn: downloadProfilerAgent, type: Copy) { + from tarTree(downloadProfilerAgent.dest) + into "${jibExtraDirectory}/${cloudProfilerLocation}" +} + +jib { + from { + // see https://github.com/broadinstitute/dsp-appsec-blessed-images/tree/main/jre + image = "us.gcr.io/broad-dsp-gcr-public/base/jre:17-distroless" + } + extraDirectories { + paths = [file(jibExtraDirectory)] + } + container { + filesModificationTime = ZonedDateTime.now().toString() // to prevent ui caching + mainClass = 'bio.terra.javatemplate.App' + jvmFlags = [ + "-agentpath:" + cloudProfilerLocation + "/profiler_java_agent.so=" + + "-cprof_service=bio.terra.javatemplate" + + ",-cprof_service_version=" + version + + ",-cprof_enable_heap_sampling=true" + + ",-logtostderr" + + ",-minloglevel=2" + ] + } +} + +tasks.jib.dependsOn extractProfilerAgent +tasks.jibDockerBuild.dependsOn extractProfilerAgent +tasks.jibBuildTar.dependsOn extractProfilerAgent diff --git a/service/src/main/java/bio/terra/javatemplate/App.java b/service/src/main/java/bio/terra/javatemplate/App.java new file mode 100644 index 00000000..3692ba19 --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/App.java @@ -0,0 +1,65 @@ +package bio.terra.javatemplate; + +import bio.terra.common.logging.LoggingInitializer; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.support.JdbcTransactionManager; +import org.springframework.retry.annotation.EnableRetry; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication( + scanBasePackages = { + // Scan for iam components & configs + "bio.terra.common.iam", + // Scan for logging-related components & configs + "bio.terra.common.logging", + // Scan for Liquibase migration components & configs + "bio.terra.common.migrate", + // Transaction management and DB retry configuration + "bio.terra.common.retry.transaction", + // Scan for tracing-related components & configs + "bio.terra.common.tracing", + // Scan all service-specific packages beneath the current package + "bio.terra.javatemplate" + }) +@ConfigurationPropertiesScan("bio.terra.javatemplate") +@EnableRetry +@EnableTransactionManagement +@EnableConfigurationProperties +public class App { + public static void main(String[] args) { + new SpringApplicationBuilder(App.class).initializers(new LoggingInitializer()).run(args); + } + + private final DataSource dataSource; + + public App(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Bean("objectMapper") + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new ParameterNamesModule()) + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()) + .setDefaultPropertyInclusion(JsonInclude.Include.NON_ABSENT); + } + + // This bean plus the @EnableTransactionManagement annotation above enables the use of the + // @Transaction annotation to control the transaction properties of the data source. + @Bean("transactionManager") + public PlatformTransactionManager getTransactionManager() { + return new JdbcTransactionManager(this.dataSource); + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/config/SamConfiguration.java b/service/src/main/java/bio/terra/javatemplate/config/SamConfiguration.java new file mode 100644 index 00000000..d6aa4d69 --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/config/SamConfiguration.java @@ -0,0 +1,6 @@ +package bio.terra.javatemplate.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "javatemplate.sam") +public record SamConfiguration(String basePath) {} diff --git a/service/src/main/java/bio/terra/javatemplate/config/StatusCheckConfiguration.java b/service/src/main/java/bio/terra/javatemplate/config/StatusCheckConfiguration.java new file mode 100644 index 00000000..4d2736a6 --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/config/StatusCheckConfiguration.java @@ -0,0 +1,10 @@ +package bio.terra.javatemplate.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "javatemplate.status-check") +public record StatusCheckConfiguration( + boolean enabled, + int pollingIntervalSeconds, + int startupWaitSeconds, + int stalenessThresholdSeconds) {} diff --git a/service/src/main/java/bio/terra/javatemplate/config/VersionConfiguration.java b/service/src/main/java/bio/terra/javatemplate/config/VersionConfiguration.java new file mode 100644 index 00000000..42d21680 --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/config/VersionConfiguration.java @@ -0,0 +1,7 @@ +package bio.terra.javatemplate.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** Read from the git.properties file auto-generated at build time */ +@ConfigurationProperties("javatemplate.version") +public record VersionConfiguration(String gitHash, String gitTag, String build, String github) {} diff --git a/service/src/main/java/bio/terra/javatemplate/controller/ExampleController.java b/service/src/main/java/bio/terra/javatemplate/controller/ExampleController.java new file mode 100644 index 00000000..98ad7add --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/controller/ExampleController.java @@ -0,0 +1,81 @@ +package bio.terra.javatemplate.controller; + +import bio.terra.common.iam.BearerTokenFactory; +import bio.terra.common.iam.SamUser; +import bio.terra.common.iam.SamUserFactory; +import bio.terra.javatemplate.api.ExampleApi; +import bio.terra.javatemplate.config.SamConfiguration; +import bio.terra.javatemplate.iam.SamService; +import bio.terra.javatemplate.model.Example; +import bio.terra.javatemplate.service.ExampleService; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; + +@Controller +public class ExampleController implements ExampleApi { + + public static final String EXAMPLE_COUNTER_TAG = "tag"; + public static final String EXAMPLE_COUNTER_NAME = "example.counter"; + + private final ExampleService exampleService; + private final BearerTokenFactory bearerTokenFactory; + private final SamUserFactory samUserFactory; + private final SamConfiguration samConfiguration; + private final HttpServletRequest request; + + private final SamService samService; + + public ExampleController( + ExampleService exampleService, + BearerTokenFactory bearerTokenFactory, + SamUserFactory samUserFactory, + SamConfiguration samConfiguration, + HttpServletRequest request, + SamService samService) { + this.exampleService = exampleService; + this.bearerTokenFactory = bearerTokenFactory; + this.samUserFactory = samUserFactory; + this.samConfiguration = samConfiguration; + this.request = request; + this.samService = samService; + } + + private SamUser getUser() { + // this automatically checks if the user is enabled + return this.samUserFactory.from(request, samConfiguration.basePath()); + } + + /** Example of getting user information from sam. */ + @Override + public ResponseEntity getMessage() { + var user = getUser(); + return ResponseEntity.of( + this.exampleService.getExampleForUser(user.getSubjectId()).map(Example::message)); + } + + @Override + public ResponseEntity setMessage(String body) { + var user = getUser(); + this.exampleService.saveExample(new Example(user.getSubjectId(), body)); + return ResponseEntity.noContent().build(); + } + + /** Example of getting the bearer token and using it to make a Sam (or other service) api call */ + @Override + public ResponseEntity getAction(String resourceType, String resourceId, String action) { + var bearerToken = bearerTokenFactory.from(request); + return ResponseEntity.ok(samService.getAction(resourceType, resourceId, action, bearerToken)); + } + + @Override + public ResponseEntity incrementCounter(String tag) { + Metrics.globalRegistry + .counter(EXAMPLE_COUNTER_NAME, List.of(Tag.of(EXAMPLE_COUNTER_TAG, tag))) + .increment(); + return ResponseEntity.noContent().build(); + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/controller/GlobalExceptionHandler.java b/service/src/main/java/bio/terra/javatemplate/controller/GlobalExceptionHandler.java new file mode 100644 index 00000000..15a84292 --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/controller/GlobalExceptionHandler.java @@ -0,0 +1,16 @@ +package bio.terra.javatemplate.controller; + +import bio.terra.common.exception.AbstractGlobalExceptionHandler; +import bio.terra.javatemplate.model.ErrorReport; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler extends AbstractGlobalExceptionHandler { + + @Override + public ErrorReport generateErrorReport(Throwable ex, HttpStatus statusCode, List causes) { + return new ErrorReport().message(ex.getMessage()).statusCode(statusCode.value()); + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/controller/PublicApiController.java b/service/src/main/java/bio/terra/javatemplate/controller/PublicApiController.java new file mode 100644 index 00000000..a59cf846 --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/controller/PublicApiController.java @@ -0,0 +1,53 @@ +package bio.terra.javatemplate.controller; + +import bio.terra.javatemplate.api.PublicApi; +import bio.terra.javatemplate.config.VersionConfiguration; +import bio.terra.javatemplate.model.SystemStatus; +import bio.terra.javatemplate.model.VersionProperties; +import bio.terra.javatemplate.service.StatusService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class PublicApiController implements PublicApi { + private final StatusService statusService; + private final VersionConfiguration versionConfiguration; + + @Autowired + public PublicApiController( + StatusService statusService, VersionConfiguration versionConfiguration) { + this.statusService = statusService; + this.versionConfiguration = versionConfiguration; + } + + @Override + public ResponseEntity getStatus() { + SystemStatus systemStatus = statusService.getCurrentStatus(); + HttpStatus httpStatus = systemStatus.isOk() ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE; + return new ResponseEntity<>(systemStatus, httpStatus); + } + + @Override + public ResponseEntity getVersion() { + VersionProperties currentVersion = + new VersionProperties() + .gitTag(versionConfiguration.gitTag()) + .gitHash(versionConfiguration.gitHash()) + .github(versionConfiguration.github()) + .build(versionConfiguration.build()); + return ResponseEntity.ok(currentVersion); + } + + @GetMapping(value = "/") + public String index() { + return "redirect:swagger-ui.html"; + } + + @GetMapping(value = "/swagger-ui.html") + public String getSwagger() { + return "index"; + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/dao/ExampleDao.java b/service/src/main/java/bio/terra/javatemplate/dao/ExampleDao.java new file mode 100644 index 00000000..c5de0a4c --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/dao/ExampleDao.java @@ -0,0 +1,48 @@ +package bio.terra.javatemplate.dao; + +import bio.terra.javatemplate.model.Example; +import io.opencensus.contrib.spring.aop.Traced; +import java.util.Optional; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +public class ExampleDao { + private static final RowMapper EXAMPLE_ROW_MAPPER = + (rs, rowNum) -> + new Example(rs.getLong("id"), rs.getString("user_id"), rs.getString("message")); + + private final NamedParameterJdbcTemplate jdbcTemplate; + + public ExampleDao(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Traced + public void upsertExample(Example example) { + var query = + "INSERT INTO example (user_id, message)" + + " VALUES (:userId, :message)" + + " ON CONFLICT (user_id) DO UPDATE SET" + + " message = excluded.message"; + + var namedParameters = + new MapSqlParameterSource() + .addValue("userId", example.userId()) + .addValue("message", example.message()); + + jdbcTemplate.update(query, namedParameters); + } + + @Traced + public Optional getExampleForUser(String userId) { + var namedParameters = new MapSqlParameterSource().addValue("userId", userId); + var selectSql = "SELECT * FROM example WHERE user_id = :userId"; + return Optional.ofNullable( + DataAccessUtils.singleResult( + jdbcTemplate.query(selectSql, namedParameters, EXAMPLE_ROW_MAPPER))); + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/iam/SamClient.java b/service/src/main/java/bio/terra/javatemplate/iam/SamClient.java new file mode 100644 index 00000000..da130afd --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/iam/SamClient.java @@ -0,0 +1,49 @@ +package bio.terra.javatemplate.iam; + +import bio.terra.common.tracing.OkHttpClientTracingInterceptor; +import bio.terra.javatemplate.config.SamConfiguration; +import io.opencensus.trace.Tracing; +import okhttp3.OkHttpClient; +import org.broadinstitute.dsde.workbench.client.sam.ApiClient; +import org.broadinstitute.dsde.workbench.client.sam.api.ResourcesApi; +import org.broadinstitute.dsde.workbench.client.sam.api.StatusApi; +import org.broadinstitute.dsde.workbench.client.sam.api.UsersApi; +import org.springframework.stereotype.Component; + +@Component +public class SamClient { + private final SamConfiguration samConfig; + private final OkHttpClient okHttpClient; + + public SamClient(SamConfiguration samConfig) { + this.samConfig = samConfig; + this.okHttpClient = new ApiClient().getHttpClient(); + } + + private ApiClient getApiClient(String accessToken) { + ApiClient apiClient = getApiClient(); + apiClient.setAccessToken(accessToken); + return apiClient; + } + + private ApiClient getApiClient() { + var okHttpClientWithTracing = + this.okHttpClient + .newBuilder() + .addInterceptor(new OkHttpClientTracingInterceptor(Tracing.getTracer())) + .build(); + return new ApiClient().setHttpClient(okHttpClientWithTracing).setBasePath(samConfig.basePath()); + } + + UsersApi usersApi(String accessToken) { + return new UsersApi(getApiClient(accessToken)); + } + + ResourcesApi resourcesApi(String accessToken) { + return new ResourcesApi(getApiClient(accessToken)); + } + + StatusApi statusApi() { + return new StatusApi(getApiClient()); + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/iam/SamService.java b/service/src/main/java/bio/terra/javatemplate/iam/SamService.java new file mode 100644 index 00000000..0ac37f0c --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/iam/SamService.java @@ -0,0 +1,61 @@ +package bio.terra.javatemplate.iam; + +import bio.terra.common.iam.BearerToken; +import bio.terra.common.sam.SamRetry; +import bio.terra.common.sam.exception.SamExceptionFactory; +import bio.terra.javatemplate.model.SystemStatusSystems; +import java.util.List; +import org.broadinstitute.dsde.workbench.client.sam.ApiException; +import org.broadinstitute.dsde.workbench.client.sam.model.SystemStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SamService { + private static final Logger logger = LoggerFactory.getLogger(SamService.class); + private final SamClient samClient; + + @Autowired + public SamService(SamClient samClient) { + this.samClient = samClient; + } + + public boolean getAction( + String resourceType, String resourceId, String action, BearerToken bearerToken) { + try { + return SamRetry.retry( + () -> + samClient + .resourcesApi(bearerToken.getToken()) + .resourcePermissionV2(resourceType, resourceId, action)); + } catch (ApiException e) { + throw SamExceptionFactory.create(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw SamExceptionFactory.create("Sam retry interrupted", e); + } + } + + public SystemStatusSystems status() { + // No access token needed since this is an unauthenticated API. + try { + // Don't retry status check + SystemStatus samStatus = samClient.statusApi().getSystemStatus(); + var result = new SystemStatusSystems().ok(samStatus.getOk()); + var samSystems = samStatus.getSystems(); + // Populate error message if Sam status is non-ok + if (result.isOk() == null || !result.isOk()) { + String errorMsg = "Sam status check failed. Messages = " + samSystems; + logger.error(errorMsg); + result.addMessagesItem(errorMsg); + } + return result; + } catch (Exception e) { + String errorMsg = "Sam status check failed"; + logger.error(errorMsg, e); + return new SystemStatusSystems().ok(false).messages(List.of(errorMsg)); + } + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/model/Example.java b/service/src/main/java/bio/terra/javatemplate/model/Example.java new file mode 100644 index 00000000..ef364ea9 --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/model/Example.java @@ -0,0 +1,15 @@ +package bio.terra.javatemplate.model; + +import java.util.Objects; +import javax.annotation.Nullable; + +public record Example(@Nullable Long id, String userId, String message) { + public Example { + Objects.requireNonNull(userId); + Objects.requireNonNull(message); + } + + public Example(String userId, String message) { + this(null, userId, message); + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/service/BaseStatusService.java b/service/src/main/java/bio/terra/javatemplate/service/BaseStatusService.java new file mode 100644 index 00000000..37f8370e --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/service/BaseStatusService.java @@ -0,0 +1,90 @@ +package bio.terra.javatemplate.service; + +import bio.terra.javatemplate.config.StatusCheckConfiguration; +import bio.terra.javatemplate.model.SystemStatus; +import bio.terra.javatemplate.model.SystemStatusSystems; +import com.google.common.annotations.VisibleForTesting; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BaseStatusService { + private static final Logger logger = LoggerFactory.getLogger(BaseStatusService.class); + /** cached status */ + private final AtomicReference cachedStatus; + /** configuration parameters */ + private final StatusCheckConfiguration configuration; + /** set of status methods to check */ + private final ConcurrentHashMap> statusCheckMap; + /** scheduler */ + private final ScheduledExecutorService scheduler; + /** last time cache was updated */ + private final AtomicReference lastStatusUpdate; + + public BaseStatusService(StatusCheckConfiguration configuration) { + this.configuration = configuration; + statusCheckMap = new ConcurrentHashMap<>(); + cachedStatus = new AtomicReference<>(new SystemStatus().ok(false)); + lastStatusUpdate = new AtomicReference<>(Instant.now()); + scheduler = Executors.newScheduledThreadPool(1); + } + + @PostConstruct + private void startStatusChecking() { + if (configuration.enabled()) { + scheduler.scheduleAtFixedRate( + this::checkStatus, + configuration.startupWaitSeconds(), + configuration.pollingIntervalSeconds(), + TimeUnit.SECONDS); + } + } + + void registerStatusCheck(String name, Supplier checkFn) { + statusCheckMap.put(name, checkFn); + } + + @VisibleForTesting + void checkStatus() { + if (configuration.enabled()) { + var newStatus = new SystemStatus(); + try { + var systems = + statusCheckMap.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); + newStatus.setOk(systems.values().stream().allMatch(SystemStatusSystems::isOk)); + newStatus.setSystems(systems); + } catch (Exception e) { + logger.warn("Status check exception", e); + newStatus.setOk(false); + } + cachedStatus.set(newStatus); + lastStatusUpdate.set(Instant.now()); + } + } + + public SystemStatus getCurrentStatus() { + if (configuration.enabled()) { + // If staleness time (last update + stale threshold) is before the current time, then + // we are officially not OK. + if (lastStatusUpdate + .get() + .plusSeconds(configuration.stalenessThresholdSeconds()) + .isBefore(Instant.now())) { + logger.warn("Status has not been updated since {}", lastStatusUpdate); + cachedStatus.set(new SystemStatus().ok(false)); + } + return cachedStatus.get(); + } + return new SystemStatus().ok(true); + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/service/ExampleService.java b/service/src/main/java/bio/terra/javatemplate/service/ExampleService.java new file mode 100644 index 00000000..5dad6530 --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/service/ExampleService.java @@ -0,0 +1,28 @@ +package bio.terra.javatemplate.service; + +import bio.terra.common.db.ReadTransaction; +import bio.terra.common.db.WriteTransaction; +import bio.terra.javatemplate.dao.ExampleDao; +import bio.terra.javatemplate.model.Example; +import java.util.Optional; +import org.springframework.stereotype.Service; + +@Service +public class ExampleService { + private final ExampleDao exampleDao; + + public ExampleService(ExampleDao exampleDao) { + this.exampleDao = exampleDao; + } + + // README docs/transactions.md + @WriteTransaction + public void saveExample(Example example) { + exampleDao.upsertExample(example); + } + + @ReadTransaction + public Optional getExampleForUser(String userId) { + return exampleDao.getExampleForUser(userId); + } +} diff --git a/service/src/main/java/bio/terra/javatemplate/service/StatusService.java b/service/src/main/java/bio/terra/javatemplate/service/StatusService.java new file mode 100644 index 00000000..846fe10e --- /dev/null +++ b/service/src/main/java/bio/terra/javatemplate/service/StatusService.java @@ -0,0 +1,41 @@ +package bio.terra.javatemplate.service; + +import bio.terra.javatemplate.config.StatusCheckConfiguration; +import bio.terra.javatemplate.iam.SamService; +import bio.terra.javatemplate.model.SystemStatusSystems; +import java.sql.Connection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Service; + +@Service +public class StatusService extends BaseStatusService { + private static final Logger logger = LoggerFactory.getLogger(StatusService.class); + + private final NamedParameterJdbcTemplate jdbcTemplate; + + @Autowired + public StatusService( + NamedParameterJdbcTemplate jdbcTemplate, + StatusCheckConfiguration configuration, + SamService samService) { + super(configuration); + this.jdbcTemplate = jdbcTemplate; + registerStatusCheck("CloudSQL", this::databaseStatus); + registerStatusCheck("Sam", samService::status); + } + + private SystemStatusSystems databaseStatus() { + try { + logger.debug("Checking database connection valid"); + return new SystemStatusSystems() + .ok(jdbcTemplate.getJdbcTemplate().execute((Connection conn) -> conn.isValid(5000))); + } catch (Exception ex) { + String errorMsg = "Database status check failed"; + logger.error(errorMsg, ex); + return new SystemStatusSystems().ok(false).addMessagesItem(errorMsg + ": " + ex.getMessage()); + } + } +} diff --git a/service/src/main/resources/api/openapi.yml b/service/src/main/resources/api/openapi.yml new file mode 100644 index 00000000..b5d174f7 --- /dev/null +++ b/service/src/main/resources/api/openapi.yml @@ -0,0 +1,200 @@ +openapi: 3.0.3 +info: + title: Terra Java Project Template + description: Terra Java Project Template + version: 0.0.1 +paths: + /status: + get: + summary: Check status of the service + tags: [ public ] + operationId: getStatus + security: [ ] + responses: + '200': + $ref: '#/components/responses/SystemStatusResponse' + '500': + $ref: '#/components/responses/ServerError' + '503': + $ref: '#/components/responses/SystemStatusResponse' + + /version: + get: + summary: Get version info of the deployed service + tags: [ public ] + operationId: getVersion + security: [ ] + responses: + '200': + description: Version information + content: + application/json: + schema: + $ref: '#/components/schemas/VersionProperties' + '404': + description: "Version not configured" + '500': + $ref: '#/components/responses/ServerError' + + # README /docs/api_versioning.md + /api/example/v1/message: + get: + summary: Gets your message + tags: [ example ] + operationId: getMessage + responses: + '200': + description: Your message + content: + application/json: + schema: + type: string + '404': + description: You don't have a message + '500': + $ref: '#/components/responses/ServerError' + post: + summary: Stores your message + tags: [ example ] + operationId: setMessage + requestBody: + content: + 'application/json': + schema: + type: string + required: true + responses: + '204': + description: Message saved + '500': + $ref: '#/components/responses/ServerError' + /api/example/v1/{resourceType}/{resourceId}/{action}: + get: + summary: Checks sam access + tags: [ example ] + operationId: getAction + parameters: + - name: resourceType + in: path + required: true + schema: + type: string + - name: resourceId + in: path + required: true + schema: + type: string + - name: action + in: path + required: true + schema: + type: string + responses: + '200': + description: action access + content: + application/json: + schema: + type: boolean + '500': + $ref: '#/components/responses/ServerError' + /api/example/v1/counter: + post: + summary: increment a metrics counter + tags: [ example ] + operationId: incrementCounter + requestBody: + description: tag for counter + content: + application/json: + schema: + type: string + responses: + '204': + description: success + '500': + $ref: '#/components/responses/ServerError' + +components: + responses: + SystemStatusResponse: + description: A JSON description of the subsystems and their statuses. + content: + application/json: + schema: + $ref: '#/components/schemas/SystemStatus' + + # Error Responses + BadRequest: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + PermissionDenied: + description: Permission denied + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + NotFound: + description: Not found (or unauthorized) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + ServerError: + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + + schemas: + ErrorReport: + type: object + required: [ message, statusCode ] + properties: + message: + type: string + statusCode: + type: integer + + SystemStatus: + required: [ ok, systems ] + type: object + properties: + ok: + type: boolean + description: whether any system(s) need attention + systems: + type: object + additionalProperties: + type: object + properties: + ok: + type: boolean + messages: + type: array + items: + type: string + + VersionProperties: + type: object + properties: + gitTag: + type: string + gitHash: + type: string + github: + type: string + build: + type: string + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + +security: + - bearerAuth: [ ] diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml new file mode 100644 index 00000000..dac5984e --- /dev/null +++ b/service/src/main/resources/application.yml @@ -0,0 +1,84 @@ +# All env variables that are used in one place +# This is for deployment-specific values, which may be managed by other teams + +env: + db: + host: ${DATABASE_HOSTNAME:127.0.0.1}:5432 + init: ${INIT_DB:false} + name: ${DATABASE_NAME:javatemplate_db} + password: ${DATABASE_USER_PASSWORD:dbpwd} + user: ${DATABASE_USER:dbuser} + tracing: + exportEnabled: ${CLOUD_TRACE_ENABLED:false} + samplingRate: ${SAMPLING_PROBABILITY:0} + sam: + basePath: ${SAM_ADDRESS:https://sam.dsde-dev.broadinstitute.org} + +# Below here is non-deployment-specific + +# When the target is 'local' the write-config.sh script will generate this properties file. It +# contains the configuration of the BPM test application. We can use that application in our +# integration testing to make sure the application code paths are working. However, we do not +# want it to appear in production environments. +spring.config.import: optional:file:../config/local-properties.yml;classpath:git.properties + +logging.pattern.level: '%X{requestId} %5p' + +server: + compression: + enabled: true + mimeTypes: text/css,application/javascript + port: 8080 + +spring: + # application name and version are used to populate the logging serviceContext + # https://github.com/DataBiosphere/terra-common-lib/blob/480ab3daae282ddff0fef8dc329494a4422e32f1/src/main/java/bio/terra/common/logging/GoogleJsonLayout.java#L118 + application.name: javatemplate + application.version: ${javatemplate.version.gitHash:unknown} + + datasource: + hikari: + connection-timeout: 5000 + maximum-pool-size: 8 # cpu count * 2 https://kwahome.medium.com/database-connections-less-is-more-86c406b6fad + password: ${env.db.password} + url: jdbc:postgresql://${env.db.host}/${env.db.name} + username: ${env.db.user} + + web: + resources: + cache: + cachecontrol: + maxAge: 0 + mustRevalidate: true + useLastModified: false + staticLocations: classpath:/api/ + +management: + server: + port: 9098 + endpoints: + web: + exposure: + include: "*" + +javatemplate: + ingress: + # Default value that's overridden by Helm. + domainName: localhost:8080 + + status-check: + enabled: true + pollingIntervalSeconds: 60 + startupWaitSeconds: 5 + stalenessThresholdSeconds: 125 + + sam: + basePath: ${env.sam.basePath} + +terra.common: + kubernetes: + inKubernetes: false + + tracing: + stackdriverExportEnabled: ${env.tracing.exportEnabled} + samplingRate: ${env.tracing.samplingRate} diff --git a/service/src/main/resources/db/changelog/changesets/initial_schema.yaml b/service/src/main/resources/db/changelog/changesets/initial_schema.yaml new file mode 100644 index 00000000..c9a7b9c7 --- /dev/null +++ b/service/src/main/resources/db/changelog/changesets/initial_schema.yaml @@ -0,0 +1,26 @@ +databaseChangeLog: + - changeSet: + id: "not_so_interesting" + author: doge + changes: + - createTable: + tableName: example + columns: + - column: + name: id + type: int + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: user_id + type: text + constraints: + nullable: false + unique: true + - column: + name: message + type: text + constraints: + nullable: false diff --git a/service/src/main/resources/db/changelog/db.changelog-master.yaml b/service/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 00000000..3f922bff --- /dev/null +++ b/service/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,7 @@ +databaseChangeLog: + - include: + file: changesets/initial_schema.yaml + relativeToChangelogFile: true +# README: it is a best practice to put each DDL statement in its own change set. DDL statements +# are atomic. When they are grouped in a changeset and one fails the changeset cannot be +# rolled back or rerun making recovery more difficult \ No newline at end of file diff --git a/service/src/main/resources/rendered/.gitignore b/service/src/main/resources/rendered/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/service/src/main/resources/rendered/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/service/src/main/resources/templates/index.html b/service/src/main/resources/templates/index.html new file mode 100644 index 00000000..f17775f3 --- /dev/null +++ b/service/src/main/resources/templates/index.html @@ -0,0 +1,144 @@ + + + + + + + + + Terra Java Project Template SwaggerUI + + + + + +
+ + + + + diff --git a/service/src/test/java/bio/terra/javatemplate/BaseSpringBootTest.java b/service/src/test/java/bio/terra/javatemplate/BaseSpringBootTest.java new file mode 100644 index 00000000..02fdfc18 --- /dev/null +++ b/service/src/test/java/bio/terra/javatemplate/BaseSpringBootTest.java @@ -0,0 +1,8 @@ +package bio.terra.javatemplate; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"test", "human-readable-logging"}) +public abstract class BaseSpringBootTest {} diff --git a/service/src/test/java/bio/terra/javatemplate/api/ExampleControllerTest.java b/service/src/test/java/bio/terra/javatemplate/api/ExampleControllerTest.java new file mode 100644 index 00000000..ce0d2d4a --- /dev/null +++ b/service/src/test/java/bio/terra/javatemplate/api/ExampleControllerTest.java @@ -0,0 +1,103 @@ +package bio.terra.javatemplate.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import bio.terra.common.iam.BearerToken; +import bio.terra.common.iam.BearerTokenFactory; +import bio.terra.common.iam.SamUser; +import bio.terra.common.iam.SamUserFactory; +import bio.terra.javatemplate.config.SamConfiguration; +import bio.terra.javatemplate.controller.ExampleController; +import bio.terra.javatemplate.iam.SamService; +import bio.terra.javatemplate.model.Example; +import bio.terra.javatemplate.service.ExampleService; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import java.util.Optional; +import java.util.UUID; +import javax.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; + +@ContextConfiguration(classes = ExampleController.class) +@WebMvcTest +public class ExampleControllerTest { + @MockBean ExampleService serviceMock; + @MockBean SamUserFactory samUserFactoryMock; + @MockBean BearerTokenFactory bearerTokenFactory; + @MockBean SamConfiguration samConfiguration; + @MockBean SamService samService; + + @Autowired private MockMvc mockMvc; + + private SamUser testUser = + new SamUser( + "test@email", + UUID.randomUUID().toString(), + new BearerToken(UUID.randomUUID().toString())); + + @BeforeEach + void beforeEach() { + when(samUserFactoryMock.from(any(HttpServletRequest.class), any())).thenReturn(testUser); + } + + @Test + void testGetMessageOk() throws Exception { + var example = new Example(testUser.getSubjectId(), "message"); + when(serviceMock.getExampleForUser(testUser.getSubjectId())).thenReturn(Optional.of(example)); + + mockMvc + .perform(get("/api/example/v1/message")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string(example.message())); + } + + @Test + void testGetMessageNotFound() throws Exception { + when(serviceMock.getExampleForUser(testUser.getSubjectId())).thenReturn(Optional.empty()); + + mockMvc.perform(get("/api/example/v1/message")).andExpect(status().isNotFound()); + } + + @Test + void testIncrementCounter() throws Exception { + var meterRegistry = new SimpleMeterRegistry(); + Metrics.globalRegistry.add(meterRegistry); + + try { + final String tagValue = "tag_value"; + mockMvc + .perform( + post("/api/example/v1/counter") + .contentType(MediaType.APPLICATION_JSON) + .content(tagValue)) + .andExpect(status().isNoContent()); + + var counter = + meterRegistry + .find(ExampleController.EXAMPLE_COUNTER_NAME) + .tags(ExampleController.EXAMPLE_COUNTER_TAG, tagValue) + .counter(); + + assertNotNull(counter); + assertEquals(counter.count(), 1); + + } finally { + Metrics.globalRegistry.remove(meterRegistry); + } + } +} diff --git a/service/src/test/java/bio/terra/javatemplate/api/PublicApiControllerTest.java b/service/src/test/java/bio/terra/javatemplate/api/PublicApiControllerTest.java new file mode 100644 index 00000000..71554ff7 --- /dev/null +++ b/service/src/test/java/bio/terra/javatemplate/api/PublicApiControllerTest.java @@ -0,0 +1,74 @@ +package bio.terra.javatemplate.api; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import bio.terra.javatemplate.config.VersionConfiguration; +import bio.terra.javatemplate.controller.PublicApiController; +import bio.terra.javatemplate.model.SystemStatus; +import bio.terra.javatemplate.service.StatusService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; + +@ContextConfiguration(classes = PublicApiController.class) +@WebMvcTest +class PublicApiControllerTest { + + @Autowired private MockMvc mockMvc; + + @MockBean private StatusService statusService; + + @MockBean private VersionConfiguration versionConfiguration; + + @Test + void testStatus() throws Exception { + SystemStatus systemStatus = new SystemStatus().ok(true); + when(statusService.getCurrentStatus()).thenReturn(systemStatus); + this.mockMvc.perform(get("/status")).andExpect(status().isOk()); + } + + @Test + void testStatusCheckFails() throws Exception { + SystemStatus systemStatus = new SystemStatus().ok(false); + when(statusService.getCurrentStatus()).thenReturn(systemStatus); + this.mockMvc.perform(get("/status")).andExpect(status().is5xxServerError()); + } + + @Test + void testVersion() throws Exception { + String gitTag = "0.1.0"; + String gitHash = "abc1234"; + String github = "https://github.com/DataBiosphere/terra-java-project-template/tree/0.9.0"; + String build = "0.1.0"; + + when(versionConfiguration.gitTag()).thenReturn(gitTag); + when(versionConfiguration.gitHash()).thenReturn(gitHash); + when(versionConfiguration.github()).thenReturn(github); + when(versionConfiguration.build()).thenReturn(build); + + this.mockMvc + .perform(get("/version")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.gitTag").value(gitTag)) + .andExpect(jsonPath("$.gitHash").value(gitHash)) + .andExpect(jsonPath("$.github").value(github)) + .andExpect(jsonPath("$.build").value(build)); + } + + @Test + void testGetSwagger() throws Exception { + this.mockMvc.perform(get("/swagger-ui.html")).andExpect(status().isOk()); + } + + @Test + void testIndex() throws Exception { + this.mockMvc.perform(get("/")).andExpect(redirectedUrl("swagger-ui.html")); + } +} diff --git a/service/src/test/java/bio/terra/javatemplate/dao/BaseDaoTest.java b/service/src/test/java/bio/terra/javatemplate/dao/BaseDaoTest.java new file mode 100644 index 00000000..90552465 --- /dev/null +++ b/service/src/test/java/bio/terra/javatemplate/dao/BaseDaoTest.java @@ -0,0 +1,9 @@ +package bio.terra.javatemplate.dao; + +import bio.terra.javatemplate.BaseSpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Rollback +public abstract class BaseDaoTest extends BaseSpringBootTest {} diff --git a/service/src/test/java/bio/terra/javatemplate/dao/ExampleDaoTest.java b/service/src/test/java/bio/terra/javatemplate/dao/ExampleDaoTest.java new file mode 100644 index 00000000..9a39b75b --- /dev/null +++ b/service/src/test/java/bio/terra/javatemplate/dao/ExampleDaoTest.java @@ -0,0 +1,54 @@ +package bio.terra.javatemplate.dao; + +import static org.junit.jupiter.api.Assertions.*; + +import bio.terra.javatemplate.model.Example; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class ExampleDaoTest extends BaseDaoTest { + @Autowired ExampleDao exampleDao; + + @Test + void testDifferentUserUpsert() { + var user1 = new Example("user1", "user1 message"); + exampleDao.upsertExample(user1); + var user1Actual = exampleDao.getExampleForUser(user1.userId()); + + var user2 = new Example("user2", "user2 message"); + exampleDao.upsertExample(user2); + var user2Actual = exampleDao.getExampleForUser(user2.userId()); + + assertTrue(user1Actual.isPresent()); + assertEquals(user1.userId(), user1Actual.get().userId()); + assertEquals(user1.message(), user1Actual.get().message()); + + assertTrue(user2Actual.isPresent()); + assertEquals(user2.userId(), user2Actual.get().userId()); + assertEquals(user2.message(), user2Actual.get().message()); + } + + @Test + void testRepeatedUpsert() { + var example1 = new Example("testUser", "testMessage"); + exampleDao.upsertExample(example1); + var firstSave = exampleDao.getExampleForUser(example1.userId()); + assertTrue(firstSave.isPresent()); + assertEquals(example1.userId(), firstSave.get().userId()); + assertEquals(example1.message(), firstSave.get().message()); + + var example2 = new Example(example1.userId(), "differentMessage"); + exampleDao.upsertExample(example2); + var secondSave = exampleDao.getExampleForUser(example1.userId()); + + assertTrue(secondSave.isPresent()); + assertEquals(firstSave.get().id(), secondSave.get().id()); + assertEquals(example2.userId(), secondSave.get().userId()); + assertEquals(example2.message(), secondSave.get().message()); + } + + @Test + void testNotFound() { + assertTrue(exampleDao.getExampleForUser("testUser").isEmpty()); + } +} diff --git a/service/src/test/java/bio/terra/javatemplate/iam/SamClientTest.java b/service/src/test/java/bio/terra/javatemplate/iam/SamClientTest.java new file mode 100644 index 00000000..6c2467c6 --- /dev/null +++ b/service/src/test/java/bio/terra/javatemplate/iam/SamClientTest.java @@ -0,0 +1,30 @@ +package bio.terra.javatemplate.iam; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import bio.terra.javatemplate.config.SamConfiguration; +import org.broadinstitute.dsde.workbench.client.sam.ApiClient; +import org.broadinstitute.dsde.workbench.client.sam.auth.OAuth; +import org.junit.jupiter.api.Test; + +class SamClientTest { + private static final String BASE_PATH = "basepath"; + private static final String TOKEN = "token"; + private static final String AUTH_NAME = "b2coauth"; + + private final SamClient client = new SamClient(new SamConfiguration(BASE_PATH)); + + @Test + void testApis() { + validateClient(client.statusApi().getApiClient(), null); + validateClient(client.usersApi(TOKEN).getApiClient(), TOKEN); + validateClient(client.resourcesApi(TOKEN).getApiClient(), TOKEN); + } + + private static void validateClient(ApiClient client, String token) { + assertThat(client.getBasePath(), is(BASE_PATH)); + OAuth oauth = (OAuth) client.getAuthentication(AUTH_NAME); + assertThat(oauth.getAccessToken(), is(token)); + } +} diff --git a/service/src/test/java/bio/terra/javatemplate/iam/SamServiceTest.java b/service/src/test/java/bio/terra/javatemplate/iam/SamServiceTest.java new file mode 100644 index 00000000..fc247907 --- /dev/null +++ b/service/src/test/java/bio/terra/javatemplate/iam/SamServiceTest.java @@ -0,0 +1,54 @@ +package bio.terra.javatemplate.iam; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import bio.terra.javatemplate.BaseSpringBootTest; +import org.broadinstitute.dsde.workbench.client.sam.ApiException; +import org.broadinstitute.dsde.workbench.client.sam.api.StatusApi; +import org.broadinstitute.dsde.workbench.client.sam.model.SystemStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +@ExtendWith(MockitoExtension.class) +class SamServiceTest extends BaseSpringBootTest { + + @MockBean private SamClient samClient; + @MockBean private StatusApi statusApi; + + @Autowired private SamService samService; + + private void mockStatus() { + when(samClient.statusApi()).thenReturn(statusApi); + } + + @Test + void status() throws Exception { + mockStatus(); + SystemStatus status = new SystemStatus().ok(true); + when(statusApi.getSystemStatus()).thenReturn(status); + var samStatus = samService.status(); + assertTrue(samStatus.isOk()); + } + + @Test + void statusDown() throws Exception { + mockStatus(); + SystemStatus status = new SystemStatus().ok(false); + when(statusApi.getSystemStatus()).thenReturn(status); + var samStatus = samService.status(); + assertFalse(samStatus.isOk()); + } + + @Test + void statusException() throws Exception { + mockStatus(); + when(statusApi.getSystemStatus()).thenThrow(new ApiException()); + var samStatus = samService.status(); + assertFalse(samStatus.isOk()); + } +} diff --git a/service/src/test/java/bio/terra/javatemplate/service/BaseStatusServiceTest.java b/service/src/test/java/bio/terra/javatemplate/service/BaseStatusServiceTest.java new file mode 100644 index 00000000..26e0c5a4 --- /dev/null +++ b/service/src/test/java/bio/terra/javatemplate/service/BaseStatusServiceTest.java @@ -0,0 +1,26 @@ +package bio.terra.javatemplate.service; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import bio.terra.javatemplate.config.StatusCheckConfiguration; +import bio.terra.javatemplate.model.SystemStatus; +import bio.terra.javatemplate.model.SystemStatusSystems; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class BaseStatusServiceTest { + + @Test + void getCurrentStatus() { + var config = new StatusCheckConfiguration(true, 0, 0, 10); + BaseStatusService service = new BaseStatusService(config); + var status = new SystemStatusSystems().ok(true); + service.registerStatusCheck("test", () -> status); + assertThat(service.getCurrentStatus(), is(new SystemStatus().ok(false))); + service.checkStatus(); + assertThat( + service.getCurrentStatus(), + is(new SystemStatus().ok(true).systems(Map.of("test", status)))); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..0e0df3b0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'terra-java-project-template' +include('service', 'client', 'integration') + +gradle.ext.releaseVersion = '0.11.0' From ccd077c338b51fed00f8033562148159b0df8b3f Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 20 Jun 2023 21:58:58 -0400 Subject: [PATCH 02/46] Remove example repo code Remove template docs Remove datasource from App --- docs/api_versioning.md | 7 -- docs/controller_service_dao.md | 42 ------- docs/transactions.md | 116 ------------------ .../main/java/bio/terra/javatemplate/App.java | 17 +-- .../controller/ExampleController.java | 81 ------------ .../controller/PublicApiController.java | 53 -------- .../terra/javatemplate/dao/ExampleDao.java | 48 -------- .../bio/terra/javatemplate/iam/SamClient.java | 49 -------- .../terra/javatemplate/iam/SamService.java | 61 --------- .../javatemplate/service/ExampleService.java | 28 ----- .../javatemplate/service/StatusService.java | 41 ------- .../api/ExampleControllerTest.java | 103 ---------------- .../api/PublicApiControllerTest.java | 74 ----------- .../terra/javatemplate/dao/BaseDaoTest.java | 9 -- .../javatemplate/dao/ExampleDaoTest.java | 54 -------- .../terra/javatemplate/iam/SamClientTest.java | 30 ----- .../javatemplate/iam/SamServiceTest.java | 54 -------- 17 files changed, 1 insertion(+), 866 deletions(-) delete mode 100644 docs/api_versioning.md delete mode 100644 docs/controller_service_dao.md delete mode 100644 docs/transactions.md delete mode 100644 service/src/main/java/bio/terra/javatemplate/controller/ExampleController.java delete mode 100644 service/src/main/java/bio/terra/javatemplate/controller/PublicApiController.java delete mode 100644 service/src/main/java/bio/terra/javatemplate/dao/ExampleDao.java delete mode 100644 service/src/main/java/bio/terra/javatemplate/iam/SamClient.java delete mode 100644 service/src/main/java/bio/terra/javatemplate/iam/SamService.java delete mode 100644 service/src/main/java/bio/terra/javatemplate/service/ExampleService.java delete mode 100644 service/src/main/java/bio/terra/javatemplate/service/StatusService.java delete mode 100644 service/src/test/java/bio/terra/javatemplate/api/ExampleControllerTest.java delete mode 100644 service/src/test/java/bio/terra/javatemplate/api/PublicApiControllerTest.java delete mode 100644 service/src/test/java/bio/terra/javatemplate/dao/BaseDaoTest.java delete mode 100644 service/src/test/java/bio/terra/javatemplate/dao/ExampleDaoTest.java delete mode 100644 service/src/test/java/bio/terra/javatemplate/iam/SamClientTest.java delete mode 100644 service/src/test/java/bio/terra/javatemplate/iam/SamServiceTest.java diff --git a/docs/api_versioning.md b/docs/api_versioning.md deleted file mode 100644 index 0bdfbd03..00000000 --- a/docs/api_versioning.md +++ /dev/null @@ -1,7 +0,0 @@ -We have taken inspiration for our API versioning standards from the [Google cloud API versioning scheme](https://cloud.google.com/apis/design/versioning). While not exactly identical, for practical purposes it is. - -APIs shall be versioned per interface. An interface is a logical collection of endpoints which can be viewed as a singular entity. A service might implement multiple interfaces. In terms of the correct granularity for an interface, think of it as something which would make sense for a single service to implement. As an example, Cromwell has endpoints both for workflow submission & manipulation but also reading from the metadata store. These could be seen as two separate interfaces as one could imagine a service dedicated to reading from the metadata store. - -Versions shall be bumped on any breaking, non-backwards compatible change. In [Semantic Versioning](http://semver.org/) terms, these would be for major version changes. Unless we encounter reasons to diverge we shall follow the [Google definition of compatibility](https://cloud.google.com/apis/design/compatibility). Versions are expected to change infrequently, with every effort made to integrate required functionality in an additive fashion. - -URLs shall take the form INTERFACE/vVERSION/path. For example: `/api/example/v1/message` where `/api/example` is the interface, `v1` is the version and `message` is the path. diff --git a/docs/controller_service_dao.md b/docs/controller_service_dao.md deleted file mode 100644 index 3341e79c..00000000 --- a/docs/controller_service_dao.md +++ /dev/null @@ -1,42 +0,0 @@ -Terra services are generally organized into 3 layers, controller, service and DAO (data access object). -Controllers handle api requests and responses. -Services implement all business logic. -DAOs implement data persistence and querying. - -# Controller -Controllers are the api entry point to the service. [OpenAPI](../service/src/main/resources/api/openapi.yml) is used to [generate](../service/generators.gradle) `*API` interfaces -which are implemented by controller classes. In this way we can craft a service's api and have the implementation -flow from there. Alternatively we could use java annotations on controller classes to generate OpenAPI -but that tends to lead to a poor API, we think about the api second or not at all. - -Controllers are generally responsible for -* Resolving the user if required -* Checking access control -* Any translation between API model objects and service model objects -* Making service calls -* Translating service responses and errors to API responses and status codes - -Controllers talk to services. It should be an exceptional situation that they talk directly to DAOs. - -Note on access control: There can be some debate on whether this could be elsewhere. Certainly -more complicated or cross resource access control checks can be elsewhere. Pushing access control -checks down to the service layer generally make services less reusable. The most important thing -about access control checks is that it are implemented consistently and readably to avoid mistakes. - -# Service -The service layer handles all business logic. -If it is interesting, the code for it probably lives here. -If it is coordinating anything, the code for it probably lives here. -If it is making a decision, the code for it probably lives here. - -The service layer is the transaction boundary (see [transactions](transactions.md) for more details). - -Services talk to other services and DAOs. - -# DAOs -DAOs or data access objects know how to do data things. Reading or writing to databases. -Accessing other Terra services. DAOs do not make decisions, they get information for the service -layer to use to make decisions. These should be stupid that unconditionally do what they are told -and return undigested information. - -DAOs are the bottom layer and should not call other DAOs or services. \ No newline at end of file diff --git a/docs/transactions.md b/docs/transactions.md deleted file mode 100644 index 08e56e79..00000000 --- a/docs/transactions.md +++ /dev/null @@ -1,116 +0,0 @@ -# Transaction Barrier -Transactions are tricky, it is best not to have to think about them all the time. -Therefore, it is beneficial to have a consistent place in your code flow where transactions -are handled. This is the transaction barrier. - -If transactions begin too early in the code flow there is risk of them running too long. For example, -making external api calls within a transaction. Long running transactions lead to scalability problems -because there is are greater chances for transactions to collide leading to rollbacks and retries. - -If transactions begin too late in the code flow then you might as well turn on auto-commit. -Essentially the value of transactions disappear because you can't do more than one thing atomically. - -#Transaction Implementation with Spring -Transaction management in Spring starts with an annotation on a method which tells Spring that the entire -method should be wrapped in a transaction. But how does Spring do this? From the caller's perspective, -it is just calling a method on a java class. From the method's code perspective there's no special code. -The answer is that Spring can wrap a Bean inside a Proxy. When the caller invokes a method of a Bean, -it is not on the java class that it appears to be but rather a Proxy that can add special sauce before and -after calling the target java class itself. See [this article](https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring) for more depth. - -This has one very important implication: it is critical that methods are invoked upon the Spring Bean and -not the underlying java class instance. Any injected dependency (e.g. via `@Autowired`) is ok. Using -`this` within a Bean to call other methods may be a problem if features from the Proxy are expected -and the error is not obvious_. Another implication is that transaction annotations only work on public -methods. - -### Examples -Assume a service that wants to perform a transaction, then do some stuff (such as an external api call), then -perform another transaction. - -#### Problematic (Intra-Bean) Code Example -``` -@Service -public class ProblematicService { - public void complexCode() { - doTransactionOne(); - - // code between transactions - - doTransactionTwo(); - } - - @Transactional - public void doTransactionOne() {...} - - @Transactional - public void doTransactionTwo() {...} -} -``` -This code is problematic because the calls from `complexCode` to `doTransactionOne` and `doTransactionTwo` -are internal to the class and do not go through the Proxy. The `@Transactional` annotations have no affect. - -#### Inter-Bean Code Solution -``` -@Service -public class OrchestratingService { - @Autowired LowerLevelService lls; - - public void complexCode() { - lls.doTransactionOne(); - - // code between transactions - - lls.doTransactionTwo(); - } -} - -@Service -public class LowerLevelService() { - @Transactional - public void doTransactionOne() {...} - - @Transactional - public void doTransactionTwo() {...} -} -``` -This code puts the transactions in a separate service. Sometimes this pattern feels like an arbitrary -division of code where it feels more natural for `OrchestratingService` and `LowerLevelService` to be -the same class. Need to be careful to put transactional code in the right place. - -#### Self-Reference Code Solution -``` -@Service -public class SelfReferencingService { - @Autowired SelfReferencingService self; - - public void complexCode() { - self.doTransactionOne(); - - // code between transactions - - self.doTransactionTwo(); - } - - @Transactional - public void doTransactionOne() {...} - - @Transactional - public void doTransactionTwo() {...} -} -``` -This code keeps one class but is written so Spring injects a self reference. `this` is different from `self`, -the former is naked and the latter is clothed in a Proxy. But it is easy to lose track of why one should -use `this` vs. `self` (not everyone is going to read this). - -# Transaction Barrier in the Service Layer -This all boils down to transactions are important and have pitfalls. One thought is to put the transaction -boundary at the DAO layer since they should be self-contained. But this can lead to packing too much -business logic in DAOs or DAOs that span subject areas. This can be ok for cases where the -state being stored is simple. More complicated state argues for transactions in the Service Layer. -But then care is required to structure Service classes so that it is easy to do the right thing. - -# Transactions with Terra Common Library (TCL) -TCL provides 2 handy annotations `@ReadTransaction` and `@WriteTransaction`. These go above -and beyond the Spring provided `@Transactional` annotation by setting the isolation level -to `SERIALIZABLE` and adding appropriate retries. This is tuned for Postgres. \ No newline at end of file diff --git a/service/src/main/java/bio/terra/javatemplate/App.java b/service/src/main/java/bio/terra/javatemplate/App.java index 3692ba19..6463e92f 100644 --- a/service/src/main/java/bio/terra/javatemplate/App.java +++ b/service/src/main/java/bio/terra/javatemplate/App.java @@ -19,14 +19,8 @@ @SpringBootApplication( scanBasePackages = { - // Scan for iam components & configs - "bio.terra.common.iam", // Scan for logging-related components & configs "bio.terra.common.logging", - // Scan for Liquibase migration components & configs - "bio.terra.common.migrate", - // Transaction management and DB retry configuration - "bio.terra.common.retry.transaction", // Scan for tracing-related components & configs "bio.terra.common.tracing", // Scan all service-specific packages beneath the current package @@ -41,10 +35,8 @@ public static void main(String[] args) { new SpringApplicationBuilder(App.class).initializers(new LoggingInitializer()).run(args); } - private final DataSource dataSource; + public App() { - public App(DataSource dataSource) { - this.dataSource = dataSource; } @Bean("objectMapper") @@ -55,11 +47,4 @@ public ObjectMapper objectMapper() { .registerModule(new JavaTimeModule()) .setDefaultPropertyInclusion(JsonInclude.Include.NON_ABSENT); } - - // This bean plus the @EnableTransactionManagement annotation above enables the use of the - // @Transaction annotation to control the transaction properties of the data source. - @Bean("transactionManager") - public PlatformTransactionManager getTransactionManager() { - return new JdbcTransactionManager(this.dataSource); - } } diff --git a/service/src/main/java/bio/terra/javatemplate/controller/ExampleController.java b/service/src/main/java/bio/terra/javatemplate/controller/ExampleController.java deleted file mode 100644 index 98ad7add..00000000 --- a/service/src/main/java/bio/terra/javatemplate/controller/ExampleController.java +++ /dev/null @@ -1,81 +0,0 @@ -package bio.terra.javatemplate.controller; - -import bio.terra.common.iam.BearerTokenFactory; -import bio.terra.common.iam.SamUser; -import bio.terra.common.iam.SamUserFactory; -import bio.terra.javatemplate.api.ExampleApi; -import bio.terra.javatemplate.config.SamConfiguration; -import bio.terra.javatemplate.iam.SamService; -import bio.terra.javatemplate.model.Example; -import bio.terra.javatemplate.service.ExampleService; -import io.micrometer.core.instrument.Metrics; -import io.micrometer.core.instrument.Tag; -import java.util.List; -import javax.servlet.http.HttpServletRequest; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; - -@Controller -public class ExampleController implements ExampleApi { - - public static final String EXAMPLE_COUNTER_TAG = "tag"; - public static final String EXAMPLE_COUNTER_NAME = "example.counter"; - - private final ExampleService exampleService; - private final BearerTokenFactory bearerTokenFactory; - private final SamUserFactory samUserFactory; - private final SamConfiguration samConfiguration; - private final HttpServletRequest request; - - private final SamService samService; - - public ExampleController( - ExampleService exampleService, - BearerTokenFactory bearerTokenFactory, - SamUserFactory samUserFactory, - SamConfiguration samConfiguration, - HttpServletRequest request, - SamService samService) { - this.exampleService = exampleService; - this.bearerTokenFactory = bearerTokenFactory; - this.samUserFactory = samUserFactory; - this.samConfiguration = samConfiguration; - this.request = request; - this.samService = samService; - } - - private SamUser getUser() { - // this automatically checks if the user is enabled - return this.samUserFactory.from(request, samConfiguration.basePath()); - } - - /** Example of getting user information from sam. */ - @Override - public ResponseEntity getMessage() { - var user = getUser(); - return ResponseEntity.of( - this.exampleService.getExampleForUser(user.getSubjectId()).map(Example::message)); - } - - @Override - public ResponseEntity setMessage(String body) { - var user = getUser(); - this.exampleService.saveExample(new Example(user.getSubjectId(), body)); - return ResponseEntity.noContent().build(); - } - - /** Example of getting the bearer token and using it to make a Sam (or other service) api call */ - @Override - public ResponseEntity getAction(String resourceType, String resourceId, String action) { - var bearerToken = bearerTokenFactory.from(request); - return ResponseEntity.ok(samService.getAction(resourceType, resourceId, action, bearerToken)); - } - - @Override - public ResponseEntity incrementCounter(String tag) { - Metrics.globalRegistry - .counter(EXAMPLE_COUNTER_NAME, List.of(Tag.of(EXAMPLE_COUNTER_TAG, tag))) - .increment(); - return ResponseEntity.noContent().build(); - } -} diff --git a/service/src/main/java/bio/terra/javatemplate/controller/PublicApiController.java b/service/src/main/java/bio/terra/javatemplate/controller/PublicApiController.java deleted file mode 100644 index a59cf846..00000000 --- a/service/src/main/java/bio/terra/javatemplate/controller/PublicApiController.java +++ /dev/null @@ -1,53 +0,0 @@ -package bio.terra.javatemplate.controller; - -import bio.terra.javatemplate.api.PublicApi; -import bio.terra.javatemplate.config.VersionConfiguration; -import bio.terra.javatemplate.model.SystemStatus; -import bio.terra.javatemplate.model.VersionProperties; -import bio.terra.javatemplate.service.StatusService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; - -@Controller -public class PublicApiController implements PublicApi { - private final StatusService statusService; - private final VersionConfiguration versionConfiguration; - - @Autowired - public PublicApiController( - StatusService statusService, VersionConfiguration versionConfiguration) { - this.statusService = statusService; - this.versionConfiguration = versionConfiguration; - } - - @Override - public ResponseEntity getStatus() { - SystemStatus systemStatus = statusService.getCurrentStatus(); - HttpStatus httpStatus = systemStatus.isOk() ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE; - return new ResponseEntity<>(systemStatus, httpStatus); - } - - @Override - public ResponseEntity getVersion() { - VersionProperties currentVersion = - new VersionProperties() - .gitTag(versionConfiguration.gitTag()) - .gitHash(versionConfiguration.gitHash()) - .github(versionConfiguration.github()) - .build(versionConfiguration.build()); - return ResponseEntity.ok(currentVersion); - } - - @GetMapping(value = "/") - public String index() { - return "redirect:swagger-ui.html"; - } - - @GetMapping(value = "/swagger-ui.html") - public String getSwagger() { - return "index"; - } -} diff --git a/service/src/main/java/bio/terra/javatemplate/dao/ExampleDao.java b/service/src/main/java/bio/terra/javatemplate/dao/ExampleDao.java deleted file mode 100644 index c5de0a4c..00000000 --- a/service/src/main/java/bio/terra/javatemplate/dao/ExampleDao.java +++ /dev/null @@ -1,48 +0,0 @@ -package bio.terra.javatemplate.dao; - -import bio.terra.javatemplate.model.Example; -import io.opencensus.contrib.spring.aop.Traced; -import java.util.Optional; -import org.springframework.dao.support.DataAccessUtils; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.stereotype.Repository; - -@Repository -public class ExampleDao { - private static final RowMapper EXAMPLE_ROW_MAPPER = - (rs, rowNum) -> - new Example(rs.getLong("id"), rs.getString("user_id"), rs.getString("message")); - - private final NamedParameterJdbcTemplate jdbcTemplate; - - public ExampleDao(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - @Traced - public void upsertExample(Example example) { - var query = - "INSERT INTO example (user_id, message)" - + " VALUES (:userId, :message)" - + " ON CONFLICT (user_id) DO UPDATE SET" - + " message = excluded.message"; - - var namedParameters = - new MapSqlParameterSource() - .addValue("userId", example.userId()) - .addValue("message", example.message()); - - jdbcTemplate.update(query, namedParameters); - } - - @Traced - public Optional getExampleForUser(String userId) { - var namedParameters = new MapSqlParameterSource().addValue("userId", userId); - var selectSql = "SELECT * FROM example WHERE user_id = :userId"; - return Optional.ofNullable( - DataAccessUtils.singleResult( - jdbcTemplate.query(selectSql, namedParameters, EXAMPLE_ROW_MAPPER))); - } -} diff --git a/service/src/main/java/bio/terra/javatemplate/iam/SamClient.java b/service/src/main/java/bio/terra/javatemplate/iam/SamClient.java deleted file mode 100644 index da130afd..00000000 --- a/service/src/main/java/bio/terra/javatemplate/iam/SamClient.java +++ /dev/null @@ -1,49 +0,0 @@ -package bio.terra.javatemplate.iam; - -import bio.terra.common.tracing.OkHttpClientTracingInterceptor; -import bio.terra.javatemplate.config.SamConfiguration; -import io.opencensus.trace.Tracing; -import okhttp3.OkHttpClient; -import org.broadinstitute.dsde.workbench.client.sam.ApiClient; -import org.broadinstitute.dsde.workbench.client.sam.api.ResourcesApi; -import org.broadinstitute.dsde.workbench.client.sam.api.StatusApi; -import org.broadinstitute.dsde.workbench.client.sam.api.UsersApi; -import org.springframework.stereotype.Component; - -@Component -public class SamClient { - private final SamConfiguration samConfig; - private final OkHttpClient okHttpClient; - - public SamClient(SamConfiguration samConfig) { - this.samConfig = samConfig; - this.okHttpClient = new ApiClient().getHttpClient(); - } - - private ApiClient getApiClient(String accessToken) { - ApiClient apiClient = getApiClient(); - apiClient.setAccessToken(accessToken); - return apiClient; - } - - private ApiClient getApiClient() { - var okHttpClientWithTracing = - this.okHttpClient - .newBuilder() - .addInterceptor(new OkHttpClientTracingInterceptor(Tracing.getTracer())) - .build(); - return new ApiClient().setHttpClient(okHttpClientWithTracing).setBasePath(samConfig.basePath()); - } - - UsersApi usersApi(String accessToken) { - return new UsersApi(getApiClient(accessToken)); - } - - ResourcesApi resourcesApi(String accessToken) { - return new ResourcesApi(getApiClient(accessToken)); - } - - StatusApi statusApi() { - return new StatusApi(getApiClient()); - } -} diff --git a/service/src/main/java/bio/terra/javatemplate/iam/SamService.java b/service/src/main/java/bio/terra/javatemplate/iam/SamService.java deleted file mode 100644 index 0ac37f0c..00000000 --- a/service/src/main/java/bio/terra/javatemplate/iam/SamService.java +++ /dev/null @@ -1,61 +0,0 @@ -package bio.terra.javatemplate.iam; - -import bio.terra.common.iam.BearerToken; -import bio.terra.common.sam.SamRetry; -import bio.terra.common.sam.exception.SamExceptionFactory; -import bio.terra.javatemplate.model.SystemStatusSystems; -import java.util.List; -import org.broadinstitute.dsde.workbench.client.sam.ApiException; -import org.broadinstitute.dsde.workbench.client.sam.model.SystemStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class SamService { - private static final Logger logger = LoggerFactory.getLogger(SamService.class); - private final SamClient samClient; - - @Autowired - public SamService(SamClient samClient) { - this.samClient = samClient; - } - - public boolean getAction( - String resourceType, String resourceId, String action, BearerToken bearerToken) { - try { - return SamRetry.retry( - () -> - samClient - .resourcesApi(bearerToken.getToken()) - .resourcePermissionV2(resourceType, resourceId, action)); - } catch (ApiException e) { - throw SamExceptionFactory.create(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw SamExceptionFactory.create("Sam retry interrupted", e); - } - } - - public SystemStatusSystems status() { - // No access token needed since this is an unauthenticated API. - try { - // Don't retry status check - SystemStatus samStatus = samClient.statusApi().getSystemStatus(); - var result = new SystemStatusSystems().ok(samStatus.getOk()); - var samSystems = samStatus.getSystems(); - // Populate error message if Sam status is non-ok - if (result.isOk() == null || !result.isOk()) { - String errorMsg = "Sam status check failed. Messages = " + samSystems; - logger.error(errorMsg); - result.addMessagesItem(errorMsg); - } - return result; - } catch (Exception e) { - String errorMsg = "Sam status check failed"; - logger.error(errorMsg, e); - return new SystemStatusSystems().ok(false).messages(List.of(errorMsg)); - } - } -} diff --git a/service/src/main/java/bio/terra/javatemplate/service/ExampleService.java b/service/src/main/java/bio/terra/javatemplate/service/ExampleService.java deleted file mode 100644 index 5dad6530..00000000 --- a/service/src/main/java/bio/terra/javatemplate/service/ExampleService.java +++ /dev/null @@ -1,28 +0,0 @@ -package bio.terra.javatemplate.service; - -import bio.terra.common.db.ReadTransaction; -import bio.terra.common.db.WriteTransaction; -import bio.terra.javatemplate.dao.ExampleDao; -import bio.terra.javatemplate.model.Example; -import java.util.Optional; -import org.springframework.stereotype.Service; - -@Service -public class ExampleService { - private final ExampleDao exampleDao; - - public ExampleService(ExampleDao exampleDao) { - this.exampleDao = exampleDao; - } - - // README docs/transactions.md - @WriteTransaction - public void saveExample(Example example) { - exampleDao.upsertExample(example); - } - - @ReadTransaction - public Optional getExampleForUser(String userId) { - return exampleDao.getExampleForUser(userId); - } -} diff --git a/service/src/main/java/bio/terra/javatemplate/service/StatusService.java b/service/src/main/java/bio/terra/javatemplate/service/StatusService.java deleted file mode 100644 index 846fe10e..00000000 --- a/service/src/main/java/bio/terra/javatemplate/service/StatusService.java +++ /dev/null @@ -1,41 +0,0 @@ -package bio.terra.javatemplate.service; - -import bio.terra.javatemplate.config.StatusCheckConfiguration; -import bio.terra.javatemplate.iam.SamService; -import bio.terra.javatemplate.model.SystemStatusSystems; -import java.sql.Connection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.stereotype.Service; - -@Service -public class StatusService extends BaseStatusService { - private static final Logger logger = LoggerFactory.getLogger(StatusService.class); - - private final NamedParameterJdbcTemplate jdbcTemplate; - - @Autowired - public StatusService( - NamedParameterJdbcTemplate jdbcTemplate, - StatusCheckConfiguration configuration, - SamService samService) { - super(configuration); - this.jdbcTemplate = jdbcTemplate; - registerStatusCheck("CloudSQL", this::databaseStatus); - registerStatusCheck("Sam", samService::status); - } - - private SystemStatusSystems databaseStatus() { - try { - logger.debug("Checking database connection valid"); - return new SystemStatusSystems() - .ok(jdbcTemplate.getJdbcTemplate().execute((Connection conn) -> conn.isValid(5000))); - } catch (Exception ex) { - String errorMsg = "Database status check failed"; - logger.error(errorMsg, ex); - return new SystemStatusSystems().ok(false).addMessagesItem(errorMsg + ": " + ex.getMessage()); - } - } -} diff --git a/service/src/test/java/bio/terra/javatemplate/api/ExampleControllerTest.java b/service/src/test/java/bio/terra/javatemplate/api/ExampleControllerTest.java deleted file mode 100644 index ce0d2d4a..00000000 --- a/service/src/test/java/bio/terra/javatemplate/api/ExampleControllerTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package bio.terra.javatemplate.api; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import bio.terra.common.iam.BearerToken; -import bio.terra.common.iam.BearerTokenFactory; -import bio.terra.common.iam.SamUser; -import bio.terra.common.iam.SamUserFactory; -import bio.terra.javatemplate.config.SamConfiguration; -import bio.terra.javatemplate.controller.ExampleController; -import bio.terra.javatemplate.iam.SamService; -import bio.terra.javatemplate.model.Example; -import bio.terra.javatemplate.service.ExampleService; -import io.micrometer.core.instrument.Metrics; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import java.util.Optional; -import java.util.UUID; -import javax.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.servlet.MockMvc; - -@ContextConfiguration(classes = ExampleController.class) -@WebMvcTest -public class ExampleControllerTest { - @MockBean ExampleService serviceMock; - @MockBean SamUserFactory samUserFactoryMock; - @MockBean BearerTokenFactory bearerTokenFactory; - @MockBean SamConfiguration samConfiguration; - @MockBean SamService samService; - - @Autowired private MockMvc mockMvc; - - private SamUser testUser = - new SamUser( - "test@email", - UUID.randomUUID().toString(), - new BearerToken(UUID.randomUUID().toString())); - - @BeforeEach - void beforeEach() { - when(samUserFactoryMock.from(any(HttpServletRequest.class), any())).thenReturn(testUser); - } - - @Test - void testGetMessageOk() throws Exception { - var example = new Example(testUser.getSubjectId(), "message"); - when(serviceMock.getExampleForUser(testUser.getSubjectId())).thenReturn(Optional.of(example)); - - mockMvc - .perform(get("/api/example/v1/message")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().string(example.message())); - } - - @Test - void testGetMessageNotFound() throws Exception { - when(serviceMock.getExampleForUser(testUser.getSubjectId())).thenReturn(Optional.empty()); - - mockMvc.perform(get("/api/example/v1/message")).andExpect(status().isNotFound()); - } - - @Test - void testIncrementCounter() throws Exception { - var meterRegistry = new SimpleMeterRegistry(); - Metrics.globalRegistry.add(meterRegistry); - - try { - final String tagValue = "tag_value"; - mockMvc - .perform( - post("/api/example/v1/counter") - .contentType(MediaType.APPLICATION_JSON) - .content(tagValue)) - .andExpect(status().isNoContent()); - - var counter = - meterRegistry - .find(ExampleController.EXAMPLE_COUNTER_NAME) - .tags(ExampleController.EXAMPLE_COUNTER_TAG, tagValue) - .counter(); - - assertNotNull(counter); - assertEquals(counter.count(), 1); - - } finally { - Metrics.globalRegistry.remove(meterRegistry); - } - } -} diff --git a/service/src/test/java/bio/terra/javatemplate/api/PublicApiControllerTest.java b/service/src/test/java/bio/terra/javatemplate/api/PublicApiControllerTest.java deleted file mode 100644 index 71554ff7..00000000 --- a/service/src/test/java/bio/terra/javatemplate/api/PublicApiControllerTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package bio.terra.javatemplate.api; - -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import bio.terra.javatemplate.config.VersionConfiguration; -import bio.terra.javatemplate.controller.PublicApiController; -import bio.terra.javatemplate.model.SystemStatus; -import bio.terra.javatemplate.service.StatusService; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.servlet.MockMvc; - -@ContextConfiguration(classes = PublicApiController.class) -@WebMvcTest -class PublicApiControllerTest { - - @Autowired private MockMvc mockMvc; - - @MockBean private StatusService statusService; - - @MockBean private VersionConfiguration versionConfiguration; - - @Test - void testStatus() throws Exception { - SystemStatus systemStatus = new SystemStatus().ok(true); - when(statusService.getCurrentStatus()).thenReturn(systemStatus); - this.mockMvc.perform(get("/status")).andExpect(status().isOk()); - } - - @Test - void testStatusCheckFails() throws Exception { - SystemStatus systemStatus = new SystemStatus().ok(false); - when(statusService.getCurrentStatus()).thenReturn(systemStatus); - this.mockMvc.perform(get("/status")).andExpect(status().is5xxServerError()); - } - - @Test - void testVersion() throws Exception { - String gitTag = "0.1.0"; - String gitHash = "abc1234"; - String github = "https://github.com/DataBiosphere/terra-java-project-template/tree/0.9.0"; - String build = "0.1.0"; - - when(versionConfiguration.gitTag()).thenReturn(gitTag); - when(versionConfiguration.gitHash()).thenReturn(gitHash); - when(versionConfiguration.github()).thenReturn(github); - when(versionConfiguration.build()).thenReturn(build); - - this.mockMvc - .perform(get("/version")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.gitTag").value(gitTag)) - .andExpect(jsonPath("$.gitHash").value(gitHash)) - .andExpect(jsonPath("$.github").value(github)) - .andExpect(jsonPath("$.build").value(build)); - } - - @Test - void testGetSwagger() throws Exception { - this.mockMvc.perform(get("/swagger-ui.html")).andExpect(status().isOk()); - } - - @Test - void testIndex() throws Exception { - this.mockMvc.perform(get("/")).andExpect(redirectedUrl("swagger-ui.html")); - } -} diff --git a/service/src/test/java/bio/terra/javatemplate/dao/BaseDaoTest.java b/service/src/test/java/bio/terra/javatemplate/dao/BaseDaoTest.java deleted file mode 100644 index 90552465..00000000 --- a/service/src/test/java/bio/terra/javatemplate/dao/BaseDaoTest.java +++ /dev/null @@ -1,9 +0,0 @@ -package bio.terra.javatemplate.dao; - -import bio.terra.javatemplate.BaseSpringBootTest; -import org.springframework.test.annotation.Rollback; -import org.springframework.transaction.annotation.Transactional; - -@Transactional -@Rollback -public abstract class BaseDaoTest extends BaseSpringBootTest {} diff --git a/service/src/test/java/bio/terra/javatemplate/dao/ExampleDaoTest.java b/service/src/test/java/bio/terra/javatemplate/dao/ExampleDaoTest.java deleted file mode 100644 index 9a39b75b..00000000 --- a/service/src/test/java/bio/terra/javatemplate/dao/ExampleDaoTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package bio.terra.javatemplate.dao; - -import static org.junit.jupiter.api.Assertions.*; - -import bio.terra.javatemplate.model.Example; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -public class ExampleDaoTest extends BaseDaoTest { - @Autowired ExampleDao exampleDao; - - @Test - void testDifferentUserUpsert() { - var user1 = new Example("user1", "user1 message"); - exampleDao.upsertExample(user1); - var user1Actual = exampleDao.getExampleForUser(user1.userId()); - - var user2 = new Example("user2", "user2 message"); - exampleDao.upsertExample(user2); - var user2Actual = exampleDao.getExampleForUser(user2.userId()); - - assertTrue(user1Actual.isPresent()); - assertEquals(user1.userId(), user1Actual.get().userId()); - assertEquals(user1.message(), user1Actual.get().message()); - - assertTrue(user2Actual.isPresent()); - assertEquals(user2.userId(), user2Actual.get().userId()); - assertEquals(user2.message(), user2Actual.get().message()); - } - - @Test - void testRepeatedUpsert() { - var example1 = new Example("testUser", "testMessage"); - exampleDao.upsertExample(example1); - var firstSave = exampleDao.getExampleForUser(example1.userId()); - assertTrue(firstSave.isPresent()); - assertEquals(example1.userId(), firstSave.get().userId()); - assertEquals(example1.message(), firstSave.get().message()); - - var example2 = new Example(example1.userId(), "differentMessage"); - exampleDao.upsertExample(example2); - var secondSave = exampleDao.getExampleForUser(example1.userId()); - - assertTrue(secondSave.isPresent()); - assertEquals(firstSave.get().id(), secondSave.get().id()); - assertEquals(example2.userId(), secondSave.get().userId()); - assertEquals(example2.message(), secondSave.get().message()); - } - - @Test - void testNotFound() { - assertTrue(exampleDao.getExampleForUser("testUser").isEmpty()); - } -} diff --git a/service/src/test/java/bio/terra/javatemplate/iam/SamClientTest.java b/service/src/test/java/bio/terra/javatemplate/iam/SamClientTest.java deleted file mode 100644 index 6c2467c6..00000000 --- a/service/src/test/java/bio/terra/javatemplate/iam/SamClientTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package bio.terra.javatemplate.iam; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -import bio.terra.javatemplate.config.SamConfiguration; -import org.broadinstitute.dsde.workbench.client.sam.ApiClient; -import org.broadinstitute.dsde.workbench.client.sam.auth.OAuth; -import org.junit.jupiter.api.Test; - -class SamClientTest { - private static final String BASE_PATH = "basepath"; - private static final String TOKEN = "token"; - private static final String AUTH_NAME = "b2coauth"; - - private final SamClient client = new SamClient(new SamConfiguration(BASE_PATH)); - - @Test - void testApis() { - validateClient(client.statusApi().getApiClient(), null); - validateClient(client.usersApi(TOKEN).getApiClient(), TOKEN); - validateClient(client.resourcesApi(TOKEN).getApiClient(), TOKEN); - } - - private static void validateClient(ApiClient client, String token) { - assertThat(client.getBasePath(), is(BASE_PATH)); - OAuth oauth = (OAuth) client.getAuthentication(AUTH_NAME); - assertThat(oauth.getAccessToken(), is(token)); - } -} diff --git a/service/src/test/java/bio/terra/javatemplate/iam/SamServiceTest.java b/service/src/test/java/bio/terra/javatemplate/iam/SamServiceTest.java deleted file mode 100644 index fc247907..00000000 --- a/service/src/test/java/bio/terra/javatemplate/iam/SamServiceTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package bio.terra.javatemplate.iam; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; - -import bio.terra.javatemplate.BaseSpringBootTest; -import org.broadinstitute.dsde.workbench.client.sam.ApiException; -import org.broadinstitute.dsde.workbench.client.sam.api.StatusApi; -import org.broadinstitute.dsde.workbench.client.sam.model.SystemStatus; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; - -@ExtendWith(MockitoExtension.class) -class SamServiceTest extends BaseSpringBootTest { - - @MockBean private SamClient samClient; - @MockBean private StatusApi statusApi; - - @Autowired private SamService samService; - - private void mockStatus() { - when(samClient.statusApi()).thenReturn(statusApi); - } - - @Test - void status() throws Exception { - mockStatus(); - SystemStatus status = new SystemStatus().ok(true); - when(statusApi.getSystemStatus()).thenReturn(status); - var samStatus = samService.status(); - assertTrue(samStatus.isOk()); - } - - @Test - void statusDown() throws Exception { - mockStatus(); - SystemStatus status = new SystemStatus().ok(false); - when(statusApi.getSystemStatus()).thenReturn(status); - var samStatus = samService.status(); - assertFalse(samStatus.isOk()); - } - - @Test - void statusException() throws Exception { - mockStatus(); - when(statusApi.getSystemStatus()).thenThrow(new ApiException()); - var samStatus = samService.status(); - assertFalse(samStatus.isOk()); - } -} From 7194f92f5fd0bc68ea69615075a33b7af726b808 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 20 Jun 2023 15:34:13 -0400 Subject: [PATCH 03/46] rename javatemplate to javapfb remove .idea files --- .github/workflows/publish.yml | 6 +-- .gitignore | 38 +++++++++++++++++++ .../bio.terra.java-spring-conventions.gradle | 1 + client/artifactory.gradle | 6 +-- client/swagger.gradle | 2 +- common/postgres-init.sql | 2 - ...templateClient.java => JavaPfbClient.java} | 8 ++-- .../java/scripts/testscripts/GetStatus.java | 6 +-- .../java/scripts/testscripts/GetVersion.java | 6 +-- service/build.gradle | 22 +++++------ service/generators.gradle | 10 ++--- service/publishing.gradle | 4 +- .../terra/{javatemplate => javapfb}/App.java | 6 +-- .../config/StatusCheckConfiguration.java | 4 +- .../config/VersionConfiguration.java | 4 +- .../controller/GlobalExceptionHandler.java | 4 +- .../model/Example.java | 2 +- .../service/BaseStatusService.java | 8 ++-- .../javatemplate/config/SamConfiguration.java | 6 --- service/src/main/resources/application.yml | 19 +++++----- .../BaseSpringBootTest.java | 2 +- .../service/BaseStatusServiceTest.java | 12 +++--- settings.gradle | 2 +- 23 files changed, 107 insertions(+), 73 deletions(-) create mode 100644 .gitignore delete mode 100644 common/postgres-init.sql rename integration/src/main/java/scripts/client/{JavatemplateClient.java => JavaPfbClient.java} (86%) rename service/src/main/java/bio/terra/{javatemplate => javapfb}/App.java (94%) rename service/src/main/java/bio/terra/{javatemplate => javapfb}/config/StatusCheckConfiguration.java (69%) rename service/src/main/java/bio/terra/{javatemplate => javapfb}/config/VersionConfiguration.java (73%) rename service/src/main/java/bio/terra/{javatemplate => javapfb}/controller/GlobalExceptionHandler.java (85%) rename service/src/main/java/bio/terra/{javatemplate => javapfb}/model/Example.java (89%) rename service/src/main/java/bio/terra/{javatemplate => javapfb}/service/BaseStatusService.java (93%) delete mode 100644 service/src/main/java/bio/terra/javatemplate/config/SamConfiguration.java rename service/src/test/java/bio/terra/{javatemplate => javapfb}/BaseSpringBootTest.java (87%) rename service/src/test/java/bio/terra/{javatemplate => javapfb}/service/BaseStatusServiceTest.java (63%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 301acc9e..8b3a8648 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -76,7 +76,7 @@ jobs: # # Hey! You'll probably want to adjust below this point: it deploys newly published versions to dev! # - # You'll need to create a new chart entry in Beehive first, at https://broad.io/beehive/charts/new (chart + # You'll need to create a new chart entry in Beehive first, at https://broad.io/beehive/charts/new (chart # names can't be changed, so be sure beforehand). Replace 'javatemplate' below with whatever name you choose. # # You'll also need to add some access to your new repo to allow it to run these steps. We have docs on the @@ -92,7 +92,7 @@ jobs: needs: publish-job with: new-version: ${{ needs.publish-job.outputs.tag }} - chart-name: 'javatemplate' + chart-name: 'javapfb' permissions: contents: 'read' id-token: 'write' @@ -103,7 +103,7 @@ jobs: needs: [publish-job, report-to-sherlock] with: new-version: ${{ needs.publish-job.outputs.tag }} - chart-name: 'javatemplate' + chart-name: 'javapfb' environment-name: 'template-services' secrets: sync-git-token: ${{ secrets.BROADBOT_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3daa5166 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +bootrun.log + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +# Emacs backup files # +*.*~ + +### IntelliJ IDEA ### +.idea/ +*.iml + +### VS Code ### +.vscode/ + +# Mac directory metadata +.DS_Store + +# PyEnv environment files +.env/ + +# Ignore generated credentials from google-github-actions/auth +gha-creds-*.json \ No newline at end of file diff --git a/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle index 3bc0e9c7..df2a4be5 100644 --- a/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle +++ b/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle @@ -2,5 +2,6 @@ plugins { // Apply the common convention plugin for shared build configuration between library and application projects. id 'bio.terra.java-common-conventions' + id 'io.spring.dependency-management' id 'org.springframework.boot' } diff --git a/client/artifactory.gradle b/client/artifactory.gradle index 6c0338a6..008de4d5 100644 --- a/client/artifactory.gradle +++ b/client/artifactory.gradle @@ -18,8 +18,8 @@ java { publishing { publications { - javatemplateClientLibrary(MavenPublication) { - artifactId = "javatemplate-client" + javaPfbClientLibrary(MavenPublication) { + artifactId = "javapfb-client" from components.java versionMapping { usage("java-runtime") { @@ -41,7 +41,7 @@ artifactory { defaults { // This is how we tell the Artifactory Plugin which artifacts should be published to Artifactory. // Reference to Gradle publications defined in the build script. - publications("javatemplateClientLibrary") + publications("javapfbClientLibrary") publishArtifacts = true publishPom = true } diff --git a/client/swagger.gradle b/client/swagger.gradle index b46dc5bc..fe55676f 100644 --- a/client/swagger.gradle +++ b/client/swagger.gradle @@ -9,7 +9,7 @@ dependencies { swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli' } -def artifactGroup = "${group}.javatemplate" +def artifactGroup = "${group}.javapfb" generateSwaggerCode { inputFile = file('../service/src/main/resources/api/openapi.yml') diff --git a/common/postgres-init.sql b/common/postgres-init.sql deleted file mode 100644 index e49ff37a..00000000 --- a/common/postgres-init.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE ROLE dbuser WITH LOGIN ENCRYPTED PASSWORD 'dbpwd'; -CREATE DATABASE javatemplate_db OWNER dbuser; diff --git a/integration/src/main/java/scripts/client/JavatemplateClient.java b/integration/src/main/java/scripts/client/JavaPfbClient.java similarity index 86% rename from integration/src/main/java/scripts/client/JavatemplateClient.java rename to integration/src/main/java/scripts/client/JavaPfbClient.java index 3c640156..ee2625f6 100644 --- a/integration/src/main/java/scripts/client/JavatemplateClient.java +++ b/integration/src/main/java/scripts/client/JavaPfbClient.java @@ -1,6 +1,6 @@ package scripts.client; -import bio.terra.javatemplate.client.ApiClient; +import bio.terra.javapfb.client.ApiClient; import bio.terra.testrunner.common.utils.AuthenticationUtils; import bio.terra.testrunner.runner.config.ServerSpecification; import bio.terra.testrunner.runner.config.TestUserSpecification; @@ -8,7 +8,7 @@ import java.io.IOException; import java.util.Objects; -public class JavatemplateClient extends ApiClient { +public class JavaPfbClient extends ApiClient { /** * Build a no-auth API client object for the service. No access token is needed for this API @@ -16,7 +16,7 @@ public class JavatemplateClient extends ApiClient { * * @param server the server we are testing against */ - public JavatemplateClient(ServerSpecification server) throws IOException { + public JavaPfbClient(ServerSpecification server) throws IOException { this(server, null); } @@ -28,7 +28,7 @@ public JavatemplateClient(ServerSpecification server) throws IOException { * @param server the server we are testing against * @param testUser the test user whose credentials are supplied to the API client object */ - public JavatemplateClient(ServerSpecification server, TestUserSpecification testUser) + public JavaPfbClient(ServerSpecification server, TestUserSpecification testUser) throws IOException { // note that this uses server.catalogUri. Typically a uri for a new service needs to be added to // https://github.com/DataBiosphere/terra-test-runner/blob/main/src/main/java/bio/terra/testrunner/runner/config/ServerSpecification.java diff --git a/integration/src/main/java/scripts/testscripts/GetStatus.java b/integration/src/main/java/scripts/testscripts/GetStatus.java index 1e3154b2..018da00f 100644 --- a/integration/src/main/java/scripts/testscripts/GetStatus.java +++ b/integration/src/main/java/scripts/testscripts/GetStatus.java @@ -3,16 +3,16 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import bio.terra.javatemplate.api.PublicApi; +import bio.terra.javapfb.api.PublicApi; import bio.terra.testrunner.runner.TestScript; import bio.terra.testrunner.runner.config.TestUserSpecification; import com.google.api.client.http.HttpStatusCodes; -import scripts.client.JavatemplateClient; +import scripts.client.JavaPfbClient; public class GetStatus extends TestScript { @Override public void userJourney(TestUserSpecification testUser) throws Exception { - var client = new JavatemplateClient(server); + var client = new JavaPfbClient(server); var publicApi = new PublicApi(client); publicApi.getStatus(); assertThat(client.getStatusCode(), is(HttpStatusCodes.STATUS_CODE_OK)); diff --git a/integration/src/main/java/scripts/testscripts/GetVersion.java b/integration/src/main/java/scripts/testscripts/GetVersion.java index 97cad602..e5948351 100644 --- a/integration/src/main/java/scripts/testscripts/GetVersion.java +++ b/integration/src/main/java/scripts/testscripts/GetVersion.java @@ -4,16 +4,16 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import bio.terra.javatemplate.api.PublicApi; +import bio.terra.javapfb.api.PublicApi; import bio.terra.testrunner.runner.TestScript; import bio.terra.testrunner.runner.config.TestUserSpecification; import com.google.api.client.http.HttpStatusCodes; -import scripts.client.JavatemplateClient; +import scripts.client.JavaPfbClient; public class GetVersion extends TestScript { @Override public void userJourney(TestUserSpecification testUser) throws Exception { - JavatemplateClient client = new JavatemplateClient(server); + JavaPfbClient client = new JavaPfbClient(server); var publicApi = new PublicApi(client); var versionProperties = publicApi.getVersion(); diff --git a/service/build.gradle b/service/build.gradle index 12c77cb4..caf1c936 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -51,14 +51,14 @@ sonarqube { } } -liquibase { - activities { - catalog { - changeLogFile 'src/main/resources/db/changelog.xml' - url 'jdbc:postgresql://localhost:5432/javatemplate_db' - username 'dbuser' - password 'dbpwd' - logLevel 'info' - } - } -} +//liquibase { +// activities { +// catalog { +// changeLogFile 'src/main/resources/db/changelog.xml' +// url 'jdbc:postgresql://localhost:5432/java_pfb_db' +// username 'dbuser' +// password 'dbpwd' +// logLevel 'info' +// } +// } +//} diff --git a/service/generators.gradle b/service/generators.gradle index e9df9fbf..1cc0681c 100644 --- a/service/generators.gradle +++ b/service/generators.gradle @@ -10,7 +10,7 @@ dependencies { annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' } -def artifactGroup = "${group}.javatemplate" +def artifactGroup = "${group}.javapfb" generateSwaggerCode { inputFile = file('src/main/resources/api/openapi.yml') @@ -37,8 +37,8 @@ compileJava.dependsOn generateSwaggerCode // see https://github.com/n0mer/gradle-git-properties gitProperties { keys = [] - customProperty('javatemplate.version.gitTag', { it.describe(tags: true) }) - customProperty('javatemplate.version.gitHash', { it.head().abbreviatedId }) - customProperty('javatemplate.version.github', { "https://github.com/DataBiosphere/terra-java-project-template/tree/${it.describe(tags: true)}" }) - customProperty('javatemplate.version.build', version) + customProperty('javapfb.version.gitTag', { it.describe(tags: true) }) + customProperty('javapfb.version.gitHash', { it.head().abbreviatedId }) + customProperty('javapfb.version.github', { "https://github.com/DataBiosphere/java-pfb/tree/${it.describe(tags: true)}" }) + customProperty('javapfb.version.build', version) } diff --git a/service/publishing.gradle b/service/publishing.gradle index 2564e39d..d104842a 100644 --- a/service/publishing.gradle +++ b/service/publishing.gradle @@ -28,10 +28,10 @@ jib { } container { filesModificationTime = ZonedDateTime.now().toString() // to prevent ui caching - mainClass = 'bio.terra.javatemplate.App' + mainClass = 'bio.terra.javapfb.App' jvmFlags = [ "-agentpath:" + cloudProfilerLocation + "/profiler_java_agent.so=" + - "-cprof_service=bio.terra.javatemplate" + + "-cprof_service=bio.terra.javapfb" + ",-cprof_service_version=" + version + ",-cprof_enable_heap_sampling=true" + ",-logtostderr" + diff --git a/service/src/main/java/bio/terra/javatemplate/App.java b/service/src/main/java/bio/terra/javapfb/App.java similarity index 94% rename from service/src/main/java/bio/terra/javatemplate/App.java rename to service/src/main/java/bio/terra/javapfb/App.java index 6463e92f..f0556b7a 100644 --- a/service/src/main/java/bio/terra/javatemplate/App.java +++ b/service/src/main/java/bio/terra/javapfb/App.java @@ -1,4 +1,4 @@ -package bio.terra.javatemplate; +package bio.terra.javapfb; import bio.terra.common.logging.LoggingInitializer; import com.fasterxml.jackson.annotation.JsonInclude; @@ -24,9 +24,9 @@ // Scan for tracing-related components & configs "bio.terra.common.tracing", // Scan all service-specific packages beneath the current package - "bio.terra.javatemplate" + "bio.terra.javapfb" }) -@ConfigurationPropertiesScan("bio.terra.javatemplate") +@ConfigurationPropertiesScan("bio.terra.javapfb") @EnableRetry @EnableTransactionManagement @EnableConfigurationProperties diff --git a/service/src/main/java/bio/terra/javatemplate/config/StatusCheckConfiguration.java b/service/src/main/java/bio/terra/javapfb/config/StatusCheckConfiguration.java similarity index 69% rename from service/src/main/java/bio/terra/javatemplate/config/StatusCheckConfiguration.java rename to service/src/main/java/bio/terra/javapfb/config/StatusCheckConfiguration.java index 4d2736a6..499a1550 100644 --- a/service/src/main/java/bio/terra/javatemplate/config/StatusCheckConfiguration.java +++ b/service/src/main/java/bio/terra/javapfb/config/StatusCheckConfiguration.java @@ -1,8 +1,8 @@ -package bio.terra.javatemplate.config; +package bio.terra.javapfb.config; import org.springframework.boot.context.properties.ConfigurationProperties; -@ConfigurationProperties(prefix = "javatemplate.status-check") +@ConfigurationProperties(prefix = "javapfb.status-check") public record StatusCheckConfiguration( boolean enabled, int pollingIntervalSeconds, diff --git a/service/src/main/java/bio/terra/javatemplate/config/VersionConfiguration.java b/service/src/main/java/bio/terra/javapfb/config/VersionConfiguration.java similarity index 73% rename from service/src/main/java/bio/terra/javatemplate/config/VersionConfiguration.java rename to service/src/main/java/bio/terra/javapfb/config/VersionConfiguration.java index 42d21680..b69a6a83 100644 --- a/service/src/main/java/bio/terra/javatemplate/config/VersionConfiguration.java +++ b/service/src/main/java/bio/terra/javapfb/config/VersionConfiguration.java @@ -1,7 +1,7 @@ -package bio.terra.javatemplate.config; +package bio.terra.javapfb.config; import org.springframework.boot.context.properties.ConfigurationProperties; /** Read from the git.properties file auto-generated at build time */ -@ConfigurationProperties("javatemplate.version") +@ConfigurationProperties("javapfb.version") public record VersionConfiguration(String gitHash, String gitTag, String build, String github) {} diff --git a/service/src/main/java/bio/terra/javatemplate/controller/GlobalExceptionHandler.java b/service/src/main/java/bio/terra/javapfb/controller/GlobalExceptionHandler.java similarity index 85% rename from service/src/main/java/bio/terra/javatemplate/controller/GlobalExceptionHandler.java rename to service/src/main/java/bio/terra/javapfb/controller/GlobalExceptionHandler.java index 15a84292..e9f02081 100644 --- a/service/src/main/java/bio/terra/javatemplate/controller/GlobalExceptionHandler.java +++ b/service/src/main/java/bio/terra/javapfb/controller/GlobalExceptionHandler.java @@ -1,7 +1,7 @@ -package bio.terra.javatemplate.controller; +package bio.terra.javapfb.controller; import bio.terra.common.exception.AbstractGlobalExceptionHandler; -import bio.terra.javatemplate.model.ErrorReport; +import bio.terra.javapfb.model.ErrorReport; import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; diff --git a/service/src/main/java/bio/terra/javatemplate/model/Example.java b/service/src/main/java/bio/terra/javapfb/model/Example.java similarity index 89% rename from service/src/main/java/bio/terra/javatemplate/model/Example.java rename to service/src/main/java/bio/terra/javapfb/model/Example.java index ef364ea9..0ac4c49f 100644 --- a/service/src/main/java/bio/terra/javatemplate/model/Example.java +++ b/service/src/main/java/bio/terra/javapfb/model/Example.java @@ -1,4 +1,4 @@ -package bio.terra.javatemplate.model; +package bio.terra.javapfb.model; import java.util.Objects; import javax.annotation.Nullable; diff --git a/service/src/main/java/bio/terra/javatemplate/service/BaseStatusService.java b/service/src/main/java/bio/terra/javapfb/service/BaseStatusService.java similarity index 93% rename from service/src/main/java/bio/terra/javatemplate/service/BaseStatusService.java rename to service/src/main/java/bio/terra/javapfb/service/BaseStatusService.java index 37f8370e..b8aa27df 100644 --- a/service/src/main/java/bio/terra/javatemplate/service/BaseStatusService.java +++ b/service/src/main/java/bio/terra/javapfb/service/BaseStatusService.java @@ -1,8 +1,8 @@ -package bio.terra.javatemplate.service; +package bio.terra.javapfb.service; -import bio.terra.javatemplate.config.StatusCheckConfiguration; -import bio.terra.javatemplate.model.SystemStatus; -import bio.terra.javatemplate.model.SystemStatusSystems; +import bio.terra.javapfb.config.StatusCheckConfiguration; +import bio.terra.javapfb.model.SystemStatus; +import bio.terra.javapfb.model.SystemStatusSystems; import com.google.common.annotations.VisibleForTesting; import java.time.Instant; import java.util.Map; diff --git a/service/src/main/java/bio/terra/javatemplate/config/SamConfiguration.java b/service/src/main/java/bio/terra/javatemplate/config/SamConfiguration.java deleted file mode 100644 index d6aa4d69..00000000 --- a/service/src/main/java/bio/terra/javatemplate/config/SamConfiguration.java +++ /dev/null @@ -1,6 +0,0 @@ -package bio.terra.javatemplate.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "javatemplate.sam") -public record SamConfiguration(String basePath) {} diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml index dac5984e..fecd196c 100644 --- a/service/src/main/resources/application.yml +++ b/service/src/main/resources/application.yml @@ -2,12 +2,13 @@ # This is for deployment-specific values, which may be managed by other teams env: - db: - host: ${DATABASE_HOSTNAME:127.0.0.1}:5432 - init: ${INIT_DB:false} - name: ${DATABASE_NAME:javatemplate_db} - password: ${DATABASE_USER_PASSWORD:dbpwd} - user: ${DATABASE_USER:dbuser} +# We shouldn't need a db for this client +# db: +# host: ${DATABASE_HOSTNAME:127.0.0.1}:5432 +# init: ${INIT_DB:false} +# name: ${DATABASE_NAME:java_pfb_db} +# password: ${DATABASE_USER_PASSWORD:dbpwd} +# user: ${DATABASE_USER:dbuser} tracing: exportEnabled: ${CLOUD_TRACE_ENABLED:false} samplingRate: ${SAMPLING_PROBABILITY:0} @@ -33,8 +34,8 @@ server: spring: # application name and version are used to populate the logging serviceContext # https://github.com/DataBiosphere/terra-common-lib/blob/480ab3daae282ddff0fef8dc329494a4422e32f1/src/main/java/bio/terra/common/logging/GoogleJsonLayout.java#L118 - application.name: javatemplate - application.version: ${javatemplate.version.gitHash:unknown} + application.name: javapfb + application.version: ${javapfb.version.gitHash:unknown} datasource: hikari: @@ -61,7 +62,7 @@ management: exposure: include: "*" -javatemplate: +javapfb: ingress: # Default value that's overridden by Helm. domainName: localhost:8080 diff --git a/service/src/test/java/bio/terra/javatemplate/BaseSpringBootTest.java b/service/src/test/java/bio/terra/javapfb/BaseSpringBootTest.java similarity index 87% rename from service/src/test/java/bio/terra/javatemplate/BaseSpringBootTest.java rename to service/src/test/java/bio/terra/javapfb/BaseSpringBootTest.java index 02fdfc18..1149e6d8 100644 --- a/service/src/test/java/bio/terra/javatemplate/BaseSpringBootTest.java +++ b/service/src/test/java/bio/terra/javapfb/BaseSpringBootTest.java @@ -1,4 +1,4 @@ -package bio.terra.javatemplate; +package bio.terra.javapfb; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; diff --git a/service/src/test/java/bio/terra/javatemplate/service/BaseStatusServiceTest.java b/service/src/test/java/bio/terra/javapfb/service/BaseStatusServiceTest.java similarity index 63% rename from service/src/test/java/bio/terra/javatemplate/service/BaseStatusServiceTest.java rename to service/src/test/java/bio/terra/javapfb/service/BaseStatusServiceTest.java index 26e0c5a4..41dc91b9 100644 --- a/service/src/test/java/bio/terra/javatemplate/service/BaseStatusServiceTest.java +++ b/service/src/test/java/bio/terra/javapfb/service/BaseStatusServiceTest.java @@ -1,12 +1,14 @@ -package bio.terra.javatemplate.service; +package bio.terra.javapfb.service; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import bio.terra.javatemplate.config.StatusCheckConfiguration; -import bio.terra.javatemplate.model.SystemStatus; -import bio.terra.javatemplate.model.SystemStatusSystems; +import bio.terra.javapfb.config.StatusCheckConfiguration; +import bio.terra.javapfb.model.SystemStatus; +import bio.terra.javapfb.model.SystemStatusSystems; import java.util.Map; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; class BaseStatusServiceTest { @@ -17,7 +19,7 @@ void getCurrentStatus() { BaseStatusService service = new BaseStatusService(config); var status = new SystemStatusSystems().ok(true); service.registerStatusCheck("test", () -> status); - assertThat(service.getCurrentStatus(), is(new SystemStatus().ok(false))); + MatcherAssert.assertThat(service.getCurrentStatus(), Matchers.is(new SystemStatus().ok(false))); service.checkStatus(); assertThat( service.getCurrentStatus(), diff --git a/settings.gradle b/settings.gradle index 0e0df3b0..a0165656 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -rootProject.name = 'terra-java-project-template' +rootProject.name = 'javapfb' include('service', 'client', 'integration') gradle.ext.releaseVersion = '0.11.0' From 67b3e75e2a3f7c5653a1134c649c366c95d977e0 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 21 Jun 2023 08:54:03 -0400 Subject: [PATCH 04/46] Update GHA's See questions in PR descriptions --- .github/workflows/build-and-test.yml | 152 ++++++++++----------- .github/workflows/integration-tests.yml | 174 ------------------------ .github/workflows/publish.yml | 118 ++++++++-------- .github/workflows/trivy.yml | 4 +- 4 files changed, 133 insertions(+), 315 deletions(-) delete mode 100644 .github/workflows/integration-tests.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a98d0cb0..e51204df 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -98,12 +98,12 @@ jobs: needs: [ build ] runs-on: ubuntu-latest - services: - postgres: - image: postgres:13 - env: - POSTGRES_PASSWORD: postgres - ports: [ "5432:5432" ] +# services: +# postgres: +# image: postgres:13 +# env: +# POSTGRES_PASSWORD: postgres +# ports: [ "5432:5432" ] steps: - uses: actions/checkout@v3 @@ -117,85 +117,77 @@ jobs: distribution: 'temurin' cache: 'gradle' - - name: Make sure Postgres is ready and init - env: - PGPASSWORD: postgres - run: | - pg_isready -h localhost -t 10 - psql -h localhost -U postgres -f ./common/postgres-init.sql +# - name: Make sure Postgres is ready and init +# env: +# PGPASSWORD: postgres +# run: | +# pg_isready -h localhost -t 10 +# psql -h localhost -U postgres -f ./common/postgres-init.sql - name: Test with coverage run: ./gradlew --build-cache test jacocoTestReport # The SonarQube scan is done here, so it can upload the coverage report generated by the tests. -# - name: SonarQube scan -# run: ./gradlew --build-cache sonarqube -# env: -# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - integration-tests: - needs: [ build ] - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:13 - env: - POSTGRES_PASSWORD: postgres - ports: [ "5432:5432" ] - - steps: - - uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' - - - name: Make sure Postgres is ready and init + - name: SonarQube scan + run: ./gradlew --build-cache sonarqube env: - PGPASSWORD: postgres - run: | - pg_isready -h localhost -t 10 - psql -h localhost -U postgres -f ./common/postgres-init.sql - - - name: Render GitHub Secrets - run: | - echo "${{ secrets.DEV_FIRECLOUD_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/user-delegated-sa.json" - echo "${{ secrets.PERF_TESTRUNNER_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/testrunner-perf.json" - - - name: Launch the background process for integration tests - run: ./gradlew --build-cache bootRun | tee application.log & - - - name: Wait for boot run to be ready - run: | - set +e - timeout 60 bash -c 'until echo > /dev/tcp/localhost/8080; do sleep 1; done' - resultStatus=$? - set -e - if [[ $resultStatus == 0 ]]; then - echo "Server started successfully" - else - echo "Server did not start successfully" - exit 1 - fi - - - name: Run the integration test suite - run: ./gradlew --build-cache runTest --args="suites/local/FullIntegration.json build/reports" - - - name: Archive logs - id: archive_logs - if: always() - uses: actions/upload-artifact@v3 - with: - name: application-logs - path: | - application.log + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +# integration-tests: +# needs: [ build ] +# runs-on: ubuntu-latest +# +## services: +## postgres: +## image: postgres:13 +## env: +## POSTGRES_PASSWORD: postgres +## ports: [ "5432:5432" ] +# +# steps: +# - uses: actions/checkout@v3 +# - name: Set up JDK +# uses: actions/setup-java@v3 +# with: +# java-version: '17' +# distribution: 'temurin' +# cache: 'gradle' +# - name: Render GitHub Secrets +# run: | +# echo "${{ secrets.DEV_FIRECLOUD_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/user-delegated-sa.json" +# echo "${{ secrets.PERF_TESTRUNNER_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/testrunner-perf.json" +# +# - name: Launch the background process for integration tests +# run: ./gradlew --build-cache bootRun | tee application.log & +# +# - name: Wait for boot run to be ready +# run: | +# set +e +# timeout 60 bash -c 'until echo > /dev/tcp/localhost/8080; do sleep 1; done' +# resultStatus=$? +# set -e +# if [[ $resultStatus == 0 ]]; then +# echo "Server started successfully" +# else +# echo "Server did not start successfully" +# exit 1 +# fi +# +# - name: Run the integration test suite +# run: ./gradlew --build-cache runTest --args="suites/local/FullIntegration.json build/reports" +# +# - name: Archive logs +# id: archive_logs +# if: always() +# uses: actions/upload-artifact@v3 +# with: +# name: application-logs +# path: | +# application.log notify-slack: - needs: [ build, unit-tests-and-sonarqube, source-clear, integration-tests ] + needs: [ build, unit-tests-and-sonarqube, source-clear ] runs-on: ubuntu-latest if: failure() && github.ref == 'refs/heads/main' @@ -206,15 +198,15 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} with: - channel: '#jade-data-explorer' + channel: '#dsp-analysis-journeys-alerts' status: failure author_name: Build on dev fields: job,message text: 'Build failed :sadpanda:' - username: 'Data Explorer GitHub Action' + username: 'Java-PFB GitHub Action' dispatch-tag: - needs: [ build, unit-tests-and-sonarqube, source-clear, integration-tests ] + needs: [ build, unit-tests-and-sonarqube, source-clear ] runs-on: ubuntu-latest if: success() && github.ref == 'refs/heads/main' diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml deleted file mode 100644 index 84909b0d..00000000 --- a/.github/workflows/integration-tests.yml +++ /dev/null @@ -1,174 +0,0 @@ -name: Integration Tests - -on: - workflow_dispatch: - inputs: - environment: - type: choice - description: 'environment to run test in' - required: true - options: - - staging - - alpha - - dev - default: 'dev' - -env: - TEST_ENV: ${{ github.event.inputs.environment }} - TEST_DEFAULT: dev - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' - - - name: Gradle build service - run: ./gradlew --build-cache :service:build -x test - - jib: - needs: [ build ] - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' - - - name: Construct docker image name and tag - id: image-name - run: | - GITHUB_REPO=$(basename ${{ github.repository }}) - GIT_SHORT_HASH=$(git rev-parse --short HEAD) - echo "name=${GITHUB_REPO}:${GIT_SHORT_HASH}" >> $GITHUB_OUTPUT - - - name: Build image locally with jib - run: | - ./gradlew --build-cache :service:jibDockerBuild \ - --image=${{ steps.image-name.outputs.name }} \ - -Djib.console=plain - - dispatch-trivy: - needs: [ build ] - runs-on: ubuntu-latest - - steps: - - name: Fire off Trivy action - uses: broadinstitute/workflow-dispatch@v1 - with: - workflow: Trivy - token: ${{ secrets.BROADBOT_TOKEN }} - - test-env: - runs-on: ubuntu-latest - outputs: - test-env: ${{ steps.test-env.outputs.test-env }} - - steps: - - name: Set default test env - id: test-env - run: | - echo "test-env=${{ env.TEST_ENV || env.TEST_DEFAULT }}" >> $GITHUB_OUTPUT - - test-runner: - needs: [ build, test-env ] - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Get the helm chart versions for the test env - run: | - curl -H 'Authorization: token ${{ secrets.BROADBOT_TOKEN }}' \ - -H 'Accept: application/vnd.github.v3.raw' \ - -L https://api.github.com/repos/broadinstitute/terra-helmfile/contents/versions/app/dev.yaml \ - --create-dirs -o "integration/src/main/resources/rendered/dev.yaml" - curl -H 'Authorization: token ${{ secrets.BROADBOT_TOKEN }}' \ - -H 'Accept: application/vnd.github.v3.raw' \ - -L https://api.github.com/repos/broadinstitute/terra-helmfile/contents/environments/live/${{ needs.test-env.outputs.test-env }}.yaml \ - --create-dirs -o "integration/src/main/resources/rendered/${{ needs.test-env.outputs.test-env }}.yaml" - - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' - - - name: Render GitHub Secrets - run: | - echo "${{ secrets.DEV_FIRECLOUD_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/user-delegated-sa.json" - echo "${{ secrets.PERF_TESTRUNNER_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/testrunner-perf.json" - - - name: Run integration test suite - run: | - ./gradlew --build-cache runTest --args="suites/${{ needs.test-env.outputs.test-env }}/FullIntegration.json build/reports" - - - name: Upload Test Reports for QA - if: always() - run: | - ./gradlew --build-cache uploadResults --args="CompressDirectoryToTerraKernelK8S.json build/reports" - - - name: Upload Test Reports for GitHub - if: always() - uses: actions/upload-artifact@v1 - with: - name: Test Reports - path: integration/build/reports - - notify-de-slack: - needs: [ build, jib, test-env, test-runner ] - runs-on: ubuntu-latest - if: failure() - - steps: - - name: "Notify #jade-data-explorer Slack on failure" - uses: broadinstitute/action-slack@v3.8.0 - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - STATUS: failure - with: - channel: '#jade-data-explorer' - status: ${{ env.STATUS }} - fields: job,ref - text: > - ${{ format('Catalog test *{0}* in *{1}* {2}', - env.STATUS, needs.test-env.outputs.test-env, - env.STATUS == 'success' && ':check_green:' || ':sadpanda:') }} - username: 'Data Explorer Tests' - - notify-qa-slack: - needs: [ build, jib, test-env, test-runner ] - runs-on: ubuntu-latest - if: always() && needs.test-env.outputs.test-env != 'dev' - - steps: - - name: "Always notify #dsde-qa Slack" - uses: broadinstitute/action-slack@v3.8.0 - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - STATUS: >- - ${{ needs.build.result == 'success' - && needs.jib.result == 'success' - && needs.test-runner.result == 'success' - && 'success' || 'failure' }} - with: - channel: '#dsde-qa' - status: ${{ env.STATUS }} - fields: job,ref - text: > - ${{ format('Catalog test *{0}* in *{1}* {2}', - env.STATUS, needs.test-env.outputs.test-env, - env.STATUS == 'success' && ':check_green:' || ':sadpanda:') }} - username: 'Data Explorer Tests' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8b3a8648..3b0ab7de 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,43 +34,43 @@ jobs: ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} ARTIFACTORY_REPO_KEY: "libs-release-local" - - name: Auth to Google - uses: google-github-actions/auth@v1 - with: - workload_identity_provider: projects/1038484894585/locations/global/workloadIdentityPools/github-wi-pool/providers/github-wi-provider - service_account: gcr-publish@broad-dsp-gcr-public.iam.gserviceaccount.com - - - name: Setup gcloud - uses: google-github-actions/setup-gcloud@v1 - - - name: Explicitly auth Docker for GCR - run: gcloud auth configure-docker --quiet - - - name: Construct docker image name and tag - id: image-name - run: echo "name=gcr.io/${GOOGLE_PROJECT}/${SERVICE_NAME}:${{ steps.tag.outputs.tag }}" >> $GITHUB_OUTPUT - - - name: Build image locally with jib - run: | - ./gradlew --build-cache :service:jibDockerBuild \ - --image=${{ steps.image-name.outputs.name }} \ - -Djib.console=plain - - - name: Push GCR image - run: docker push ${{ steps.image-name.outputs.name }} +# - name: Auth to Google +# uses: google-github-actions/auth@v1 +# with: +# workload_identity_provider: projects/1038484894585/locations/global/workloadIdentityPools/github-wi-pool/providers/github-wi-provider +# service_account: gcr-publish@broad-dsp-gcr-public.iam.gserviceaccount.com +# +# - name: Setup gcloud +# uses: google-github-actions/setup-gcloud@v1 +# +# - name: Explicitly auth Docker for GCR +# run: gcloud auth configure-docker --quiet +# +# - name: Construct docker image name and tag +# id: image-name +# run: echo "name=gcr.io/${GOOGLE_PROJECT}/${SERVICE_NAME}:${{ steps.tag.outputs.tag }}" >> $GITHUB_OUTPUT +# +# - name: Build image locally with jib +# run: | +# ./gradlew --build-cache :service:jibDockerBuild \ +# --image=${{ steps.image-name.outputs.name }} \ +# -Djib.console=plain +# +# - name: Push GCR image +# run: docker push ${{ steps.image-name.outputs.name }} - # - name: Notify slack on failure - # uses: broadinstitute/action-slack@v3.8.0 - # if: failure() - # env: - # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - # with: - # channel: '#jade-data-explorer' - # status: failure - # author_name: Publish to dev - # fields: job - # text: 'Publish failed :sadpanda:' - # username: 'Terra Java Project Template GitHub Action' + - name: Notify slack on failure + uses: broadinstitute/action-slack@v3.8.0 + if: failure() + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + with: + channel: '#dsp-analysis-journeys-alerts' + status: failure + author_name: Publish to dev + fields: job + text: 'Publish failed :sadpanda:' + username: 'Java-PFB GitHub Action' # @@ -86,26 +86,26 @@ jobs: # help with that, ping #dsp-devops-champions and we can point you in the right direction. # - report-to-sherlock: - # Report new version to Broad DevOps - uses: broadinstitute/sherlock/.github/workflows/client-report-app-version.yaml@main - needs: publish-job - with: - new-version: ${{ needs.publish-job.outputs.tag }} - chart-name: 'javapfb' - permissions: - contents: 'read' - id-token: 'write' - - set-version-in-dev: - # Put new version in Broad dev environment - uses: broadinstitute/sherlock/.github/workflows/client-set-environment-app-version.yaml@main - needs: [publish-job, report-to-sherlock] - with: - new-version: ${{ needs.publish-job.outputs.tag }} - chart-name: 'javapfb' - environment-name: 'template-services' - secrets: - sync-git-token: ${{ secrets.BROADBOT_TOKEN }} - permissions: - id-token: 'write' +# report-to-sherlock: +# # Report new version to Broad DevOps +# uses: broadinstitute/sherlock/.github/workflows/client-report-app-version.yaml@main +# needs: publish-job +# with: +# new-version: ${{ needs.publish-job.outputs.tag }} +# chart-name: 'javapfb' +# permissions: +# contents: 'read' +# id-token: 'write' +# +# set-version-in-dev: +# # Put new version in Broad dev environment +# uses: broadinstitute/sherlock/.github/workflows/client-set-environment-app-version.yaml@main +# needs: [publish-job, report-to-sherlock] +# with: +# new-version: ${{ needs.publish-job.outputs.tag }} +# chart-name: 'javapfb' +# environment-name: 'template-services' +# secrets: +# sync-git-token: ${{ secrets.BROADBOT_TOKEN }} +# permissions: +# id-token: 'write' diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 1010a86f..60ac2fdd 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -43,9 +43,9 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} with: - channel: '#jade-data-explorer' + channel: '#dsp-analysis-journeys-alerts' status: failure author_name: Trivy action fields: workflow,message text: 'Trivy scan failure :sadpanda:' - username: 'Data Explorer GitHub Action' + username: 'Java-PFB GitHub Action' From 10087123be2402e4f354759c33ea13d1f1d39975 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 21 Jun 2023 09:33:26 -0400 Subject: [PATCH 05/46] Hello world CLI add help command --- java-pfb-cli/README.md | 6 ++++ java-pfb-cli/build.gradle | 30 +++++++++++++++++++ .../src/main/java/JavaPfbCommand.java | 24 +++++++++++++++ settings.gradle | 2 ++ 4 files changed, 62 insertions(+) create mode 100644 java-pfb-cli/README.md create mode 100644 java-pfb-cli/build.gradle create mode 100644 java-pfb-cli/src/main/java/JavaPfbCommand.java diff --git a/java-pfb-cli/README.md b/java-pfb-cli/README.md new file mode 100644 index 00000000..b66dd36f --- /dev/null +++ b/java-pfb-cli/README.md @@ -0,0 +1,6 @@ +Current usage of CLI: +First, run "fatJar" gradle task in java-pfb-cli + +Then, you can use the CLI with the following command: +java -cp "java-pfb-cli/build/libs/java-pfb-cli.jar" JavaPfbCommand +Only command right now: "hello" \ No newline at end of file diff --git a/java-pfb-cli/build.gradle b/java-pfb-cli/build.gradle new file mode 100644 index 00000000..904e44b0 --- /dev/null +++ b/java-pfb-cli/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' +} + +version 'unspecified' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + implementation 'info.picocli:picocli:4.7.4' +} + +task fatJar(type: Jar) { + manifest { + attributes 'Main-Class': "JavaPfbCommand" + } + archiveBaseName.set('java-pfb-cli') + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + with jar +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/java-pfb-cli/src/main/java/JavaPfbCommand.java b/java-pfb-cli/src/main/java/JavaPfbCommand.java new file mode 100644 index 00000000..8a951799 --- /dev/null +++ b/java-pfb-cli/src/main/java/JavaPfbCommand.java @@ -0,0 +1,24 @@ +import picocli.CommandLine; + +import static picocli.CommandLine.Command; +import static picocli.CommandLine.Option; + +@Command(name = "pfb") +public class JavaPfbCommand implements Runnable { + public static void main(String[] args) { + CommandLine.run(new JavaPfbCommand(), args); + } + + @Override + public void run() { + System.out.println("A java implementation of pyPFB"); + } + + @Command(name = "hello") + public void helloCommand() { + System.out.println("Hello world!"); + } + + @Command(name = "--help") + public void helpCommand() { System.out.println("Help is on its way!! In the next PR...");} +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a0165656..5451eb0e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,5 @@ rootProject.name = 'javapfb' include('service', 'client', 'integration') gradle.ext.releaseVersion = '0.11.0' +include 'java-pfb-cli' + From 8d530f6cf29fad54222f672d444a5d2e8b6ce755 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Thu, 22 Jun 2023 14:18:58 -0400 Subject: [PATCH 06/46] Another round of template deletions More cleanup --- client/.swagger-codegen-ignore | 2 - client/build.gradle | 1 - client/swagger.gradle | 42 ---- integration/build.gradle | 31 --- .../java/scripts/client/JavaPfbClient.java | 48 ----- .../java/scripts/testscripts/GetStatus.java | 20 -- .../java/scripts/testscripts/GetVersion.java | 30 --- .../configs/integration/GetStatus.json | 17 -- .../configs/integration/GetVersion.json | 17 -- .../src/main/resources/rendered/.gitignore | 2 - .../src/main/resources/servers/local.json | 11 - .../serviceaccounts/delegate-user-sa.json | 5 - .../serviceaccounts/testrunner-perf.json | 5 - .../suites/local/FullIntegration.json | 9 - .../src/main/resources/testusers/admin.json | 5 - .../src/main/resources/testusers/user.json | 5 - scripts/render_configs.sh | 22 -- service/build.gradle | 12 -- service/generators.gradle | 22 -- .../terra/javapfb/{App.java => Library.java} | 11 +- .../config/StatusCheckConfiguration.java | 10 - .../javapfb/config/VersionConfiguration.java | 7 - .../controller/GlobalExceptionHandler.java | 16 -- .../javapfb/service/BaseStatusService.java | 90 -------- service/src/main/resources/api/openapi.yml | 200 ------------------ service/src/main/resources/application.yml | 85 -------- .../changelog/changesets/initial_schema.yaml | 26 --- .../db/changelog/db.changelog-master.yaml | 7 - .../src/main/resources/rendered/.gitignore | 2 - .../src/main/resources/templates/index.html | 144 ------------- ...seSpringBootTest.java => LibraryTest.java} | 8 +- .../service/BaseStatusServiceTest.java | 28 --- 32 files changed, 10 insertions(+), 930 deletions(-) delete mode 100644 client/.swagger-codegen-ignore delete mode 100644 client/swagger.gradle delete mode 100644 integration/build.gradle delete mode 100644 integration/src/main/java/scripts/client/JavaPfbClient.java delete mode 100644 integration/src/main/java/scripts/testscripts/GetStatus.java delete mode 100644 integration/src/main/java/scripts/testscripts/GetVersion.java delete mode 100644 integration/src/main/resources/configs/integration/GetStatus.json delete mode 100644 integration/src/main/resources/configs/integration/GetVersion.json delete mode 100644 integration/src/main/resources/rendered/.gitignore delete mode 100644 integration/src/main/resources/servers/local.json delete mode 100644 integration/src/main/resources/serviceaccounts/delegate-user-sa.json delete mode 100644 integration/src/main/resources/serviceaccounts/testrunner-perf.json delete mode 100644 integration/src/main/resources/suites/local/FullIntegration.json delete mode 100644 integration/src/main/resources/testusers/admin.json delete mode 100644 integration/src/main/resources/testusers/user.json delete mode 100755 scripts/render_configs.sh rename service/src/main/java/bio/terra/javapfb/{App.java => Library.java} (84%) delete mode 100644 service/src/main/java/bio/terra/javapfb/config/StatusCheckConfiguration.java delete mode 100644 service/src/main/java/bio/terra/javapfb/config/VersionConfiguration.java delete mode 100644 service/src/main/java/bio/terra/javapfb/controller/GlobalExceptionHandler.java delete mode 100644 service/src/main/java/bio/terra/javapfb/service/BaseStatusService.java delete mode 100644 service/src/main/resources/api/openapi.yml delete mode 100644 service/src/main/resources/application.yml delete mode 100644 service/src/main/resources/db/changelog/changesets/initial_schema.yaml delete mode 100644 service/src/main/resources/db/changelog/db.changelog-master.yaml delete mode 100644 service/src/main/resources/rendered/.gitignore delete mode 100644 service/src/main/resources/templates/index.html rename service/src/test/java/bio/terra/javapfb/{BaseSpringBootTest.java => LibraryTest.java} (57%) delete mode 100644 service/src/test/java/bio/terra/javapfb/service/BaseStatusServiceTest.java diff --git a/client/.swagger-codegen-ignore b/client/.swagger-codegen-ignore deleted file mode 100644 index e5dfce6d..00000000 --- a/client/.swagger-codegen-ignore +++ /dev/null @@ -1,2 +0,0 @@ -** -!**/src/main/java/** \ No newline at end of file diff --git a/client/build.gradle b/client/build.gradle index 1ed4a117..1f3ea4b9 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -15,4 +15,3 @@ dependencyManagement { } apply from: 'artifactory.gradle' -apply from: 'swagger.gradle' diff --git a/client/swagger.gradle b/client/swagger.gradle deleted file mode 100644 index fe55676f..00000000 --- a/client/swagger.gradle +++ /dev/null @@ -1,42 +0,0 @@ -dependencies { - // Version controlled by dependency management plugin - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' - implementation 'org.glassfish.jersey.core:jersey-client' - implementation 'org.glassfish.jersey.media:jersey-media-json-jackson' - implementation 'org.glassfish.jersey.media:jersey-media-multipart' - - implementation 'io.swagger.core.v3:swagger-annotations' - swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli' -} - -def artifactGroup = "${group}.javapfb" - -generateSwaggerCode { - inputFile = file('../service/src/main/resources/api/openapi.yml') - language = 'java' - library = 'jersey2' - - // For Swagger Codegen v3 on Java 16+ - // See https://github.com/swagger-api/swagger-codegen/issues/10966 - jvmArgs = ['--add-opens=java.base/java.util=ALL-UNNAMED'] - - components = [ - apiDocs : false, apiTests: false, - modelDocs: false, modelTests: false - ] - - additionalProperties = [ - modelPackage : "${artifactGroup}.model", - apiPackage : "${artifactGroup}.api", - invokerPackage: "${artifactGroup}.client", - dateLibrary : 'java11', - java8 : true - ] - - rawOptions = ['--ignore-file-override', "${projectDir}/.swagger-codegen-ignore"] -} - -idea.module.generatedSourceDirs = [file("${generateSwaggerCode.outputDir}/src/main/java")] -sourceSets.main.java.srcDir "${generateSwaggerCode.outputDir}/src/main/java" -compileJava.dependsOn generateSwaggerCode -sourcesJar.dependsOn generateSwaggerCode diff --git a/integration/build.gradle b/integration/build.gradle deleted file mode 100644 index 394a48f2..00000000 --- a/integration/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -import org.springframework.boot.gradle.plugin.SpringBootPlugin - -plugins { - id 'bio.terra.java-application-conventions' - id 'io.spring.dependency-management' - id 'bio.terra.test-runner-plugin' -} - -dependencyManagement { - imports { - mavenBom(SpringBootPlugin.BOM_COORDINATES) - } -} - -dependencies { - implementation 'org.slf4j:slf4j-api' - implementation 'org.glassfish.jersey.inject:jersey-hk2' - implementation 'org.junit.jupiter:junit-jupiter-api' - implementation 'org.hamcrest:hamcrest' - - implementation 'org.glassfish.jersey.connectors:jersey-jdk-connector' - - // Google Dependencies - implementation 'com.google.auth:google-auth-library-oauth2-http:1.4.0' - - // Terra Test Runner Library - implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' - - // Requires client libraries - implementation project(':client') -} diff --git a/integration/src/main/java/scripts/client/JavaPfbClient.java b/integration/src/main/java/scripts/client/JavaPfbClient.java deleted file mode 100644 index ee2625f6..00000000 --- a/integration/src/main/java/scripts/client/JavaPfbClient.java +++ /dev/null @@ -1,48 +0,0 @@ -package scripts.client; - -import bio.terra.javapfb.client.ApiClient; -import bio.terra.testrunner.common.utils.AuthenticationUtils; -import bio.terra.testrunner.runner.config.ServerSpecification; -import bio.terra.testrunner.runner.config.TestUserSpecification; -import com.google.auth.oauth2.GoogleCredentials; -import java.io.IOException; -import java.util.Objects; - -public class JavaPfbClient extends ApiClient { - - /** - * Build a no-auth API client object for the service. No access token is needed for this API - * client. - * - * @param server the server we are testing against - */ - public JavaPfbClient(ServerSpecification server) throws IOException { - this(server, null); - } - - /** - * Build an API client object for the given test user for the service. The test user's token is - * always refreshed. If a test user isn't configured (e.g. when running locally), return an - * un-authenticated client. - * - * @param server the server we are testing against - * @param testUser the test user whose credentials are supplied to the API client object - */ - public JavaPfbClient(ServerSpecification server, TestUserSpecification testUser) - throws IOException { - // note that this uses server.catalogUri. Typically a uri for a new service needs to be added to - // https://github.com/DataBiosphere/terra-test-runner/blob/main/src/main/java/bio/terra/testrunner/runner/config/ServerSpecification.java - // but for this template we will stick with catalog - setBasePath(Objects.requireNonNull(server.catalogUri, "Catalog URI required")); - - if (testUser != null) { - GoogleCredentials userCredential = - AuthenticationUtils.getDelegatedUserCredential( - testUser, AuthenticationUtils.userLoginScopes); - var accessToken = AuthenticationUtils.getAccessToken(userCredential); - if (accessToken != null) { - setAccessToken(accessToken.getTokenValue()); - } - } - } -} diff --git a/integration/src/main/java/scripts/testscripts/GetStatus.java b/integration/src/main/java/scripts/testscripts/GetStatus.java deleted file mode 100644 index 018da00f..00000000 --- a/integration/src/main/java/scripts/testscripts/GetStatus.java +++ /dev/null @@ -1,20 +0,0 @@ -package scripts.testscripts; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -import bio.terra.javapfb.api.PublicApi; -import bio.terra.testrunner.runner.TestScript; -import bio.terra.testrunner.runner.config.TestUserSpecification; -import com.google.api.client.http.HttpStatusCodes; -import scripts.client.JavaPfbClient; - -public class GetStatus extends TestScript { - @Override - public void userJourney(TestUserSpecification testUser) throws Exception { - var client = new JavaPfbClient(server); - var publicApi = new PublicApi(client); - publicApi.getStatus(); - assertThat(client.getStatusCode(), is(HttpStatusCodes.STATUS_CODE_OK)); - } -} diff --git a/integration/src/main/java/scripts/testscripts/GetVersion.java b/integration/src/main/java/scripts/testscripts/GetVersion.java deleted file mode 100644 index e5948351..00000000 --- a/integration/src/main/java/scripts/testscripts/GetVersion.java +++ /dev/null @@ -1,30 +0,0 @@ -package scripts.testscripts; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; - -import bio.terra.javapfb.api.PublicApi; -import bio.terra.testrunner.runner.TestScript; -import bio.terra.testrunner.runner.config.TestUserSpecification; -import com.google.api.client.http.HttpStatusCodes; -import scripts.client.JavaPfbClient; - -public class GetVersion extends TestScript { - @Override - public void userJourney(TestUserSpecification testUser) throws Exception { - JavaPfbClient client = new JavaPfbClient(server); - var publicApi = new PublicApi(client); - - var versionProperties = publicApi.getVersion(); - - // check the response code - assertThat(client.getStatusCode(), is(HttpStatusCodes.STATUS_CODE_OK)); - - // check the response body - assertThat(versionProperties.getGitHash(), notNullValue()); - assertThat(versionProperties.getGitTag(), notNullValue()); - assertThat(versionProperties.getGithub(), notNullValue()); - assertThat(versionProperties.getBuild(), notNullValue()); - } -} diff --git a/integration/src/main/resources/configs/integration/GetStatus.json b/integration/src/main/resources/configs/integration/GetStatus.json deleted file mode 100644 index 18cc4673..00000000 --- a/integration/src/main/resources/configs/integration/GetStatus.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "GetStatus", - "description": "Check the service status once. No authentication required.", - "serverSpecificationFile": "local.json", - "kubernetes": {}, - "application": {}, - "testScripts": [ - { - "name": "GetStatus", - "numberOfUserJourneyThreadsToRun": 1, - "userJourneyThreadPoolSize": 2, - "expectedTimeForEach": 5, - "expectedTimeForEachUnit": "SECONDS" - } - ], - "testUserFiles": [] -} diff --git a/integration/src/main/resources/configs/integration/GetVersion.json b/integration/src/main/resources/configs/integration/GetVersion.json deleted file mode 100644 index 96302865..00000000 --- a/integration/src/main/resources/configs/integration/GetVersion.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "GetVersion", - "description": "Check the version endpoint once. No authentication required.", - "serverSpecificationFile": "local.json", - "kubernetes": {}, - "application": {}, - "testScripts": [ - { - "name": "GetVersion", - "numberOfUserJourneyThreadsToRun": 1, - "userJourneyThreadPoolSize": 2, - "expectedTimeForEach": 5, - "expectedTimeForEachUnit": "SECONDS" - } - ], - "testUserFiles": [] -} diff --git a/integration/src/main/resources/rendered/.gitignore b/integration/src/main/resources/rendered/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/integration/src/main/resources/rendered/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/integration/src/main/resources/servers/local.json b/integration/src/main/resources/servers/local.json deleted file mode 100644 index 9b1190f9..00000000 --- a/integration/src/main/resources/servers/local.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "local", - "description": "Local env.", - - "catalogUri": "http://localhost:8080", - - "testRunnerServiceAccountFile": "testrunner-perf.json", - - "skipDeployment": true, - "skipKubernetes": true -} diff --git a/integration/src/main/resources/serviceaccounts/delegate-user-sa.json b/integration/src/main/resources/serviceaccounts/delegate-user-sa.json deleted file mode 100644 index 629fd0a6..00000000 --- a/integration/src/main/resources/serviceaccounts/delegate-user-sa.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "firecloud-dev@broad-dsde-dev.iam.gserviceaccount.com", - "jsonKeyFilename": "user-delegated-sa.json", - "jsonKeyDirectoryPath": "src/main/resources/rendered" -} diff --git a/integration/src/main/resources/serviceaccounts/testrunner-perf.json b/integration/src/main/resources/serviceaccounts/testrunner-perf.json deleted file mode 100644 index bec13a2a..00000000 --- a/integration/src/main/resources/serviceaccounts/testrunner-perf.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "testrunner-perf@broad-dsde-perf.iam.gserviceaccount.com", - "jsonKeyFilename": "testrunner-perf.json", - "jsonKeyDirectoryPath": "src/main/resources/rendered" -} diff --git a/integration/src/main/resources/suites/local/FullIntegration.json b/integration/src/main/resources/suites/local/FullIntegration.json deleted file mode 100644 index 1406547f..00000000 --- a/integration/src/main/resources/suites/local/FullIntegration.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "FullIntegration", - "description": "All integration tests", - "serverSpecificationFile": "local.json", - "testConfigurationFiles": [ - "integration/GetStatus.json", - "integration/GetVersion.json" - ] -} diff --git a/integration/src/main/resources/testusers/admin.json b/integration/src/main/resources/testusers/admin.json deleted file mode 100644 index 8d259c75..00000000 --- a/integration/src/main/resources/testusers/admin.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "admin", - "userEmail": "datacatalogadmin@test.firecloud.org", - "delegatorServiceAccountFile": "delegate-user-sa.json" -} diff --git a/integration/src/main/resources/testusers/user.json b/integration/src/main/resources/testusers/user.json deleted file mode 100644 index 8f0d7633..00000000 --- a/integration/src/main/resources/testusers/user.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "user", - "userEmail": "datacataloguser@test.firecloud.org", - "delegatorServiceAccountFile": "delegate-user-sa.json" -} diff --git a/scripts/render_configs.sh b/scripts/render_configs.sh deleted file mode 100755 index a573408f..00000000 --- a/scripts/render_configs.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -ENV=${1:-dev} -VAULT_TOKEN=${2:-$(cat "$HOME"/.vault-token)} - -VAULT_ADDR="https://clotho.broadinstitute.org:8200" - -VAULT_COMMAND="vault read" - -# use SERVICE_OUTPUT_LOCATION to add any service specific secrets -SERVICE_OUTPUT_LOCATION="$(dirname "$0")/../service/src/main/resources/rendered" -INTEGRATION_OUTPUT_LOCATION="$(dirname "$0")/../integration/src/main/resources/rendered" - -if ! [ -x "$(command -v vault)" ]; then - VAULT_COMMAND="docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN -e VAULT_ADDR=$VAULT_ADDR vault:1.7.3 $VAULT_COMMAND" -fi - -$VAULT_COMMAND -field=data -format=json "secret/dsde/firecloud/$ENV/common/firecloud-account.json" >"$INTEGRATION_OUTPUT_LOCATION/user-delegated-sa.json" - -# We use the perf testrunner account in all environments. -PERF_VAULT_PATH="secret/dsde/terra/kernel/perf/common" -$VAULT_COMMAND -field=key "$PERF_VAULT_PATH/testrunner/testrunner-sa" | base64 -d > "$INTEGRATION_OUTPUT_LOCATION/testrunner-perf.json" diff --git a/service/build.gradle b/service/build.gradle index caf1c936..f4fabc34 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -50,15 +50,3 @@ sonarqube { property 'sonar.host.url', 'https://sonarcloud.io' } } - -//liquibase { -// activities { -// catalog { -// changeLogFile 'src/main/resources/db/changelog.xml' -// url 'jdbc:postgresql://localhost:5432/java_pfb_db' -// username 'dbuser' -// password 'dbpwd' -// logLevel 'info' -// } -// } -//} diff --git a/service/generators.gradle b/service/generators.gradle index 1cc0681c..6b39c52c 100644 --- a/service/generators.gradle +++ b/service/generators.gradle @@ -12,28 +12,6 @@ dependencies { def artifactGroup = "${group}.javapfb" -generateSwaggerCode { - inputFile = file('src/main/resources/api/openapi.yml') - language = 'spring' - components = ['models', 'apis'] - jvmArgs = ['--add-opens=java.base/java.util=ALL-UNNAMED'] - additionalProperties = [ - modelPackage : "${artifactGroup}.model", - apiPackage : "${artifactGroup}.api", - dateLibrary : 'java11', - java8 : true, - interfaceOnly : 'true', - useTags : 'true', - springBootVersion: dependencyManagement.managedVersions['org.springframework.boot:spring-boot'] - ] -} - -String swaggerOutputSrc = "${generateSwaggerCode.outputDir}/src/main/java" - -idea.module.generatedSourceDirs = [file(swaggerOutputSrc)] -sourceSets.main.java.srcDir swaggerOutputSrc -compileJava.dependsOn generateSwaggerCode - // see https://github.com/n0mer/gradle-git-properties gitProperties { keys = [] diff --git a/service/src/main/java/bio/terra/javapfb/App.java b/service/src/main/java/bio/terra/javapfb/Library.java similarity index 84% rename from service/src/main/java/bio/terra/javapfb/App.java rename to service/src/main/java/bio/terra/javapfb/Library.java index f0556b7a..d57cb29c 100644 --- a/service/src/main/java/bio/terra/javapfb/App.java +++ b/service/src/main/java/bio/terra/javapfb/Library.java @@ -6,15 +6,12 @@ import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -import javax.sql.DataSource; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.support.JdbcTransactionManager; import org.springframework.retry.annotation.EnableRetry; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication( @@ -30,14 +27,12 @@ @EnableRetry @EnableTransactionManagement @EnableConfigurationProperties -public class App { +public class Library { public static void main(String[] args) { - new SpringApplicationBuilder(App.class).initializers(new LoggingInitializer()).run(args); + new SpringApplicationBuilder(Library.class).initializers(new LoggingInitializer()).run(args); } - public App() { - - } + public Library() {} @Bean("objectMapper") public ObjectMapper objectMapper() { diff --git a/service/src/main/java/bio/terra/javapfb/config/StatusCheckConfiguration.java b/service/src/main/java/bio/terra/javapfb/config/StatusCheckConfiguration.java deleted file mode 100644 index 499a1550..00000000 --- a/service/src/main/java/bio/terra/javapfb/config/StatusCheckConfiguration.java +++ /dev/null @@ -1,10 +0,0 @@ -package bio.terra.javapfb.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "javapfb.status-check") -public record StatusCheckConfiguration( - boolean enabled, - int pollingIntervalSeconds, - int startupWaitSeconds, - int stalenessThresholdSeconds) {} diff --git a/service/src/main/java/bio/terra/javapfb/config/VersionConfiguration.java b/service/src/main/java/bio/terra/javapfb/config/VersionConfiguration.java deleted file mode 100644 index b69a6a83..00000000 --- a/service/src/main/java/bio/terra/javapfb/config/VersionConfiguration.java +++ /dev/null @@ -1,7 +0,0 @@ -package bio.terra.javapfb.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** Read from the git.properties file auto-generated at build time */ -@ConfigurationProperties("javapfb.version") -public record VersionConfiguration(String gitHash, String gitTag, String build, String github) {} diff --git a/service/src/main/java/bio/terra/javapfb/controller/GlobalExceptionHandler.java b/service/src/main/java/bio/terra/javapfb/controller/GlobalExceptionHandler.java deleted file mode 100644 index e9f02081..00000000 --- a/service/src/main/java/bio/terra/javapfb/controller/GlobalExceptionHandler.java +++ /dev/null @@ -1,16 +0,0 @@ -package bio.terra.javapfb.controller; - -import bio.terra.common.exception.AbstractGlobalExceptionHandler; -import bio.terra.javapfb.model.ErrorReport; -import java.util.List; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class GlobalExceptionHandler extends AbstractGlobalExceptionHandler { - - @Override - public ErrorReport generateErrorReport(Throwable ex, HttpStatus statusCode, List causes) { - return new ErrorReport().message(ex.getMessage()).statusCode(statusCode.value()); - } -} diff --git a/service/src/main/java/bio/terra/javapfb/service/BaseStatusService.java b/service/src/main/java/bio/terra/javapfb/service/BaseStatusService.java deleted file mode 100644 index b8aa27df..00000000 --- a/service/src/main/java/bio/terra/javapfb/service/BaseStatusService.java +++ /dev/null @@ -1,90 +0,0 @@ -package bio.terra.javapfb.service; - -import bio.terra.javapfb.config.StatusCheckConfiguration; -import bio.terra.javapfb.model.SystemStatus; -import bio.terra.javapfb.model.SystemStatusSystems; -import com.google.common.annotations.VisibleForTesting; -import java.time.Instant; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import javax.annotation.PostConstruct; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class BaseStatusService { - private static final Logger logger = LoggerFactory.getLogger(BaseStatusService.class); - /** cached status */ - private final AtomicReference cachedStatus; - /** configuration parameters */ - private final StatusCheckConfiguration configuration; - /** set of status methods to check */ - private final ConcurrentHashMap> statusCheckMap; - /** scheduler */ - private final ScheduledExecutorService scheduler; - /** last time cache was updated */ - private final AtomicReference lastStatusUpdate; - - public BaseStatusService(StatusCheckConfiguration configuration) { - this.configuration = configuration; - statusCheckMap = new ConcurrentHashMap<>(); - cachedStatus = new AtomicReference<>(new SystemStatus().ok(false)); - lastStatusUpdate = new AtomicReference<>(Instant.now()); - scheduler = Executors.newScheduledThreadPool(1); - } - - @PostConstruct - private void startStatusChecking() { - if (configuration.enabled()) { - scheduler.scheduleAtFixedRate( - this::checkStatus, - configuration.startupWaitSeconds(), - configuration.pollingIntervalSeconds(), - TimeUnit.SECONDS); - } - } - - void registerStatusCheck(String name, Supplier checkFn) { - statusCheckMap.put(name, checkFn); - } - - @VisibleForTesting - void checkStatus() { - if (configuration.enabled()) { - var newStatus = new SystemStatus(); - try { - var systems = - statusCheckMap.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); - newStatus.setOk(systems.values().stream().allMatch(SystemStatusSystems::isOk)); - newStatus.setSystems(systems); - } catch (Exception e) { - logger.warn("Status check exception", e); - newStatus.setOk(false); - } - cachedStatus.set(newStatus); - lastStatusUpdate.set(Instant.now()); - } - } - - public SystemStatus getCurrentStatus() { - if (configuration.enabled()) { - // If staleness time (last update + stale threshold) is before the current time, then - // we are officially not OK. - if (lastStatusUpdate - .get() - .plusSeconds(configuration.stalenessThresholdSeconds()) - .isBefore(Instant.now())) { - logger.warn("Status has not been updated since {}", lastStatusUpdate); - cachedStatus.set(new SystemStatus().ok(false)); - } - return cachedStatus.get(); - } - return new SystemStatus().ok(true); - } -} diff --git a/service/src/main/resources/api/openapi.yml b/service/src/main/resources/api/openapi.yml deleted file mode 100644 index b5d174f7..00000000 --- a/service/src/main/resources/api/openapi.yml +++ /dev/null @@ -1,200 +0,0 @@ -openapi: 3.0.3 -info: - title: Terra Java Project Template - description: Terra Java Project Template - version: 0.0.1 -paths: - /status: - get: - summary: Check status of the service - tags: [ public ] - operationId: getStatus - security: [ ] - responses: - '200': - $ref: '#/components/responses/SystemStatusResponse' - '500': - $ref: '#/components/responses/ServerError' - '503': - $ref: '#/components/responses/SystemStatusResponse' - - /version: - get: - summary: Get version info of the deployed service - tags: [ public ] - operationId: getVersion - security: [ ] - responses: - '200': - description: Version information - content: - application/json: - schema: - $ref: '#/components/schemas/VersionProperties' - '404': - description: "Version not configured" - '500': - $ref: '#/components/responses/ServerError' - - # README /docs/api_versioning.md - /api/example/v1/message: - get: - summary: Gets your message - tags: [ example ] - operationId: getMessage - responses: - '200': - description: Your message - content: - application/json: - schema: - type: string - '404': - description: You don't have a message - '500': - $ref: '#/components/responses/ServerError' - post: - summary: Stores your message - tags: [ example ] - operationId: setMessage - requestBody: - content: - 'application/json': - schema: - type: string - required: true - responses: - '204': - description: Message saved - '500': - $ref: '#/components/responses/ServerError' - /api/example/v1/{resourceType}/{resourceId}/{action}: - get: - summary: Checks sam access - tags: [ example ] - operationId: getAction - parameters: - - name: resourceType - in: path - required: true - schema: - type: string - - name: resourceId - in: path - required: true - schema: - type: string - - name: action - in: path - required: true - schema: - type: string - responses: - '200': - description: action access - content: - application/json: - schema: - type: boolean - '500': - $ref: '#/components/responses/ServerError' - /api/example/v1/counter: - post: - summary: increment a metrics counter - tags: [ example ] - operationId: incrementCounter - requestBody: - description: tag for counter - content: - application/json: - schema: - type: string - responses: - '204': - description: success - '500': - $ref: '#/components/responses/ServerError' - -components: - responses: - SystemStatusResponse: - description: A JSON description of the subsystems and their statuses. - content: - application/json: - schema: - $ref: '#/components/schemas/SystemStatus' - - # Error Responses - BadRequest: - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorReport' - PermissionDenied: - description: Permission denied - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorReport' - NotFound: - description: Not found (or unauthorized) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorReport' - ServerError: - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorReport' - - schemas: - ErrorReport: - type: object - required: [ message, statusCode ] - properties: - message: - type: string - statusCode: - type: integer - - SystemStatus: - required: [ ok, systems ] - type: object - properties: - ok: - type: boolean - description: whether any system(s) need attention - systems: - type: object - additionalProperties: - type: object - properties: - ok: - type: boolean - messages: - type: array - items: - type: string - - VersionProperties: - type: object - properties: - gitTag: - type: string - gitHash: - type: string - github: - type: string - build: - type: string - - securitySchemes: - bearerAuth: - type: http - scheme: bearer - -security: - - bearerAuth: [ ] diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml deleted file mode 100644 index fecd196c..00000000 --- a/service/src/main/resources/application.yml +++ /dev/null @@ -1,85 +0,0 @@ -# All env variables that are used in one place -# This is for deployment-specific values, which may be managed by other teams - -env: -# We shouldn't need a db for this client -# db: -# host: ${DATABASE_HOSTNAME:127.0.0.1}:5432 -# init: ${INIT_DB:false} -# name: ${DATABASE_NAME:java_pfb_db} -# password: ${DATABASE_USER_PASSWORD:dbpwd} -# user: ${DATABASE_USER:dbuser} - tracing: - exportEnabled: ${CLOUD_TRACE_ENABLED:false} - samplingRate: ${SAMPLING_PROBABILITY:0} - sam: - basePath: ${SAM_ADDRESS:https://sam.dsde-dev.broadinstitute.org} - -# Below here is non-deployment-specific - -# When the target is 'local' the write-config.sh script will generate this properties file. It -# contains the configuration of the BPM test application. We can use that application in our -# integration testing to make sure the application code paths are working. However, we do not -# want it to appear in production environments. -spring.config.import: optional:file:../config/local-properties.yml;classpath:git.properties - -logging.pattern.level: '%X{requestId} %5p' - -server: - compression: - enabled: true - mimeTypes: text/css,application/javascript - port: 8080 - -spring: - # application name and version are used to populate the logging serviceContext - # https://github.com/DataBiosphere/terra-common-lib/blob/480ab3daae282ddff0fef8dc329494a4422e32f1/src/main/java/bio/terra/common/logging/GoogleJsonLayout.java#L118 - application.name: javapfb - application.version: ${javapfb.version.gitHash:unknown} - - datasource: - hikari: - connection-timeout: 5000 - maximum-pool-size: 8 # cpu count * 2 https://kwahome.medium.com/database-connections-less-is-more-86c406b6fad - password: ${env.db.password} - url: jdbc:postgresql://${env.db.host}/${env.db.name} - username: ${env.db.user} - - web: - resources: - cache: - cachecontrol: - maxAge: 0 - mustRevalidate: true - useLastModified: false - staticLocations: classpath:/api/ - -management: - server: - port: 9098 - endpoints: - web: - exposure: - include: "*" - -javapfb: - ingress: - # Default value that's overridden by Helm. - domainName: localhost:8080 - - status-check: - enabled: true - pollingIntervalSeconds: 60 - startupWaitSeconds: 5 - stalenessThresholdSeconds: 125 - - sam: - basePath: ${env.sam.basePath} - -terra.common: - kubernetes: - inKubernetes: false - - tracing: - stackdriverExportEnabled: ${env.tracing.exportEnabled} - samplingRate: ${env.tracing.samplingRate} diff --git a/service/src/main/resources/db/changelog/changesets/initial_schema.yaml b/service/src/main/resources/db/changelog/changesets/initial_schema.yaml deleted file mode 100644 index c9a7b9c7..00000000 --- a/service/src/main/resources/db/changelog/changesets/initial_schema.yaml +++ /dev/null @@ -1,26 +0,0 @@ -databaseChangeLog: - - changeSet: - id: "not_so_interesting" - author: doge - changes: - - createTable: - tableName: example - columns: - - column: - name: id - type: int - autoIncrement: true - constraints: - primaryKey: true - nullable: false - - column: - name: user_id - type: text - constraints: - nullable: false - unique: true - - column: - name: message - type: text - constraints: - nullable: false diff --git a/service/src/main/resources/db/changelog/db.changelog-master.yaml b/service/src/main/resources/db/changelog/db.changelog-master.yaml deleted file mode 100644 index 3f922bff..00000000 --- a/service/src/main/resources/db/changelog/db.changelog-master.yaml +++ /dev/null @@ -1,7 +0,0 @@ -databaseChangeLog: - - include: - file: changesets/initial_schema.yaml - relativeToChangelogFile: true -# README: it is a best practice to put each DDL statement in its own change set. DDL statements -# are atomic. When they are grouped in a changeset and one fails the changeset cannot be -# rolled back or rerun making recovery more difficult \ No newline at end of file diff --git a/service/src/main/resources/rendered/.gitignore b/service/src/main/resources/rendered/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/service/src/main/resources/rendered/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/service/src/main/resources/templates/index.html b/service/src/main/resources/templates/index.html deleted file mode 100644 index f17775f3..00000000 --- a/service/src/main/resources/templates/index.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - Terra Java Project Template SwaggerUI - - - - - -
- - - - - diff --git a/service/src/test/java/bio/terra/javapfb/BaseSpringBootTest.java b/service/src/test/java/bio/terra/javapfb/LibraryTest.java similarity index 57% rename from service/src/test/java/bio/terra/javapfb/BaseSpringBootTest.java rename to service/src/test/java/bio/terra/javapfb/LibraryTest.java index 1149e6d8..4ce54e1c 100644 --- a/service/src/test/java/bio/terra/javapfb/BaseSpringBootTest.java +++ b/service/src/test/java/bio/terra/javapfb/LibraryTest.java @@ -1,8 +1,14 @@ package bio.terra.javapfb; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; @SpringBootTest @ActiveProfiles({"test", "human-readable-logging"}) -public abstract class BaseSpringBootTest {} +public abstract class LibraryTest { + @Test + public void testHelloWorld() { + System.out.println("Hello World"); + } +} diff --git a/service/src/test/java/bio/terra/javapfb/service/BaseStatusServiceTest.java b/service/src/test/java/bio/terra/javapfb/service/BaseStatusServiceTest.java deleted file mode 100644 index 41dc91b9..00000000 --- a/service/src/test/java/bio/terra/javapfb/service/BaseStatusServiceTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package bio.terra.javapfb.service; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -import bio.terra.javapfb.config.StatusCheckConfiguration; -import bio.terra.javapfb.model.SystemStatus; -import bio.terra.javapfb.model.SystemStatusSystems; -import java.util.Map; -import org.hamcrest.MatcherAssert; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.Test; - -class BaseStatusServiceTest { - - @Test - void getCurrentStatus() { - var config = new StatusCheckConfiguration(true, 0, 0, 10); - BaseStatusService service = new BaseStatusService(config); - var status = new SystemStatusSystems().ok(true); - service.registerStatusCheck("test", () -> status); - MatcherAssert.assertThat(service.getCurrentStatus(), Matchers.is(new SystemStatus().ok(false))); - service.checkStatus(); - assertThat( - service.getCurrentStatus(), - is(new SystemStatus().ok(true).systems(Map.of("test", status)))); - } -} From 7fd15a5ebd74ea14db1115d170b41f43f5beeec6 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Thu, 22 Jun 2023 14:20:01 -0400 Subject: [PATCH 07/46] rename service to lib directory leftover changes --- .github/workflows/build-and-test.yml | 2 +- client/build.gradle | 1 + {service => lib}/build.gradle | 0 {service => lib}/generators.gradle | 0 {service => lib}/publishing.gradle | 0 {service => lib}/src/main/java/bio/terra/javapfb/Library.java | 0 .../src/main/java/bio/terra/javapfb/model/Example.java | 0 .../src/test/java/bio/terra/javapfb/LibraryTest.java | 0 settings.gradle | 3 +-- 9 files changed, 3 insertions(+), 3 deletions(-) rename {service => lib}/build.gradle (100%) rename {service => lib}/generators.gradle (100%) rename {service => lib}/publishing.gradle (100%) rename {service => lib}/src/main/java/bio/terra/javapfb/Library.java (100%) rename {service => lib}/src/main/java/bio/terra/javapfb/model/Example.java (100%) rename {service => lib}/src/test/java/bio/terra/javapfb/LibraryTest.java (100%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e51204df..0a52496d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -34,7 +34,7 @@ jobs: - name: Upload spotbugs results uses: github/codeql-action/upload-sarif@main with: - sarif_file: service/build/reports/spotbugs/main.sarif + sarif_file: lib/build/reports/spotbugs/main.sarif jib: needs: [ build ] diff --git a/client/build.gradle b/client/build.gradle index 1f3ea4b9..48cc2db5 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -5,6 +5,7 @@ plugins { id 'maven-publish' id 'io.spring.dependency-management' id 'com.jfrog.artifactory' version '4.18.2' + // required by java-library-conventions id 'org.hidetake.swagger.generator' } diff --git a/service/build.gradle b/lib/build.gradle similarity index 100% rename from service/build.gradle rename to lib/build.gradle diff --git a/service/generators.gradle b/lib/generators.gradle similarity index 100% rename from service/generators.gradle rename to lib/generators.gradle diff --git a/service/publishing.gradle b/lib/publishing.gradle similarity index 100% rename from service/publishing.gradle rename to lib/publishing.gradle diff --git a/service/src/main/java/bio/terra/javapfb/Library.java b/lib/src/main/java/bio/terra/javapfb/Library.java similarity index 100% rename from service/src/main/java/bio/terra/javapfb/Library.java rename to lib/src/main/java/bio/terra/javapfb/Library.java diff --git a/service/src/main/java/bio/terra/javapfb/model/Example.java b/lib/src/main/java/bio/terra/javapfb/model/Example.java similarity index 100% rename from service/src/main/java/bio/terra/javapfb/model/Example.java rename to lib/src/main/java/bio/terra/javapfb/model/Example.java diff --git a/service/src/test/java/bio/terra/javapfb/LibraryTest.java b/lib/src/test/java/bio/terra/javapfb/LibraryTest.java similarity index 100% rename from service/src/test/java/bio/terra/javapfb/LibraryTest.java rename to lib/src/test/java/bio/terra/javapfb/LibraryTest.java diff --git a/settings.gradle b/settings.gradle index 5451eb0e..28854e07 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,5 @@ rootProject.name = 'javapfb' -include('service', 'client', 'integration') +include('lib', 'client', 'java-pfb-cli') gradle.ext.releaseVersion = '0.11.0' -include 'java-pfb-cli' From d0cc905787aff3a04b2d47745e695138206cd0a4 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 23 Jun 2023 15:57:08 -0400 Subject: [PATCH 08/46] update github workflows update jib GHA --- .github/workflows/build-and-test.yml | 79 +------------------------ .github/workflows/publish.yml | 87 ++++------------------------ 2 files changed, 13 insertions(+), 153 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0a52496d..abdfc4ae 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -58,7 +58,7 @@ jobs: - name: Build image locally with jib run: | - ./gradlew --build-cache :service:jibDockerBuild \ + ./gradlew --build-cache :lib:jibDockerBuild \ --image=${{ steps.image-name.outputs.name }} \ -Djib.console=plain @@ -97,14 +97,6 @@ jobs: unit-tests-and-sonarqube: needs: [ build ] runs-on: ubuntu-latest - -# services: -# postgres: -# image: postgres:13 -# env: -# POSTGRES_PASSWORD: postgres -# ports: [ "5432:5432" ] - steps: - uses: actions/checkout@v3 # Needed by sonar to get the git history for the branch the PR will be merged into. @@ -117,75 +109,6 @@ jobs: distribution: 'temurin' cache: 'gradle' -# - name: Make sure Postgres is ready and init -# env: -# PGPASSWORD: postgres -# run: | -# pg_isready -h localhost -t 10 -# psql -h localhost -U postgres -f ./common/postgres-init.sql - - - name: Test with coverage - run: ./gradlew --build-cache test jacocoTestReport - - # The SonarQube scan is done here, so it can upload the coverage report generated by the tests. - - name: SonarQube scan - run: ./gradlew --build-cache sonarqube - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -# integration-tests: -# needs: [ build ] -# runs-on: ubuntu-latest -# -## services: -## postgres: -## image: postgres:13 -## env: -## POSTGRES_PASSWORD: postgres -## ports: [ "5432:5432" ] -# -# steps: -# - uses: actions/checkout@v3 -# - name: Set up JDK -# uses: actions/setup-java@v3 -# with: -# java-version: '17' -# distribution: 'temurin' -# cache: 'gradle' -# - name: Render GitHub Secrets -# run: | -# echo "${{ secrets.DEV_FIRECLOUD_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/user-delegated-sa.json" -# echo "${{ secrets.PERF_TESTRUNNER_ACCOUNT_B64 }}" | base64 -d > "integration/src/main/resources/rendered/testrunner-perf.json" -# -# - name: Launch the background process for integration tests -# run: ./gradlew --build-cache bootRun | tee application.log & -# -# - name: Wait for boot run to be ready -# run: | -# set +e -# timeout 60 bash -c 'until echo > /dev/tcp/localhost/8080; do sleep 1; done' -# resultStatus=$? -# set -e -# if [[ $resultStatus == 0 ]]; then -# echo "Server started successfully" -# else -# echo "Server did not start successfully" -# exit 1 -# fi -# -# - name: Run the integration test suite -# run: ./gradlew --build-cache runTest --args="suites/local/FullIntegration.json build/reports" -# -# - name: Archive logs -# id: archive_logs -# if: always() -# uses: actions/upload-artifact@v3 -# with: -# name: application-logs -# path: | -# application.log - notify-slack: needs: [ build, unit-tests-and-sonarqube, source-clear ] runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3b0ab7de..fa5a916a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,78 +34,15 @@ jobs: ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} ARTIFACTORY_REPO_KEY: "libs-release-local" -# - name: Auth to Google -# uses: google-github-actions/auth@v1 -# with: -# workload_identity_provider: projects/1038484894585/locations/global/workloadIdentityPools/github-wi-pool/providers/github-wi-provider -# service_account: gcr-publish@broad-dsp-gcr-public.iam.gserviceaccount.com -# -# - name: Setup gcloud -# uses: google-github-actions/setup-gcloud@v1 -# -# - name: Explicitly auth Docker for GCR -# run: gcloud auth configure-docker --quiet -# -# - name: Construct docker image name and tag -# id: image-name -# run: echo "name=gcr.io/${GOOGLE_PROJECT}/${SERVICE_NAME}:${{ steps.tag.outputs.tag }}" >> $GITHUB_OUTPUT -# -# - name: Build image locally with jib -# run: | -# ./gradlew --build-cache :service:jibDockerBuild \ -# --image=${{ steps.image-name.outputs.name }} \ -# -Djib.console=plain -# -# - name: Push GCR image -# run: docker push ${{ steps.image-name.outputs.name }} - - - name: Notify slack on failure - uses: broadinstitute/action-slack@v3.8.0 - if: failure() - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - with: - channel: '#dsp-analysis-journeys-alerts' - status: failure - author_name: Publish to dev - fields: job - text: 'Publish failed :sadpanda:' - username: 'Java-PFB GitHub Action' - - - # - # Hey! You'll probably want to adjust below this point: it deploys newly published versions to dev! - # - # You'll need to create a new chart entry in Beehive first, at https://broad.io/beehive/charts/new (chart - # names can't be changed, so be sure beforehand). Replace 'javatemplate' below with whatever name you choose. - # - # You'll also need to add some access to your new repo to allow it to run these steps. We have docs on the - # whole process here: https://docs.google.com/document/d/1lkUkN2KOpHKWufaqw_RIE7EN3vN4G2xMnYBU83gi8VA/edit#heading=h.ipfs1speial - # - # Lastly, the deployment part won't work until your app has an actual chart and is deployed in dev. We can - # help with that, ping #dsp-devops-champions and we can point you in the right direction. - # - -# report-to-sherlock: -# # Report new version to Broad DevOps -# uses: broadinstitute/sherlock/.github/workflows/client-report-app-version.yaml@main -# needs: publish-job -# with: -# new-version: ${{ needs.publish-job.outputs.tag }} -# chart-name: 'javapfb' -# permissions: -# contents: 'read' -# id-token: 'write' -# -# set-version-in-dev: -# # Put new version in Broad dev environment -# uses: broadinstitute/sherlock/.github/workflows/client-set-environment-app-version.yaml@main -# needs: [publish-job, report-to-sherlock] -# with: -# new-version: ${{ needs.publish-job.outputs.tag }} -# chart-name: 'javapfb' -# environment-name: 'template-services' -# secrets: -# sync-git-token: ${{ secrets.BROADBOT_TOKEN }} -# permissions: -# id-token: 'write' + - name: Notify slack on failure + uses: broadinstitute/action-slack@v3.8.0 + if: failure() + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + with: + channel: '#dsp-analysis-journeys-alerts' + status: failure + author_name: Publish to dev + fields: job + text: 'Publish failed :sadpanda:' + username: 'Java-PFB GitHub Action' \ No newline at end of file From ffbbc44082bfeed32b3710cd2be6c265c9a48599 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 23 Jun 2023 15:58:44 -0400 Subject: [PATCH 09/46] Try slightly different jfrog import --- buildSrc/build.gradle | 6 +++++- client/build.gradle | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index e7f9717a..1ba38197 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -3,8 +3,12 @@ plugins { } repositories { + mavenCentral() maven { - url 'https://broadinstitute.jfrog.io/artifactory/plugins-snapshot' + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release-local/' + } + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' } gradlePluginPortal() } diff --git a/client/build.gradle b/client/build.gradle index 48cc2db5..ffc6d230 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -4,7 +4,7 @@ plugins { id 'bio.terra.java-library-conventions' id 'maven-publish' id 'io.spring.dependency-management' - id 'com.jfrog.artifactory' version '4.18.2' + id 'com.jfrog.artifactory' version '4.32.0' // required by java-library-conventions id 'org.hidetake.swagger.generator' } From 8324e8f9a81127032f3b6614f81d89bb510082d2 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Mon, 10 Jul 2023 12:25:29 -0400 Subject: [PATCH 10/46] Update LICENSE Co-authored-by: Phil Shapiro --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 46c2fd5d..d2b8e01c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022, Broad Institute +Copyright (c) 2023, Broad Institute All rights reserved. Redistribution and use in source and binary forms, with or without From 2fb38b5e53c996bb2c94f89d507015367f3b50a9 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Mon, 10 Jul 2023 12:26:21 -0400 Subject: [PATCH 11/46] Update release version --- settings.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 28854e07..5f5df76f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ + rootProject.name = 'javapfb' include('lib', 'client', 'java-pfb-cli') -gradle.ext.releaseVersion = '0.11.0' +gradle.ext.releaseVersion = '0.1.0' From faf6fa1fdd5c44253be04dcd46405f35e2b67b4a Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Mon, 10 Jul 2023 16:08:08 -0400 Subject: [PATCH 12/46] Update gradle version --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d7e66b5c..31cca491 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From b909cd9c1fd235a962468518c8b04fbd6e7404ae Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Mon, 10 Jul 2023 21:25:32 -0400 Subject: [PATCH 13/46] Remove spring; rename lib to library better --- .github/workflows/build-and-test.yml | 42 +------- .github/workflows/trivy.yml | 51 ---------- buildSrc/build.gradle | 30 ------ ....terra.java-application-conventions.gradle | 6 -- .../bio.terra.java-common-conventions.gradle | 99 ------------------- .../bio.terra.java-library-conventions.gradle | 4 - .../bio.terra.java-spring-conventions.gradle | 7 -- {java-pfb-cli => cli}/README.md | 0 {java-pfb-cli => cli}/build.gradle | 0 .../src/main/java/JavaPfbCommand.java | 0 client/artifactory.gradle | 49 --------- client/build.gradle | 18 ---- lib/build.gradle | 52 ---------- lib/publishing.gradle | 45 --------- .../main/java/bio/terra/javapfb/Library.java | 45 --------- .../java/bio/terra/javapfb/LibraryTest.java | 14 --- library/build.gradle | 42 ++++++++ {lib => library}/generators.gradle | 12 --- .../main/java/bio/terra/javapfb/Library.java | 6 ++ .../java/bio/terra/javapfb/model/Example.java | 0 .../java/bio/terra/javapfb/LibraryTest.java | 11 +++ settings.gradle | 2 +- 22 files changed, 61 insertions(+), 474 deletions(-) delete mode 100644 .github/workflows/trivy.yml delete mode 100644 buildSrc/build.gradle delete mode 100644 buildSrc/src/main/groovy/bio.terra.java-application-conventions.gradle delete mode 100644 buildSrc/src/main/groovy/bio.terra.java-common-conventions.gradle delete mode 100644 buildSrc/src/main/groovy/bio.terra.java-library-conventions.gradle delete mode 100644 buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle rename {java-pfb-cli => cli}/README.md (100%) rename {java-pfb-cli => cli}/build.gradle (100%) rename {java-pfb-cli => cli}/src/main/java/JavaPfbCommand.java (100%) delete mode 100644 client/artifactory.gradle delete mode 100644 client/build.gradle delete mode 100644 lib/build.gradle delete mode 100644 lib/publishing.gradle delete mode 100644 lib/src/main/java/bio/terra/javapfb/Library.java delete mode 100644 lib/src/test/java/bio/terra/javapfb/LibraryTest.java create mode 100644 library/build.gradle rename {lib => library}/generators.gradle (51%) create mode 100644 library/src/main/java/bio/terra/javapfb/Library.java rename {lib => library}/src/main/java/bio/terra/javapfb/model/Example.java (100%) create mode 100644 library/src/test/java/bio/terra/javapfb/LibraryTest.java diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index abdfc4ae..7714855f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -34,47 +34,7 @@ jobs: - name: Upload spotbugs results uses: github/codeql-action/upload-sarif@main with: - sarif_file: lib/build/reports/spotbugs/main.sarif - - jib: - needs: [ build ] - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' - - - name: Construct docker image name and tag - id: image-name - run: | - GITHUB_REPO=$(basename ${{ github.repository }}) - GIT_SHORT_HASH=$(git rev-parse --short HEAD) - echo "name=${GITHUB_REPO}:${GIT_SHORT_HASH}" >> $GITHUB_OUTPUT - - - name: Build image locally with jib - run: | - ./gradlew --build-cache :lib:jibDockerBuild \ - --image=${{ steps.image-name.outputs.name }} \ - -Djib.console=plain - - dispatch-trivy: - needs: [ build ] - runs-on: ubuntu-latest - - if: github.event_name == 'pull_request' - - steps: - - name: Fire off Trivy action - uses: broadinstitute/workflow-dispatch@v1 - with: - workflow: Trivy - token: ${{ secrets.BROADBOT_TOKEN }} - ref: ${{ github.event.pull_request.head.ref }} + sarif_file: library/build/reports/spotbugs/main.sarif source-clear: needs: [ build ] diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml deleted file mode 100644 index 60ac2fdd..00000000 --- a/.github/workflows/trivy.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Trivy -on: workflow_dispatch - -jobs: - trivy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' - - - name: Build all projects without running tests - run: ./gradlew --build-cache build -x test -x spotlessCheck - - - name: Construct docker image name and tag - id: image-name - run: | - echo "name=trivy-local-testing-image" >> $GITHUB_OUTPUT - - - name: Build image locally with jib - run: | - ./gradlew --build-cache :service:jibDockerBuild \ - --image=${{ steps.image-name.outputs.name }} \ - -Djib.console=plain - - - name: Run Trivy vulnerability scanner - uses: broadinstitute/dsp-appsec-trivy-action@v1 - with: - image: ${{ steps.image-name.outputs.name }} - - notify-slack: - needs: [ trivy ] - runs-on: ubuntu-latest - if: failure() - steps: - - name: Notify slack on failure - uses: broadinstitute/action-slack@v3.8.0 - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - with: - channel: '#dsp-analysis-journeys-alerts' - status: failure - author_name: Trivy action - fields: workflow,message - text: 'Trivy scan failure :sadpanda:' - username: 'Java-PFB GitHub Action' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle deleted file mode 100644 index 1ba38197..00000000 --- a/buildSrc/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -plugins { - id 'groovy-gradle-plugin' -} - -repositories { - mavenCentral() - maven { - url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release-local/' - } - maven { - url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' - } - gradlePluginPortal() -} - -dependencies { - implementation 'com.diffplug.spotless:spotless-plugin-gradle:6.11.0' - implementation 'com.felipefzdz.gradle.shellcheck:shellcheck:1.4.6' - implementation 'com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:3.3.0' - implementation 'com.srcclr.gradle:com.srcclr.gradle.gradle.plugin:3.1.12' - implementation 'de.undercouch.download:de.undercouch.download.gradle.plugin:5.2.0' - implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' - implementation 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.0.14.RELEASE' - implementation 'org.hidetake.swagger.generator:org.hidetake.swagger.generator.gradle.plugin:2.19.2' - implementation 'org.sonarqube:org.sonarqube.gradle.plugin:3.4.0.2513' - implementation 'org.springframework.boot:spring-boot-gradle-plugin:2.7.0' - implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' - // This is required due to a dependency conflict between jib and srcclr. Removing it will cause jib to fail. - implementation 'org.apache.commons:commons-compress:1.21' -} diff --git a/buildSrc/src/main/groovy/bio.terra.java-application-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-application-conventions.gradle deleted file mode 100644 index 329b117c..00000000 --- a/buildSrc/src/main/groovy/bio.terra.java-application-conventions.gradle +++ /dev/null @@ -1,6 +0,0 @@ -plugins { - // Apply the common convention plugin for shared build configuration between library and application projects. - id 'bio.terra.java-common-conventions' - - id 'application' -} diff --git a/buildSrc/src/main/groovy/bio.terra.java-common-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-common-conventions.gradle deleted file mode 100644 index d0f33742..00000000 --- a/buildSrc/src/main/groovy/bio.terra.java-common-conventions.gradle +++ /dev/null @@ -1,99 +0,0 @@ -plugins { - id 'idea' - id 'jacoco' - id 'java' - - id 'com.diffplug.spotless' - id 'com.github.spotbugs' - id 'org.hidetake.swagger.generator' -} - -boolean isCiServer = System.getenv().containsKey("CI") - -if (!isCiServer) { - tasks.withType(JavaExec).configureEach { - systemProperty 'spring.profiles.include', 'human-readable-logging' - } - tasks.withType(Test).configureEach { - systemProperty 'spring.profiles.include', 'human-readable-logging' - } -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } -} - -repositories { - maven { - // Terra proxy for maven central - url 'https://broadinstitute.jfrog.io/broadinstitute/maven-central/' - } - mavenCentral() - maven { - url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release/' - } - maven { - url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' - } -} - -dependencies { - compileOnly 'com.github.spotbugs:spotbugs-annotations:4.7.2' - implementation 'io.swagger.core.v3:swagger-annotations:2.2.0' - swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.31' - - implementation 'org.slf4j:slf4j-api' - - testImplementation 'org.hamcrest:hamcrest:2.2' - - implementation 'bio.terra:terra-common-lib:0.0.75-SNAPSHOT' - implementation 'bio.terra:datarepo-client:1.349.0-SNAPSHOT' -} - -tasks.named('test') { - useJUnitPlatform() -} - -version = gradle.releaseVersion -group = 'bio.terra' - -spotless { - java { - targetExclude "${buildDir}/**" - targetExclude "**/swagger-code/**" - googleJavaFormat() - } -} - -// Run spotless check when running in github actions, otherwise run spotless apply. -compileJava { - if (isCiServer) { - dependsOn(spotlessCheck) - } else { - dependsOn(spotlessApply) - } -} - -// Spotbugs configuration -spotbugs { - reportLevel = 'high' - effort = 'max' -} -spotbugsMain { - reports { - if (isCiServer) { - sarif.enabled = true - } else { - html.enabled = true - } - } -} - -jacocoTestReport { - reports { - // sonarqube requires XML coverage output to upload coverage data - xml.required = true - } -} diff --git a/buildSrc/src/main/groovy/bio.terra.java-library-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-library-conventions.gradle deleted file mode 100644 index 81623b76..00000000 --- a/buildSrc/src/main/groovy/bio.terra.java-library-conventions.gradle +++ /dev/null @@ -1,4 +0,0 @@ -plugins { - id 'bio.terra.java-common-conventions' - id 'java-library' -} diff --git a/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle deleted file mode 100644 index df2a4be5..00000000 --- a/buildSrc/src/main/groovy/bio.terra.java-spring-conventions.gradle +++ /dev/null @@ -1,7 +0,0 @@ -plugins { - // Apply the common convention plugin for shared build configuration between library and application projects. - id 'bio.terra.java-common-conventions' - - id 'io.spring.dependency-management' - id 'org.springframework.boot' -} diff --git a/java-pfb-cli/README.md b/cli/README.md similarity index 100% rename from java-pfb-cli/README.md rename to cli/README.md diff --git a/java-pfb-cli/build.gradle b/cli/build.gradle similarity index 100% rename from java-pfb-cli/build.gradle rename to cli/build.gradle diff --git a/java-pfb-cli/src/main/java/JavaPfbCommand.java b/cli/src/main/java/JavaPfbCommand.java similarity index 100% rename from java-pfb-cli/src/main/java/JavaPfbCommand.java rename to cli/src/main/java/JavaPfbCommand.java diff --git a/client/artifactory.gradle b/client/artifactory.gradle deleted file mode 100644 index 008de4d5..00000000 --- a/client/artifactory.gradle +++ /dev/null @@ -1,49 +0,0 @@ -// This and the test below makes sure the build will fail reasonably if you try -// to publish without the environment variables defined. -def artifactory_username = System.getenv("ARTIFACTORY_USERNAME") -def artifactory_password = System.getenv("ARTIFACTORY_PASSWORD") -def artifactory_repo_key = System.getenv("ARTIFACTORY_REPO_KEY") - -gradle.taskGraph.whenReady { taskGraph -> - if (taskGraph.hasTask(artifactoryPublish) && - (artifactory_username == null || artifactory_password == null)) { - throw new GradleException("Set env vars ARTIFACTORY_USERNAME and ARTIFACTORY_PASSWORD to publish") - } -} - -java { - // Builds sources into the published package as part of the 'assemble' task. - withSourcesJar() -} - -publishing { - publications { - javaPfbClientLibrary(MavenPublication) { - artifactId = "javapfb-client" - from components.java - versionMapping { - usage("java-runtime") { - fromResolutionResult() - } - } - } - } -} - -artifactory { - publish { - contextUrl = "https://broadinstitute.jfrog.io/broadinstitute/" - repository { - repoKey = "${artifactory_repo_key}" // The Artifactory repository key to publish to - username = "${artifactory_username}" // The publisher user name - password = "${artifactory_password}" // The publisher password - } - defaults { - // This is how we tell the Artifactory Plugin which artifacts should be published to Artifactory. - // Reference to Gradle publications defined in the build script. - publications("javapfbClientLibrary") - publishArtifacts = true - publishPom = true - } - } -} diff --git a/client/build.gradle b/client/build.gradle deleted file mode 100644 index ffc6d230..00000000 --- a/client/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -import org.springframework.boot.gradle.plugin.SpringBootPlugin - -plugins { - id 'bio.terra.java-library-conventions' - id 'maven-publish' - id 'io.spring.dependency-management' - id 'com.jfrog.artifactory' version '4.32.0' - // required by java-library-conventions - id 'org.hidetake.swagger.generator' -} - -dependencyManagement { - imports { - mavenBom(SpringBootPlugin.BOM_COORDINATES) - } -} - -apply from: 'artifactory.gradle' diff --git a/lib/build.gradle b/lib/build.gradle deleted file mode 100644 index f4fabc34..00000000 --- a/lib/build.gradle +++ /dev/null @@ -1,52 +0,0 @@ -plugins { - id 'bio.terra.java-spring-conventions' - id 'de.undercouch.download' - id 'com.google.cloud.tools.jib' - id 'com.srcclr.gradle' - id 'org.sonarqube' - - id 'com.gorylenko.gradle-git-properties' version '2.3.1' - id 'org.liquibase.gradle' version '2.1.0' -} - -apply from: 'generators.gradle' -apply from: 'publishing.gradle' - -dependencies { - implementation 'bio.terra:terra-common-lib' - implementation 'org.apache.commons:commons-dbcp2' - implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.retry:spring-retry' - implementation 'org.broadinstitute.dsde.workbench:sam-client_2.13:0.1-9867891-SNAP' - implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' - implementation 'org.postgresql:postgresql' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-actuator:3.0.2' - implementation 'io.micrometer:micrometer-registry-prometheus:1.10.4' - - liquibaseRuntime 'org.liquibase:liquibase-core' - liquibaseRuntime 'info.picocli:picocli:4.6.1' - liquibaseRuntime 'org.postgresql:postgresql' - liquibaseRuntime 'ch.qos.logback:logback-classic' - - testImplementation 'org.junit.jupiter:junit-jupiter-api' - testImplementation('org.springframework.boot:spring-boot-starter-test') { - // Fixes warning about multiple occurrences of JSONObject on the classpath - exclude group: 'com.vaadin.external.google', module: 'android-json' - } - testImplementation 'org.mockito:mockito-inline' -} - -test { - useJUnitPlatform () -} - -sonarqube { - properties { - property 'sonar.projectName', 'terra-java-project-template' - property 'sonar.projectKey', 'terra-java-project-template' - property 'sonar.organization', 'broad-databiosphere' - property 'sonar.host.url', 'https://sonarcloud.io' - } -} diff --git a/lib/publishing.gradle b/lib/publishing.gradle deleted file mode 100644 index d104842a..00000000 --- a/lib/publishing.gradle +++ /dev/null @@ -1,45 +0,0 @@ -import java.time.ZonedDateTime - -// Download and extract the Cloud Profiler Java Agent -ext { - // where to place the Cloud Profiler agent in the container - cloudProfilerLocation = "/opt/cprof" - - // location for jib extras, including the Java agent - jibExtraDirectory = "${buildDir}/jib-agents" -} -task downloadProfilerAgent(type: Download) { - // where to download the Cloud Profiler agent https://cloud.google.com/profiler/docs/profiling-java - src "https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent.tar.gz" - dest "${buildDir}/cprof_java_agent_gce.tar.gz" -} -task extractProfilerAgent(dependsOn: downloadProfilerAgent, type: Copy) { - from tarTree(downloadProfilerAgent.dest) - into "${jibExtraDirectory}/${cloudProfilerLocation}" -} - -jib { - from { - // see https://github.com/broadinstitute/dsp-appsec-blessed-images/tree/main/jre - image = "us.gcr.io/broad-dsp-gcr-public/base/jre:17-distroless" - } - extraDirectories { - paths = [file(jibExtraDirectory)] - } - container { - filesModificationTime = ZonedDateTime.now().toString() // to prevent ui caching - mainClass = 'bio.terra.javapfb.App' - jvmFlags = [ - "-agentpath:" + cloudProfilerLocation + "/profiler_java_agent.so=" + - "-cprof_service=bio.terra.javapfb" + - ",-cprof_service_version=" + version + - ",-cprof_enable_heap_sampling=true" + - ",-logtostderr" + - ",-minloglevel=2" - ] - } -} - -tasks.jib.dependsOn extractProfilerAgent -tasks.jibDockerBuild.dependsOn extractProfilerAgent -tasks.jibBuildTar.dependsOn extractProfilerAgent diff --git a/lib/src/main/java/bio/terra/javapfb/Library.java b/lib/src/main/java/bio/terra/javapfb/Library.java deleted file mode 100644 index d57cb29c..00000000 --- a/lib/src/main/java/bio/terra/javapfb/Library.java +++ /dev/null @@ -1,45 +0,0 @@ -package bio.terra.javapfb; - -import bio.terra.common.logging.LoggingInitializer; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.retry.annotation.EnableRetry; -import org.springframework.transaction.annotation.EnableTransactionManagement; - -@SpringBootApplication( - scanBasePackages = { - // Scan for logging-related components & configs - "bio.terra.common.logging", - // Scan for tracing-related components & configs - "bio.terra.common.tracing", - // Scan all service-specific packages beneath the current package - "bio.terra.javapfb" - }) -@ConfigurationPropertiesScan("bio.terra.javapfb") -@EnableRetry -@EnableTransactionManagement -@EnableConfigurationProperties -public class Library { - public static void main(String[] args) { - new SpringApplicationBuilder(Library.class).initializers(new LoggingInitializer()).run(args); - } - - public Library() {} - - @Bean("objectMapper") - public ObjectMapper objectMapper() { - return new ObjectMapper() - .registerModule(new ParameterNamesModule()) - .registerModule(new Jdk8Module()) - .registerModule(new JavaTimeModule()) - .setDefaultPropertyInclusion(JsonInclude.Include.NON_ABSENT); - } -} diff --git a/lib/src/test/java/bio/terra/javapfb/LibraryTest.java b/lib/src/test/java/bio/terra/javapfb/LibraryTest.java deleted file mode 100644 index 4ce54e1c..00000000 --- a/lib/src/test/java/bio/terra/javapfb/LibraryTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package bio.terra.javapfb; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@ActiveProfiles({"test", "human-readable-logging"}) -public abstract class LibraryTest { - @Test - public void testHelloWorld() { - System.out.println("Hello World"); - } -} diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 00000000..7cd63091 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'groovy-gradle-plugin' + id 'com.gorylenko.gradle-git-properties' version '2.3.1' +} + +apply from: 'generators.gradle' + + +test { + useJUnitPlatform () +} + +//sonarqube { +// properties { +// property 'sonar.projectName', 'terra-java-project-template' +// property 'sonar.projectKey', 'terra-java-project-template' +// property 'sonar.organization', 'broad-databiosphere' +// property 'sonar.host.url', 'https://sonarcloud.io' +// } +//} + +repositories { + mavenCentral() + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release-local/' + } + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' + } + gradlePluginPortal() +} + +dependencies { + implementation 'com.diffplug.spotless:spotless-plugin-gradle:6.11.0' + implementation 'com.felipefzdz.gradle.shellcheck:shellcheck:1.4.6' + implementation 'com.srcclr.gradle:com.srcclr.gradle.gradle.plugin:3.1.12' + implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' + implementation 'org.sonarqube:org.sonarqube.gradle.plugin:3.4.0.2513' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' +} + diff --git a/lib/generators.gradle b/library/generators.gradle similarity index 51% rename from lib/generators.gradle rename to library/generators.gradle index 6b39c52c..5c0ed6a1 100644 --- a/lib/generators.gradle +++ b/library/generators.gradle @@ -1,15 +1,3 @@ -dependencies { - implementation 'io.swagger.core.v3:swagger-annotations' - runtimeOnly 'org.webjars.npm:swagger-ui-dist:4.9.0' - swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli' - - // Versioned by Spring: - implementation 'javax.validation:validation-api' - implementation 'org.webjars:webjars-locator-core' - - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' -} - def artifactGroup = "${group}.javapfb" // see https://github.com/n0mer/gradle-git-properties diff --git a/library/src/main/java/bio/terra/javapfb/Library.java b/library/src/main/java/bio/terra/javapfb/Library.java new file mode 100644 index 00000000..ccd7d0e9 --- /dev/null +++ b/library/src/main/java/bio/terra/javapfb/Library.java @@ -0,0 +1,6 @@ +package bio.terra.javapfb; + +public class Library { + public static void main(String[] args) { + } +} diff --git a/lib/src/main/java/bio/terra/javapfb/model/Example.java b/library/src/main/java/bio/terra/javapfb/model/Example.java similarity index 100% rename from lib/src/main/java/bio/terra/javapfb/model/Example.java rename to library/src/main/java/bio/terra/javapfb/model/Example.java diff --git a/library/src/test/java/bio/terra/javapfb/LibraryTest.java b/library/src/test/java/bio/terra/javapfb/LibraryTest.java new file mode 100644 index 00000000..86f108ef --- /dev/null +++ b/library/src/test/java/bio/terra/javapfb/LibraryTest.java @@ -0,0 +1,11 @@ +package bio.terra.javapfb; + +import org.junit.jupiter.api.Test; + +public class LibraryTest { + + @Test + public void testHelloWorld() { + System.out.println("Hello World"); + } +} diff --git a/settings.gradle b/settings.gradle index 5f5df76f..215ed775 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ rootProject.name = 'javapfb' -include('lib', 'client', 'java-pfb-cli') +include('library', 'cli') gradle.ext.releaseVersion = '0.1.0' From 2e041ac965f2943b2b7885de12a7c87af787b7b4 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Thu, 13 Jul 2023 10:43:27 -0400 Subject: [PATCH 14/46] rework build.gradle rework --- buildSrc/build.gradle | 34 +++++++ ...ra.pfb.java-application-conventions.gradle | 5 + ...o.terra.pfb.java-common-conventions.gradle | 99 +++++++++++++++++++ ....terra.pfb.java-library-conventions.gradle | 4 + library/build.gradle | 74 ++++++++------ .../main/java/bio/terra/javapfb/Library.java | 3 +- .../java/bio/terra/javapfb/model/Example.java | 15 --- 7 files changed, 188 insertions(+), 46 deletions(-) create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/groovy/bio.terra.pfb.java-application-conventions.gradle create mode 100644 buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle create mode 100644 buildSrc/src/main/groovy/bio.terra.pfb.java-library-conventions.gradle delete mode 100644 library/src/main/java/bio/terra/javapfb/model/Example.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 00000000..634e7286 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + mavenCentral() + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release-local/' + } + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' + } + gradlePluginPortal() +} + +dependencies { + implementation 'com.diffplug.spotless:spotless-plugin-gradle:6.11.0' + implementation 'com.felipefzdz.gradle.shellcheck:shellcheck:1.4.6' +// implementation 'com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:3.3.0' + implementation 'com.srcclr.gradle:com.srcclr.gradle.gradle.plugin:3.1.12' + implementation 'de.undercouch.download:de.undercouch.download.gradle.plugin:5.2.0' + implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' +// implementation 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.0.14.RELEASE' +// implementation 'org.hidetake.swagger.generator:org.hidetake.swagger.generator.gradle.plugin:2.19.2' + implementation 'org.sonarqube:org.sonarqube.gradle.plugin:3.4.0.2513' +// implementation 'org.springframework.boot:spring-boot-gradle-plugin:2.7.0' +// implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' +// // This is required due to a dependency conflict between jib and srcclfr. Removing it will cause jib to fail. +// implementation 'org.apache.commons:commons-compress:1.21' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/bio.terra.pfb.java-application-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.pfb.java-application-conventions.gradle new file mode 100644 index 00000000..5de6f188 --- /dev/null +++ b/buildSrc/src/main/groovy/bio.terra.pfb.java-application-conventions.gradle @@ -0,0 +1,5 @@ +plugins { + // Apply the common convention plugin for shared build configuration between library and application projects. + id 'bio.terra.pfb.java-common-conventions' + id 'application' +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle new file mode 100644 index 00000000..62708874 --- /dev/null +++ b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle @@ -0,0 +1,99 @@ +plugins { + id 'idea' + id 'jacoco' + id 'java' + + id 'com.diffplug.spotless' + id 'com.github.spotbugs' +// id 'org.hidetake.swagger.generator' +} + +boolean isCiServer = System.getenv().containsKey("CI") + +//if (!isCiServer) { +// tasks.withType(JavaExec).configureEach { +// systemProperty 'spring.profiles.include', 'human-readable-logging' +// } +// tasks.withType(Test).configureEach { +// systemProperty 'spring.profiles.include', 'human-readable-logging' +// } +//} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + maven { + // Terra proxy for maven central + url 'https://broadinstitute.jfrog.io/broadinstitute/maven-central/' + } + mavenCentral() + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release/' + } + maven { + url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' + } +} + +dependencies { + compileOnly 'com.github.spotbugs:spotbugs-annotations:4.7.2' +// implementation 'io.swagger.core.v3:swagger-annotations:2.2.0' +// swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.31' +// +// implementation 'org.slf4j:slf4j-api' + + testImplementation 'org.hamcrest:hamcrest:2.2' +// +// implementation 'bio.terra:terra-common-lib:0.0.75-SNAPSHOT' +// implementation 'bio.terra:datarepo-client:1.349.0-SNAPSHOT' +} + +tasks.named('test') { + useJUnitPlatform() +} + +version = gradle.releaseVersion +group = 'bio.terra' + +spotless { + java { + targetExclude "${buildDir}/**" +// targetExclude "**/swagger-code/**" + googleJavaFormat() + } +} + +// Run spotless check when running in github actions, otherwise run spotless apply. +compileJava { + if (isCiServer) { + dependsOn(spotlessCheck) + } else { + dependsOn(spotlessApply) + } +} + +// Spotbugs configuration +spotbugs { + reportLevel = 'high' + effort = 'max' +} +spotbugsMain { + reports { + if (isCiServer) { + sarif.enabled = true + } else { + html.enabled = true + } + } +} + +jacocoTestReport { + reports { + // sonarqube requires XML coverage output to upload coverage data + xml.required = true + } +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/bio.terra.pfb.java-library-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.pfb.java-library-conventions.gradle new file mode 100644 index 00000000..2cbe90af --- /dev/null +++ b/buildSrc/src/main/groovy/bio.terra.pfb.java-library-conventions.gradle @@ -0,0 +1,4 @@ +plugins { + id 'bio.terra.pfb.java-common-conventions' + id 'java-library' +} \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index 7cd63091..bbebc0f4 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,42 +1,58 @@ plugins { - id 'groovy-gradle-plugin' - id 'com.gorylenko.gradle-git-properties' version '2.3.1' -} - -apply from: 'generators.gradle' + id 'de.undercouch.download' + id 'com.srcclr.gradle' + id 'org.sonarqube' + id 'com.gorylenko.gradle-git-properties' version '2.3.1' -test { - useJUnitPlatform () + id 'bio.terra.pfb.java-library-conventions' } -//sonarqube { -// properties { -// property 'sonar.projectName', 'terra-java-project-template' -// property 'sonar.projectKey', 'terra-java-project-template' -// property 'sonar.organization', 'broad-databiosphere' -// property 'sonar.host.url', 'https://sonarcloud.io' -// } -//} - repositories { mavenCentral() - maven { - url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release-local/' - } - maven { - url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' - } - gradlePluginPortal() } +apply from: 'generators.gradle' +//apply from: 'publishing.gradle' + dependencies { - implementation 'com.diffplug.spotless:spotless-plugin-gradle:6.11.0' - implementation 'com.felipefzdz.gradle.shellcheck:shellcheck:1.4.6' - implementation 'com.srcclr.gradle:com.srcclr.gradle.gradle.plugin:3.1.12' - implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' - implementation 'org.sonarqube:org.sonarqube.gradle.plugin:3.4.0.2513' +// implementation 'bio.terra:terra-common-lib' +// implementation 'org.apache.commons:commons-dbcp2' +// implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' +// implementation 'org.springframework.boot:spring-boot-starter-web' +// implementation 'org.springframework.retry:spring-retry' +// implementation 'org.broadinstitute.dsde.workbench:sam-client_2.13:0.1-9867891-SNAP' + implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' +// implementation 'javax.annotation:javax.annotation-api:1.3.2' +// implementation 'org.postgresql:postgresql' +// implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' +// implementation 'org.springframework.boot:spring-boot-starter-actuator:3.0.2' +// implementation 'io.micrometer:micrometer-registry-prometheus:1.10.4' + +// liquibaseRuntime 'org.liquibase:liquibase-core' + implementation'info.picocli:picocli:4.6.1' + implementation 'org.junit.jupiter:junit-jupiter:5.8.1' +// liquibaseRuntime 'org.postgresql:postgresql' +// liquibaseRuntime 'ch.qos.logback:logback-classic' + + +// testImplementation('org.springframework.boot:spring-boot-starter-test') { +// // Fixes warning about multiple occurrences of JSONObject on the classpath +// exclude group: 'com.vaadin.external.google', module: 'android-json' +// } +// testImplementation 'org.junit.jupiter:junit-jupiter-api' +// testImplementation 'org.mockito:mockito-inline' +} - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' +test { + useJUnitPlatform () } +sonarqube { + properties { + property 'sonar.projectName', 'java-pfb' + property 'sonar.projectKey', 'java-pfb' + property 'sonar.organization', 'broad-databiosphere' + property 'sonar.host.url', 'https://sonarcloud.io' + } +} \ No newline at end of file diff --git a/library/src/main/java/bio/terra/javapfb/Library.java b/library/src/main/java/bio/terra/javapfb/Library.java index ccd7d0e9..d8ca80ed 100644 --- a/library/src/main/java/bio/terra/javapfb/Library.java +++ b/library/src/main/java/bio/terra/javapfb/Library.java @@ -1,6 +1,5 @@ package bio.terra.javapfb; public class Library { - public static void main(String[] args) { - } + public static void main(String[] args) {} } diff --git a/library/src/main/java/bio/terra/javapfb/model/Example.java b/library/src/main/java/bio/terra/javapfb/model/Example.java deleted file mode 100644 index 0ac4c49f..00000000 --- a/library/src/main/java/bio/terra/javapfb/model/Example.java +++ /dev/null @@ -1,15 +0,0 @@ -package bio.terra.javapfb.model; - -import java.util.Objects; -import javax.annotation.Nullable; - -public record Example(@Nullable Long id, String userId, String message) { - public Example { - Objects.requireNonNull(userId); - Objects.requireNonNull(message); - } - - public Example(String userId, String message) { - this(null, userId, message); - } -} From 41b8068801498be1067d388d2e510a3a39da603a Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 14 Jul 2023 07:50:51 -0400 Subject: [PATCH 15/46] Update build/test GHA: Remove python check; add best test and sonar scan steps spacing --- .github/workflows/build-and-test.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7714855f..cafe23d7 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -13,14 +13,6 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Install black and link shellcheck into expected location - run: | - pip install black --force-reinstall black==22.3.0 - sudo ln -s $(which shellcheck) /usr/local/bin/shellcheck - name: Set up JDK uses: actions/setup-java@v3 with: @@ -68,6 +60,14 @@ jobs: java-version: '17' distribution: 'temurin' cache: 'gradle' + - name: Test with coverage + run: ./gradlew --build-cache test jacocoTestReport + # The SonarQube scan is done here, so it can upload the coverage report generated by the tests. + - name: SonarQube scan + run: ./gradlew --build-cache sonarqube + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} notify-slack: needs: [ build, unit-tests-and-sonarqube, source-clear ] From dac7e437aef6c4425c82dbdde05d97501f6217b6 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 14 Jul 2023 08:51:55 -0400 Subject: [PATCH 16/46] update sonar configuration Update sonar scan fix spacing sonar command set config update project key Revert "update sonar configuration" This reverts commit 6875df0c6a388fcb37d89979944d6512a1e21137. Revert "Revert "update sonar configuration"" This reverts commit bf5451fa47b48a46e7d9d78601b22e14603922fe. Need project name --- .github/workflows/build-and-test.yml | 7 +++---- buildSrc/build.gradle | 2 +- library/build.gradle | 10 +++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index cafe23d7..7d4d117d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -62,12 +62,11 @@ jobs: cache: 'gradle' - name: Test with coverage run: ./gradlew --build-cache test jacocoTestReport - # The SonarQube scan is done here, so it can upload the coverage report generated by the tests. - - name: SonarQube scan - run: ./gradlew --build-cache sonarqube + - name: Build and analyze env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew --build-cache sonar notify-slack: needs: [ build, unit-tests-and-sonarqube, source-clear ] diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 634e7286..1e305e87 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -22,7 +22,7 @@ dependencies { implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' // implementation 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.0.14.RELEASE' // implementation 'org.hidetake.swagger.generator:org.hidetake.swagger.generator.gradle.plugin:2.19.2' - implementation 'org.sonarqube:org.sonarqube.gradle.plugin:3.4.0.2513' + implementation 'org.sonarqube:org.sonarqube.gradle.plugin:4.2.1.3168' // implementation 'org.springframework.boot:spring-boot-gradle-plugin:2.7.0' // implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' // // This is required due to a dependency conflict between jib and srcclfr. Removing it will cause jib to fail. diff --git a/library/build.gradle b/library/build.gradle index bbebc0f4..42d4a16f 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -48,11 +48,11 @@ test { useJUnitPlatform () } -sonarqube { +sonar { properties { - property 'sonar.projectName', 'java-pfb' - property 'sonar.projectKey', 'java-pfb' - property 'sonar.organization', 'broad-databiosphere' - property 'sonar.host.url', 'https://sonarcloud.io' + property "sonar.projectKey", "java-pfb" + property "sonar.projectName", "java-pfb" + property "sonar.organization", "broad-databiosphere" + property "sonar.host.url", "https://sonarcloud.io" } } \ No newline at end of file From 834ce97db7fe080623343a0080a094b56706d286 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 14 Jul 2023 10:51:16 -0400 Subject: [PATCH 17/46] info all the info --- .github/workflows/build-and-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7d4d117d..9b047823 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -61,12 +61,12 @@ jobs: distribution: 'temurin' cache: 'gradle' - name: Test with coverage - run: ./gradlew --build-cache test jacocoTestReport + run: ./gradlew --build-cache test jacocoTestReport --info - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew --build-cache sonar + run: ./gradlew --build-cache sonar --info notify-slack: needs: [ build, unit-tests-and-sonarqube, source-clear ] From 460bececbf48bb4ab47b207652e8fa251b4fccd7 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 14 Jul 2023 11:31:11 -0400 Subject: [PATCH 18/46] Update readme --- .github/workflows/build-and-test.yml | 2 +- README.md | 31 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 9b047823..0e8bc470 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -66,7 +66,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew --build-cache sonar --info + run: ./gradlew --build-cache :library:sonarqube --info notify-slack: needs: [ build, unit-tests-and-sonarqube, source-clear ] diff --git a/README.md b/README.md index 3b844636..46172227 100644 --- a/README.md +++ b/README.md @@ -1 +1,32 @@ # java-pfb + +## Running SourceClear locally + +[SourceClear](https://srcclr.github.io) is a static analysis tool that scans a project's Java +dependencies for known vulnerabilities. If you get a build failure due a SourceClear error and want +to debug the problem locally, you need to get the API token from vault before running the gradle +task. + +```shell +export SRCCLR_API_TOKEN=$(vault read -field=api_token secret/secops/ci/srcclr/gradle-agent) +./gradlew srcclr +``` + +## Running SonarQube locally + +[SonarQube](https://www.sonarqube.org) is a static analysis code that scans code for a wide +range of issues, including maintainability and possible bugs. If you get a build failure due to +SonarQube and want to debug the problem locally, you need to get the the sonar token from vault +before runing the gradle task. + +```shell +export SONAR_TOKEN=$(vault read -field=sonar_token secret/secops/ci/sonarcloud/java-pfb) +./gradlew sonarqube +``` + +Unlike SourceClear, running this task produces no output unless your project has errors. To always +generate a report, run using `--info`: + +```shell +./gradlew sonarqube --info +``` From fc87d7fdee4528f8a04c54564a12839414db36d7 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 14 Jul 2023 11:38:30 -0400 Subject: [PATCH 19/46] switch back to sonarqube command --- buildSrc/build.gradle | 2 +- library/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 1e305e87..634e7286 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -22,7 +22,7 @@ dependencies { implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' // implementation 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.0.14.RELEASE' // implementation 'org.hidetake.swagger.generator:org.hidetake.swagger.generator.gradle.plugin:2.19.2' - implementation 'org.sonarqube:org.sonarqube.gradle.plugin:4.2.1.3168' + implementation 'org.sonarqube:org.sonarqube.gradle.plugin:3.4.0.2513' // implementation 'org.springframework.boot:spring-boot-gradle-plugin:2.7.0' // implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' // // This is required due to a dependency conflict between jib and srcclfr. Removing it will cause jib to fail. diff --git a/library/build.gradle b/library/build.gradle index 42d4a16f..fcc12943 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -48,7 +48,7 @@ test { useJUnitPlatform () } -sonar { +sonarqube { properties { property "sonar.projectKey", "java-pfb" property "sonar.projectName", "java-pfb" From b693257d5f532cad65b8c82d7731bc7f9b278460 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 14 Jul 2023 12:34:49 -0400 Subject: [PATCH 20/46] same exact setup as data catalog --- .github/workflows/build-and-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0e8bc470..b1745e08 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -61,12 +61,12 @@ jobs: distribution: 'temurin' cache: 'gradle' - name: Test with coverage - run: ./gradlew --build-cache test jacocoTestReport --info - - name: Build and analyze + run: ./gradlew --build-cache test jacocoTestReport + - name: SonarQube scan + run: ./gradlew --build-cache :library:sonarqube env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew --build-cache :library:sonarqube --info + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} notify-slack: needs: [ build, unit-tests-and-sonarqube, source-clear ] From d494492c55eed8d90fc1754adaf55a0d520cf87b Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 14 Jul 2023 12:49:00 -0400 Subject: [PATCH 21/46] update project key --- library/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/build.gradle b/library/build.gradle index fcc12943..44efec15 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -50,7 +50,7 @@ test { sonarqube { properties { - property "sonar.projectKey", "java-pfb" + property "sonar.projectKey", "DataBiosphere_java-pfb" property "sonar.projectName", "java-pfb" property "sonar.organization", "broad-databiosphere" property "sonar.host.url", "https://sonarcloud.io" From 86bc3940556ad780576ae098c0c25a6584b287a1 Mon Sep 17 00:00:00 2001 From: Tom Conner Date: Fri, 14 Jul 2023 13:50:42 -0400 Subject: [PATCH 22/46] Update library/build.gradle attempt to fix sonar "project not found" error --- library/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/build.gradle b/library/build.gradle index 44efec15..bce78b7e 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -48,7 +48,7 @@ test { useJUnitPlatform () } -sonarqube { +sonar { properties { property "sonar.projectKey", "DataBiosphere_java-pfb" property "sonar.projectName", "java-pfb" From 81b7874aa09c54f3756350e0ff88509db74ccd2f Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Fri, 14 Jul 2023 13:59:46 -0400 Subject: [PATCH 23/46] switch to sonar command and upgrade to v4 Rework sonar action trying to figure out issue Revert "add back github token" This reverts commit 26a0c521c5bda4c7e656026dc6863c84c765c652. What happens if I delete the sonar-project.properties? sanity check set sonar source Revert "add project base directory to sonar scan" This reverts commit c36bf2edae25885e038bbfc2ba7c4c7aeed5f5fa. add back github token add project base directory to sonar scan increase sonar logging try setting sonar properties in properties file cleanup --- .github/workflows/build-and-test.yml | 2 +- buildSrc/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b1745e08..f4eb0aa7 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -63,7 +63,7 @@ jobs: - name: Test with coverage run: ./gradlew --build-cache test jacocoTestReport - name: SonarQube scan - run: ./gradlew --build-cache :library:sonarqube + run: ./gradlew --build-cache :library:sonar --info env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 634e7286..1e305e87 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -22,7 +22,7 @@ dependencies { implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' // implementation 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.0.14.RELEASE' // implementation 'org.hidetake.swagger.generator:org.hidetake.swagger.generator.gradle.plugin:2.19.2' - implementation 'org.sonarqube:org.sonarqube.gradle.plugin:3.4.0.2513' + implementation 'org.sonarqube:org.sonarqube.gradle.plugin:4.2.1.3168' // implementation 'org.springframework.boot:spring-boot-gradle-plugin:2.7.0' // implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' // // This is required due to a dependency conflict between jib and srcclfr. Removing it will cause jib to fail. From 22927aca9b84a2228b0932529c397f704ff191de Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Mon, 17 Jul 2023 11:16:51 -0400 Subject: [PATCH 24/46] Dependency cleanup rework remove shellcheck --- buildSrc/build.gradle | 10 +----- ...o.terra.pfb.java-common-conventions.gradle | 21 ++--------- cli/build.gradle | 4 +-- cli/src/main/java/JavaPfbCommand.java | 35 ++++++++++--------- library/build.gradle | 31 ---------------- 5 files changed, 23 insertions(+), 78 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 1e305e87..f80a9b17 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -15,18 +15,10 @@ repositories { dependencies { implementation 'com.diffplug.spotless:spotless-plugin-gradle:6.11.0' - implementation 'com.felipefzdz.gradle.shellcheck:shellcheck:1.4.6' -// implementation 'com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:3.3.0' implementation 'com.srcclr.gradle:com.srcclr.gradle.gradle.plugin:3.1.12' - implementation 'de.undercouch.download:de.undercouch.download.gradle.plugin:5.2.0' implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' -// implementation 'io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.0.14.RELEASE' -// implementation 'org.hidetake.swagger.generator:org.hidetake.swagger.generator.gradle.plugin:2.19.2' implementation 'org.sonarqube:org.sonarqube.gradle.plugin:4.2.1.3168' -// implementation 'org.springframework.boot:spring-boot-gradle-plugin:2.7.0' -// implementation 'bio.terra:terra-test-runner:0.1.5-SNAPSHOT' -// // This is required due to a dependency conflict between jib and srcclfr. Removing it will cause jib to fail. -// implementation 'org.apache.commons:commons-compress:1.21' + implementation 'info.picocli:picocli:4.7.4' } test { diff --git a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle index 62708874..e21ad4a3 100644 --- a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle +++ b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle @@ -5,20 +5,10 @@ plugins { id 'com.diffplug.spotless' id 'com.github.spotbugs' -// id 'org.hidetake.swagger.generator' } boolean isCiServer = System.getenv().containsKey("CI") -//if (!isCiServer) { -// tasks.withType(JavaExec).configureEach { -// systemProperty 'spring.profiles.include', 'human-readable-logging' -// } -// tasks.withType(Test).configureEach { -// systemProperty 'spring.profiles.include', 'human-readable-logging' -// } -//} - java { toolchain { languageVersion = JavaLanguageVersion.of(17) @@ -41,15 +31,11 @@ repositories { dependencies { compileOnly 'com.github.spotbugs:spotbugs-annotations:4.7.2' -// implementation 'io.swagger.core.v3:swagger-annotations:2.2.0' -// swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.31' -// -// implementation 'org.slf4j:slf4j-api' testImplementation 'org.hamcrest:hamcrest:2.2' -// -// implementation 'bio.terra:terra-common-lib:0.0.75-SNAPSHOT' -// implementation 'bio.terra:datarepo-client:1.349.0-SNAPSHOT' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } tasks.named('test') { @@ -62,7 +48,6 @@ group = 'bio.terra' spotless { java { targetExclude "${buildDir}/**" -// targetExclude "**/swagger-code/**" googleJavaFormat() } } diff --git a/cli/build.gradle b/cli/build.gradle index 904e44b0..c54c62d7 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'java' + id 'bio.terra.pfb.java-common-conventions' } version 'unspecified' @@ -9,8 +9,6 @@ repositories { } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' implementation 'info.picocli:picocli:4.7.4' } diff --git a/cli/src/main/java/JavaPfbCommand.java b/cli/src/main/java/JavaPfbCommand.java index 8a951799..879ebb25 100644 --- a/cli/src/main/java/JavaPfbCommand.java +++ b/cli/src/main/java/JavaPfbCommand.java @@ -1,24 +1,25 @@ -import picocli.CommandLine; - import static picocli.CommandLine.Command; -import static picocli.CommandLine.Option; + +import picocli.CommandLine; @Command(name = "pfb") public class JavaPfbCommand implements Runnable { - public static void main(String[] args) { - CommandLine.run(new JavaPfbCommand(), args); - } + public static void main(String[] args) { + CommandLine.run(new JavaPfbCommand(), args); + } - @Override - public void run() { - System.out.println("A java implementation of pyPFB"); - } + @Override + public void run() { + System.out.println("A java implementation of pyPFB"); + } - @Command(name = "hello") - public void helloCommand() { - System.out.println("Hello world!"); - } + @Command(name = "hello") + public void helloCommand() { + System.out.println("Hello world!"); + } - @Command(name = "--help") - public void helpCommand() { System.out.println("Help is on its way!! In the next PR...");} -} \ No newline at end of file + @Command(name = "--help") + public void helpCommand() { + System.out.println("Help is on its way!! In the next PR..."); + } +} diff --git a/library/build.gradle b/library/build.gradle index bce78b7e..595da659 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'de.undercouch.download' id 'com.srcclr.gradle' id 'org.sonarqube' @@ -13,36 +12,6 @@ repositories { } apply from: 'generators.gradle' -//apply from: 'publishing.gradle' - -dependencies { -// implementation 'bio.terra:terra-common-lib' -// implementation 'org.apache.commons:commons-dbcp2' -// implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' -// implementation 'org.springframework.boot:spring-boot-starter-web' -// implementation 'org.springframework.retry:spring-retry' -// implementation 'org.broadinstitute.dsde.workbench:sam-client_2.13:0.1-9867891-SNAP' - implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' -// implementation 'javax.annotation:javax.annotation-api:1.3.2' -// implementation 'org.postgresql:postgresql' -// implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' -// implementation 'org.springframework.boot:spring-boot-starter-actuator:3.0.2' -// implementation 'io.micrometer:micrometer-registry-prometheus:1.10.4' - -// liquibaseRuntime 'org.liquibase:liquibase-core' - implementation'info.picocli:picocli:4.6.1' - implementation 'org.junit.jupiter:junit-jupiter:5.8.1' -// liquibaseRuntime 'org.postgresql:postgresql' -// liquibaseRuntime 'ch.qos.logback:logback-classic' - - -// testImplementation('org.springframework.boot:spring-boot-starter-test') { -// // Fixes warning about multiple occurrences of JSONObject on the classpath -// exclude group: 'com.vaadin.external.google', module: 'android-json' -// } -// testImplementation 'org.junit.jupiter:junit-jupiter-api' -// testImplementation 'org.mockito:mockito-inline' -} test { useJUnitPlatform () From 7b4294ab0c89b8688cc91852e95b36ffcf68811f Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 09:23:26 -0400 Subject: [PATCH 25/46] Run sonar both CLI and library --- .github/workflows/build-and-test.yml | 2 +- README.md | 4 ++-- .../bio.terra.pfb.java-common-conventions.gradle | 11 ++++++++++- library/build.gradle | 10 ---------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f4eb0aa7..c70f0f3f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -63,7 +63,7 @@ jobs: - name: Test with coverage run: ./gradlew --build-cache test jacocoTestReport - name: SonarQube scan - run: ./gradlew --build-cache :library:sonar --info + run: ./gradlew --build-cache sonar --info env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 46172227..ed1acef2 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,12 @@ before runing the gradle task. ```shell export SONAR_TOKEN=$(vault read -field=sonar_token secret/secops/ci/sonarcloud/java-pfb) -./gradlew sonarqube +./gradlew sonar ``` Unlike SourceClear, running this task produces no output unless your project has errors. To always generate a report, run using `--info`: ```shell -./gradlew sonarqube --info +./gradlew sonar --info ``` diff --git a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle index e21ad4a3..f494d45e 100644 --- a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle +++ b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle @@ -2,7 +2,7 @@ plugins { id 'idea' id 'jacoco' id 'java' - + id 'org.sonarqube' id 'com.diffplug.spotless' id 'com.github.spotbugs' } @@ -81,4 +81,13 @@ jacocoTestReport { // sonarqube requires XML coverage output to upload coverage data xml.required = true } +} + +sonar { + properties { + property "sonar.projectKey", "DataBiosphere_java-pfb" + property "sonar.projectName", "java-pfb" + property "sonar.organization", "broad-databiosphere" + property "sonar.host.url", "https://sonarcloud.io" + } } \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index 595da659..e4363a04 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,6 +1,5 @@ plugins { id 'com.srcclr.gradle' - id 'org.sonarqube' id 'com.gorylenko.gradle-git-properties' version '2.3.1' @@ -15,13 +14,4 @@ apply from: 'generators.gradle' test { useJUnitPlatform () -} - -sonar { - properties { - property "sonar.projectKey", "DataBiosphere_java-pfb" - property "sonar.projectName", "java-pfb" - property "sonar.organization", "broad-databiosphere" - property "sonar.host.url", "https://sonarcloud.io" - } } \ No newline at end of file From 6b41911f0700fd063e8bcd2387fa065f68b64f16 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 09:51:54 -0400 Subject: [PATCH 26/46] Remove spotbugs: Sonar is sufficient to meet to appsec requirement --- .github/workflows/build-and-test.yml | 5 ----- buildSrc/build.gradle | 1 - ...io.terra.pfb.java-common-conventions.gradle | 18 ------------------ 3 files changed, 24 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c70f0f3f..ec27f7dc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -23,11 +23,6 @@ jobs: - name: Build all projects without running tests run: ./gradlew --build-cache build -x test - - name: Upload spotbugs results - uses: github/codeql-action/upload-sarif@main - with: - sarif_file: library/build/reports/spotbugs/main.sarif - source-clear: needs: [ build ] runs-on: ubuntu-latest diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index f80a9b17..bddbf35b 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -16,7 +16,6 @@ repositories { dependencies { implementation 'com.diffplug.spotless:spotless-plugin-gradle:6.11.0' implementation 'com.srcclr.gradle:com.srcclr.gradle.gradle.plugin:3.1.12' - implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.12' implementation 'org.sonarqube:org.sonarqube.gradle.plugin:4.2.1.3168' implementation 'info.picocli:picocli:4.7.4' } diff --git a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle index f494d45e..e3b1145e 100644 --- a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle +++ b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle @@ -4,7 +4,6 @@ plugins { id 'java' id 'org.sonarqube' id 'com.diffplug.spotless' - id 'com.github.spotbugs' } boolean isCiServer = System.getenv().containsKey("CI") @@ -30,8 +29,6 @@ repositories { } dependencies { - compileOnly 'com.github.spotbugs:spotbugs-annotations:4.7.2' - testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' @@ -61,21 +58,6 @@ compileJava { } } -// Spotbugs configuration -spotbugs { - reportLevel = 'high' - effort = 'max' -} -spotbugsMain { - reports { - if (isCiServer) { - sarif.enabled = true - } else { - html.enabled = true - } - } -} - jacocoTestReport { reports { // sonarqube requires XML coverage output to upload coverage data From 6fc95f3428bdedd91e4336fae95d971932811fe9 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 11:09:51 -0400 Subject: [PATCH 27/46] Just run sonar in library for now --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ec27f7dc..807e6dcd 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -58,7 +58,7 @@ jobs: - name: Test with coverage run: ./gradlew --build-cache test jacocoTestReport - name: SonarQube scan - run: ./gradlew --build-cache sonar --info + run: ./gradlew --build-cache :library:sonar --info env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 93ce6cf208bd3fd2b5ab909bc9b05ab4bb96d104 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 11:34:48 -0400 Subject: [PATCH 28/46] ignore publish action; will fix with AJ-1095 update publish --- .github/workflows/publish.yml | 62 ++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fa5a916a..47d10352 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,34 +15,36 @@ jobs: outputs: tag: ${{ steps.tag.outputs.tag }} steps: - - uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' + - name: Enable publish with AJ-1095 + run: echo "TODO" +# - uses: actions/checkout@v3 +# - name: Set up JDK +# uses: actions/setup-java@v3 +# with: +# java-version: '17' +# distribution: 'temurin' +# cache: 'gradle' - - name: Parse tag - id: tag - run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT - - - name: Publish to Artifactory - run: ./gradlew --build-cache :client:artifactoryPublish - env: - ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} - ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - ARTIFACTORY_REPO_KEY: "libs-release-local" - - - name: Notify slack on failure - uses: broadinstitute/action-slack@v3.8.0 - if: failure() - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - with: - channel: '#dsp-analysis-journeys-alerts' - status: failure - author_name: Publish to dev - fields: job - text: 'Publish failed :sadpanda:' - username: 'Java-PFB GitHub Action' \ No newline at end of file +# - name: Parse tag +# id: tag +# run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT +# +# - name: Publish to Artifactory +# run: ./gradlew --build-cache :client:artifactoryPublish +# env: +# ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} +# ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} +# ARTIFACTORY_REPO_KEY: "libs-release-local" +# +# - name: Notify slack on failure +# uses: broadinstitute/action-slack@v3.8.0 +# if: failure() +# env: +# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} +# with: +# channel: '#dsp-analysis-journeys-alerts' +# status: failure +# author_name: Publish to dev +# fields: job +# text: 'Publish failed :sadpanda:' +# username: 'Java-PFB GitHub Action' \ No newline at end of file From 7371bb2602379f2d186da6a1413af9603ab661c9 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 14:03:29 -0400 Subject: [PATCH 29/46] Update main method so it's not empty --- library/src/main/java/bio/terra/javapfb/Library.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/bio/terra/javapfb/Library.java b/library/src/main/java/bio/terra/javapfb/Library.java index d8ca80ed..59822a4f 100644 --- a/library/src/main/java/bio/terra/javapfb/Library.java +++ b/library/src/main/java/bio/terra/javapfb/Library.java @@ -1,5 +1,9 @@ package bio.terra.javapfb; public class Library { - public static void main(String[] args) {} + + // This is a stub app. It has no `main` and should never be run. + public static void main(String[] args) { + System.exit(-1); + } } From 397ae318a9ab113484cf33bb9babf0ffade6f601 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 14:03:59 -0400 Subject: [PATCH 30/46] update fatjar task to just be named jar --- cli/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/build.gradle b/cli/build.gradle index c54c62d7..3eeb19e1 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -12,7 +12,7 @@ dependencies { implementation 'info.picocli:picocli:4.7.4' } -task fatJar(type: Jar) { +jar { manifest { attributes 'Main-Class': "JavaPfbCommand" } @@ -20,7 +20,6 @@ task fatJar(type: Jar) { from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } - with jar } test { From 9dc57a029eb3059f17725bcd1d73932a246c7a68 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 15:22:50 -0400 Subject: [PATCH 31/46] Update readme --- README.md | 2 ++ cli/README.md | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ed1acef2..ba365c7f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # java-pfb + + ## Running SourceClear locally [SourceClear](https://srcclr.github.io) is a static analysis tool that scans a project's Java diff --git a/cli/README.md b/cli/README.md index b66dd36f..ba980030 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,6 +1,11 @@ Current usage of CLI: -First, run "fatJar" gradle task in java-pfb-cli +First, run "jar" gradle task in cli project. +```shell +./gradlew :cli:jar +``` Then, you can use the CLI with the following command: +```shell java -cp "java-pfb-cli/build/libs/java-pfb-cli.jar" JavaPfbCommand +``` Only command right now: "hello" \ No newline at end of file From f519144b272e367f10a8d41a37d94a5c0a3c45d6 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 15:44:24 -0400 Subject: [PATCH 32/46] stub out some library code remove public --- library/src/main/java/bio/terra/javapfb/Library.java | 5 ++--- library/src/test/java/bio/terra/javapfb/LibraryTest.java | 9 ++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/bio/terra/javapfb/Library.java b/library/src/main/java/bio/terra/javapfb/Library.java index 59822a4f..58bec3a0 100644 --- a/library/src/main/java/bio/terra/javapfb/Library.java +++ b/library/src/main/java/bio/terra/javapfb/Library.java @@ -2,8 +2,7 @@ public class Library { - // This is a stub app. It has no `main` and should never be run. - public static void main(String[] args) { - System.exit(-1); + public static int getNumber5() { + return 5; } } diff --git a/library/src/test/java/bio/terra/javapfb/LibraryTest.java b/library/src/test/java/bio/terra/javapfb/LibraryTest.java index 86f108ef..bca1dbd5 100644 --- a/library/src/test/java/bio/terra/javapfb/LibraryTest.java +++ b/library/src/test/java/bio/terra/javapfb/LibraryTest.java @@ -1,11 +1,14 @@ package bio.terra.javapfb; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + import org.junit.jupiter.api.Test; -public class LibraryTest { +class LibraryTest { @Test - public void testHelloWorld() { - System.out.println("Hello World"); + void testStubMethod() { + assertThat(Library.getNumber5(), equalTo(5)); } } From cda1397de602f83c12951887eec30adc4dd242f8 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 16:17:21 -0400 Subject: [PATCH 33/46] rework package naming/structure --- cli/README.md | 2 +- cli/build.gradle | 2 +- .../java/{ => bio/terra/pfb}/JavaPfbCommand.java | 2 ++ library/src/main/java/bio/terra/javapfb/Library.java | 8 -------- library/src/main/java/bio/terra/pfb/Library.java | 12 ++++++++++++ .../java/bio/terra/{javapfb => pfb}/LibraryTest.java | 2 +- 6 files changed, 17 insertions(+), 11 deletions(-) rename cli/src/main/java/{ => bio/terra/pfb}/JavaPfbCommand.java (95%) delete mode 100644 library/src/main/java/bio/terra/javapfb/Library.java create mode 100644 library/src/main/java/bio/terra/pfb/Library.java rename library/src/test/java/bio/terra/{javapfb => pfb}/LibraryTest.java (90%) diff --git a/cli/README.md b/cli/README.md index ba980030..5d58c528 100644 --- a/cli/README.md +++ b/cli/README.md @@ -6,6 +6,6 @@ First, run "jar" gradle task in cli project. Then, you can use the CLI with the following command: ```shell -java -cp "java-pfb-cli/build/libs/java-pfb-cli.jar" JavaPfbCommand +java -cp "java-pfb-cli/build/libs/java-pfb-cli.jar" bio.terra.pfb.JavaPfbCommand ``` Only command right now: "hello" \ No newline at end of file diff --git a/cli/build.gradle b/cli/build.gradle index 3eeb19e1..b1d30b33 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -14,7 +14,7 @@ dependencies { jar { manifest { - attributes 'Main-Class': "JavaPfbCommand" + attributes 'Main-Class': 'bio.terra.pfb.JavaPfbCommand' } archiveBaseName.set('java-pfb-cli') from { diff --git a/cli/src/main/java/JavaPfbCommand.java b/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java similarity index 95% rename from cli/src/main/java/JavaPfbCommand.java rename to cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java index 879ebb25..c3025d11 100644 --- a/cli/src/main/java/JavaPfbCommand.java +++ b/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java @@ -1,3 +1,5 @@ +package bio.terra.pfb; + import static picocli.CommandLine.Command; import picocli.CommandLine; diff --git a/library/src/main/java/bio/terra/javapfb/Library.java b/library/src/main/java/bio/terra/javapfb/Library.java deleted file mode 100644 index 58bec3a0..00000000 --- a/library/src/main/java/bio/terra/javapfb/Library.java +++ /dev/null @@ -1,8 +0,0 @@ -package bio.terra.javapfb; - -public class Library { - - public static int getNumber5() { - return 5; - } -} diff --git a/library/src/main/java/bio/terra/pfb/Library.java b/library/src/main/java/bio/terra/pfb/Library.java new file mode 100644 index 00000000..ed84bf4d --- /dev/null +++ b/library/src/main/java/bio/terra/pfb/Library.java @@ -0,0 +1,12 @@ +package bio.terra.pfb; + +public class Library { + + private Library() { + // intentionally empty + } + + public static int getNumber5() { + return 5 * 1; + } +} diff --git a/library/src/test/java/bio/terra/javapfb/LibraryTest.java b/library/src/test/java/bio/terra/pfb/LibraryTest.java similarity index 90% rename from library/src/test/java/bio/terra/javapfb/LibraryTest.java rename to library/src/test/java/bio/terra/pfb/LibraryTest.java index bca1dbd5..a30ca9f3 100644 --- a/library/src/test/java/bio/terra/javapfb/LibraryTest.java +++ b/library/src/test/java/bio/terra/pfb/LibraryTest.java @@ -1,4 +1,4 @@ -package bio.terra.javapfb; +package bio.terra.pfb; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; From c07d89cc3d386da5a2c45d8d7bbdae9d4ee44c7f Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Tue, 18 Jul 2023 16:45:23 -0400 Subject: [PATCH 34/46] update to new setup method of picocli --- cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java b/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java index c3025d11..8712cb82 100644 --- a/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java +++ b/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java @@ -7,7 +7,8 @@ @Command(name = "pfb") public class JavaPfbCommand implements Runnable { public static void main(String[] args) { - CommandLine.run(new JavaPfbCommand(), args); + int exitCode = new CommandLine(new JavaPfbCommand()).execute(args); + System.exit(exitCode); } @Override From e66e9abe8cf24af41e8236020af1ad0b2d83251e Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 19 Jul 2023 10:02:46 -0400 Subject: [PATCH 35/46] set up help and version message for CLI --- cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java b/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java index 8712cb82..78e70d63 100644 --- a/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java +++ b/cli/src/main/java/bio/terra/pfb/JavaPfbCommand.java @@ -4,7 +4,11 @@ import picocli.CommandLine; -@Command(name = "pfb") +@Command( + name = "pfb", + mixinStandardHelpOptions = true, + description = "A java implementation of pyPFB", + version = "java-pfb 0.1.0") public class JavaPfbCommand implements Runnable { public static void main(String[] args) { int exitCode = new CommandLine(new JavaPfbCommand()).execute(args); @@ -13,16 +17,11 @@ public static void main(String[] args) { @Override public void run() { - System.out.println("A java implementation of pyPFB"); + System.out.println("PFB RUN"); } @Command(name = "hello") public void helloCommand() { System.out.println("Hello world!"); } - - @Command(name = "--help") - public void helpCommand() { - System.out.println("Help is on its way!! In the next PR..."); - } } From fb48d59b16aea4b1986b215925bd44c193f31a48 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 19 Jul 2023 10:23:00 -0400 Subject: [PATCH 36/46] Add some test coverage for CLI --- .../bio/terra/pfb/JavaPfbCommandTest.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 cli/src/test/java/bio/terra/pfb/JavaPfbCommandTest.java diff --git a/cli/src/test/java/bio/terra/pfb/JavaPfbCommandTest.java b/cli/src/test/java/bio/terra/pfb/JavaPfbCommandTest.java new file mode 100644 index 00000000..fa002e62 --- /dev/null +++ b/cli/src/test/java/bio/terra/pfb/JavaPfbCommandTest.java @@ -0,0 +1,43 @@ +package bio.terra.pfb; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JavaPfbCommandTest { + JavaPfbCommand javaPfbCommand; + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + @BeforeEach + void setup() { + javaPfbCommand = new JavaPfbCommand(); + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + void run() { + javaPfbCommand.run(); + assertThat(outContent.toString(), containsString("PFB RUN")); + } + + @Test + void helloCommand() { + javaPfbCommand.helloCommand(); + assertThat(outContent.toString(), containsString("Hello world!")); + } +} From 79ad761a2927a05dbf6ba82bf2c713d13154e40b Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 19 Jul 2023 10:31:04 -0400 Subject: [PATCH 37/46] bare minimum description for library --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba365c7f..77ce795e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# java-pfb +# Java-PFB +A java implementation of the [pyPFB](https://github.com/uc-cdis/pypfb) library that includes a CLI and a java library. +The CLI is a wrapper around the library. See the [CLI README](cli/README.md) for more information. ## Running SourceClear locally From 41b05dea454942772543b98b1645da6f95421821 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 19 Jul 2023 10:32:28 -0400 Subject: [PATCH 38/46] update cli readme Update README.md --- cli/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/README.md b/cli/README.md index 5d58c528..06e7ad63 100644 --- a/cli/README.md +++ b/cli/README.md @@ -6,6 +6,9 @@ First, run "jar" gradle task in cli project. Then, you can use the CLI with the following command: ```shell -java -cp "java-pfb-cli/build/libs/java-pfb-cli.jar" bio.terra.pfb.JavaPfbCommand +java -cp "cli/build/libs/java-pfb-cli.jar" bio.terra.pfb.JavaPfbCommand ``` -Only command right now: "hello" \ No newline at end of file +Available Commands: +- hello +- --version +- --help \ No newline at end of file From 4870da5def40bf737c719420f71720e53b35cb2e Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 19 Jul 2023 11:24:57 -0400 Subject: [PATCH 39/46] Try running sonar for both CLI and library --- .github/workflows/build-and-test.yml | 2 +- cli/build.gradle | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 807e6dcd..ec27f7dc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -58,7 +58,7 @@ jobs: - name: Test with coverage run: ./gradlew --build-cache test jacocoTestReport - name: SonarQube scan - run: ./gradlew --build-cache :library:sonar --info + run: ./gradlew --build-cache sonar --info env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cli/build.gradle b/cli/build.gradle index b1d30b33..6018231c 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -24,4 +24,13 @@ jar { test { useJUnitPlatform() +} + +sonar { + properties { + property "sonar.projectKey", "DataBiosphere_java-pfb-cli" + property "sonar.projectName", "java-pfb-cli" + property "sonar.organization", "broad-databiosphere" + property "sonar.host.url", "https://sonarcloud.io" + } } \ No newline at end of file From e90b6cc9847549e8eaf1d396036ff5d81c0c210b Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 19 Jul 2023 11:26:55 -0400 Subject: [PATCH 40/46] Remove unneeded gradle conventions file --- .../groovy/bio.terra.pfb.java-application-conventions.gradle | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 buildSrc/src/main/groovy/bio.terra.pfb.java-application-conventions.gradle diff --git a/buildSrc/src/main/groovy/bio.terra.pfb.java-application-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.pfb.java-application-conventions.gradle deleted file mode 100644 index 5de6f188..00000000 --- a/buildSrc/src/main/groovy/bio.terra.pfb.java-application-conventions.gradle +++ /dev/null @@ -1,5 +0,0 @@ -plugins { - // Apply the common convention plugin for shared build configuration between library and application projects. - id 'bio.terra.pfb.java-common-conventions' - id 'application' -} \ No newline at end of file From b18f455ccc9cc7d95df7de9747e9c53bc3180451 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Wed, 19 Jul 2023 11:58:06 -0400 Subject: [PATCH 41/46] separate step for cli and library sonar scans --- .github/workflows/build-and-test.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ec27f7dc..a263a4fb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -57,8 +57,13 @@ jobs: cache: 'gradle' - name: Test with coverage run: ./gradlew --build-cache test jacocoTestReport - - name: SonarQube scan - run: ./gradlew --build-cache sonar --info + - name: SonarQube scan for library + run: ./gradlew --build-cache :library:sonar --info + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: SonarQube scan for cli + run: ./gradlew --build-cache :cli:sonar --info env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 82642ac6b42c650e0106ee76b1f0b7489afa279f Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Thu, 20 Jul 2023 09:02:28 -0400 Subject: [PATCH 42/46] upgrade gradle to 8.2.1 Revert "upgrade gradle to 8.2.1" This reverts commit 525bbd83c6a71eaecbc30c8ee4e63f08f5b3dd38. Revert "Revert "upgrade gradle to 8.2.1"" This reverts commit 200ccedb27f81a2b28e6f6c545ff792fe33c2ceb. --- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 269 ++++++++++++++--------- gradlew.bat | 15 +- 4 files changed, 175 insertions(+), 112 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 36900 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

yHlr~Qj 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 GTKgBj+ffbx diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 31cca491..17a8ddce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882e..65dcd68d 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..6689b85b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 2017fad2face45440fdb3075c32c4d6bb93edbce Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Thu, 20 Jul 2023 10:48:46 -0400 Subject: [PATCH 43/46] Remove cli test command --- cli/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/build.gradle b/cli/build.gradle index 6018231c..428ffa7e 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -22,10 +22,6 @@ jar { } } -test { - useJUnitPlatform() -} - sonar { properties { property "sonar.projectKey", "DataBiosphere_java-pfb-cli" From f484b35c7f43edbeb41b090942b88a53a4690f6e Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Thu, 20 Jul 2023 10:49:28 -0400 Subject: [PATCH 44/46] Set srcclr scope to runtime --- library/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index e4363a04..9bbe4108 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -12,6 +12,6 @@ repositories { apply from: 'generators.gradle' -test { - useJUnitPlatform () +srcclr { + scope = "runtime" } \ No newline at end of file From 0aecde2eaf68d6725795c8c9a6f7db14bd360f37 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Thu, 20 Jul 2023 11:46:51 -0400 Subject: [PATCH 45/46] Add a little more context to our source clear and sonar cloud runs. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 77ce795e..938ac1b4 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ export SRCCLR_API_TOKEN=$(vault read -field=api_token secret/secops/ci/srcclr/gr ./gradlew srcclr ``` +Results of the scan are uploaded to [Defect DOJO](https://defectdojo.dsp-appsec.broadinstitute.org/dashboard). + ## Running SonarQube locally [SonarQube](https://www.sonarqube.org) is a static analysis code that scans code for a wide @@ -34,3 +36,5 @@ generate a report, run using `--info`: ```shell ./gradlew sonar --info ``` + +We run the scans for two projects: [java-pfb](https://sonarcloud.io/project/overview?id=DataBiosphere_java-pfb) and [java-pfb-cli](https://sonarcloud.io/project/overview?id=DataBiosphere_java-pfb-cli). The results are uploaded to the sonarcloud dashbaord. \ No newline at end of file From 868ab1416c2fe363c7ab586020a777eabf8d6340 Mon Sep 17 00:00:00 2001 From: Shelby Holden Date: Thu, 20 Jul 2023 12:06:09 -0400 Subject: [PATCH 46/46] Only need test action in common definitions; don't need jfrog def add back picocli --- buildSrc/build.gradle | 11 ----------- .../bio.terra.pfb.java-common-conventions.gradle | 8 ++++---- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index bddbf35b..eb9963e4 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -3,13 +3,6 @@ plugins { } repositories { - mavenCentral() - maven { - url 'https://broadinstitute.jfrog.io/broadinstitute/libs-release-local/' - } - maven { - url 'https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/' - } gradlePluginPortal() } @@ -18,8 +11,4 @@ dependencies { implementation 'com.srcclr.gradle:com.srcclr.gradle.gradle.plugin:3.1.12' implementation 'org.sonarqube:org.sonarqube.gradle.plugin:4.2.1.3168' implementation 'info.picocli:picocli:4.7.4' -} - -test { - useJUnitPlatform() } \ No newline at end of file diff --git a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle index e3b1145e..df686b67 100644 --- a/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle +++ b/buildSrc/src/main/groovy/bio.terra.pfb.java-common-conventions.gradle @@ -35,10 +35,6 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } -tasks.named('test') { - useJUnitPlatform() -} - version = gradle.releaseVersion group = 'bio.terra' @@ -58,6 +54,10 @@ compileJava { } } +test { + useJUnitPlatform() +} + jacocoTestReport { reports { // sonarqube requires XML coverage output to upload coverage data