{"id":28395169,"url":"https://github.com/databiosphere/terra-workspace-manager","last_synced_at":"2025-06-27T01:31:30.641Z","repository":{"id":37056997,"uuid":"238543912","full_name":"DataBiosphere/terra-workspace-manager","owner":"DataBiosphere","description":null,"archived":false,"fork":false,"pushed_at":"2025-06-11T18:57:00.000Z","size":14267,"stargazers_count":14,"open_issues_count":4,"forks_count":7,"subscribers_count":31,"default_branch":"main","last_synced_at":"2025-06-11T20:20:01.751Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DataBiosphere.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-02-05T20:39:23.000Z","updated_at":"2025-06-03T13:45:08.000Z","dependencies_parsed_at":"2023-09-28T15:59:07.060Z","dependency_job_id":"cd82324f-46ab-4d50-bfe6-33f2730986bb","html_url":"https://github.com/DataBiosphere/terra-workspace-manager","commit_stats":null,"previous_names":[],"tags_count":1452,"template":false,"template_full_name":null,"purl":"pkg:github/DataBiosphere/terra-workspace-manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DataBiosphere%2Fterra-workspace-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DataBiosphere%2Fterra-workspace-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DataBiosphere%2Fterra-workspace-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DataBiosphere%2Fterra-workspace-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DataBiosphere","download_url":"https://codeload.github.com/DataBiosphere/terra-workspace-manager/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DataBiosphere%2Fterra-workspace-manager/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262172421,"owners_count":23270008,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-05-31T19:39:22.501Z","updated_at":"2025-06-27T01:31:30.623Z","avatar_url":"https://github.com/DataBiosphere.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# terra-workspace-manager\n\nThis repository holds the MC Terra Workspace Manager (WSM) service, client, and\nintegration test projects.\n\n* [Overview](#overview)\n* [WSM Client](#wsm-client)\n    * [Usage (Gradle)](#usage-gradle)\n    * [Publishing and Versioning](#publishing-and-versioning)\n        * [Compatible Changes of Significance](#compatible-changes-of-significance)\n        * [Incompatible Changes](#incompatible-changes)\n* [Deployment](#deployment)\n    * [On commit to main](#on-commit-to-main)\n* [Setup](#setup)\n    * [Prerequisites:](#prerequisites)\n        * [Postgres](#postgres)\n        * [JDK](#jdk)\n        * [Configuring IntelliJ SDK](#configuring-intellij-sdk)\n        * [Python3](#python3)\n    * [Database Configuration](#database-configuration)\n        * [Docker Postgres](#running-the-postgres-container)\n    * [IntelliJ Setup](#intellij-setup)\n* [Running](#running)\n    * [Writing Configuration](#writing-configuration)\n    * [Code Coverage](#code-coverage)\n    * [Running Workspace Manager Locally](#running-workspace-manager-locally)\n    * [OpenAPI V3 - formerly swagger](#openapi-v3---formerly-swagger)\n* [Code walk-through](#code-walk-through)\n    * [Dependencies](#dependencies)\n    * [Workspace Manager Service](#workspace-manager-service)\n        * [Layering](#layering)\n        * [REST API Class Usage](#rest-api-class-usage)\n        * [Spring Boot](#spring-boot)\n    * [Service Code Structure](#service-code-structure)\n    * [GitHub Actions](#github-actions)\n* [Tests](#tests)\n    * [Running tests](#running-tests)\n    * [Unit Tests](#unit-tests)\n    * [Connected Tests](#connected-tests)\n    * [Integration Tests](#integration-tests)\n    * [Making tests fast](#making-tests-fast)\n        * [Create one workspace/context/resource for entire test](#create-one-workspacecontextresource-for-entire-test)\n        * [For local runs, skip workspace/context creation](#for-local-runs-skip-workspacecontext-creation)\n    * [Cleaning up workspaces in tests](#cleaning-up-workspaces-in-tests)\n* [Adding a new flight](#adding-a-new-flight)\n* [Logging During Test Runs](#logging-during-test-runs)\n    * [Seeing Log Output](#seeing-log-output)\n    * [Human Readable Logging](#human-readable-logging)\n    * [Controlling Log Level](#controlling-log-level)\n* [Update custom IAM role permissions for GCP projects.](#update-custom-iam-role-permissions-for-gcp-projects)\n* [Tips](#tips)\n\n## Overview\n\nWSM provides workspaces; contexts for holding the work of\nindividuals and teams. A _workspace_ has members that are granted some role on the\nworkspace (OWNER, READER, WRITER). The members can create and manage _resources_ in the\nworkspace. There are two types of resources:\n\n- _controlled resources_ are cloud resources (e.g., buckets) whose attributes,\n  permissions, and lifecycle are controlled by the Workspace Manager. Controlled resources\n  are created and managed using Workspace Manager APIs.\n- _referenced resources_ are cloud resources that are independent of the\n  Workspace Manager. A workspace may hold a reference to such a resource. The Workspace\n  Manager has no role in managing the resource’s lifecycle or attributes.\n\nResources have unique names within the workspace, allowing users of the workspace to\nlocate and refer to them in a consistent way, whether they are controlled or referenced.\n\nThe resources in a workspace may reside on different clouds. Users may create one _cloud\ncontext_ for each cloud platform where they have controlled or referenced resources.\n\nWorkspace Manager provides the minimum interface to allow it to control permissions and\nlifecycle of controlled resources. All other access, in particular data reading and\nwriting, are done using the native cloud APIs.\n\nControlled resources may be _shared_ or _private_. Shared resources are accessible to\nworkspace members with their workspace role. That is, if you have READER on the workspace,\nthen you can read the resource (however that is defined for the specific resource); if you\nhave WRITER on the workspace, then you can write the resource.\n\nPrivate resources are available to a single member of the workspace. At the present time,\na private resource is available only to its creator.\n\nWSM has latent support for _applications_. [Leonardo](https://github.com/DataBiosphere/leonardo) is the only\nsupported application at this time. The concept is that an application is a distinguished service account. Owners of \nthe workspace can control which applications are allowed access to the workspace. If an application is given\naccess, then it can create application-owned resources. The goal is to allow applications\nto create constellations of resources that support the application, and not let them be\nmessed with by workspace READERS and WRITERS.\n\n## WSM Client\n\nWorkspace Manager publishes an API client library generated from its OpenAPI Spec v3\ninterface definition.\n\n### Usage (Gradle)\n\nInclude the Broad Artifactory repositories:\n\n```gradle\nrepositories {\n    maven {\n        url \"https://us-central1-maven.pkg.dev/dsp-artifact-registry/libs-snapshot/\"\n    }\n}\n```\n\nAdd a dependency like\n\n```gradle\nimplementation(group: 'bio.terra', name: 'workspace-manager-client', version: 'x.x.x')\n```\n\nSee [settings.gradle](settings.gradle) for the latest version information.\n\n### Publishing and Versioning\n\nNew versions of the WSM client library are automatically published with each merge to the\nmain branch. Since we publish very frequently, and Broad Dev Ops needs specific versions to\ntrack through the release process, we use a variation of semantic versioning.\n\nBy default, the patch version is incremented after each merge to main. You can cause other\nparts of the version to be changed as follows.\n\n- To bump the minor version, put the string `#minor` in your commit message. The minor\n  version will be incremented and the patch version will be set to 0: `major.minor.0`\n- To bump the major version, put the string `#major` in your commit message. The minor and\n  patch versions will be set to 0: `major.0.0`.\n\nIn addition, you can manually trigger the `tag-publish` github action and specify the part\nof the version to change.\n\n#### Compatible Changes of Significance\n\nWe should bump the minor version number when releasing significant features that are\nbackward compatible.\n\n#### Incompatible Changes\n\nIncompatible changes require incrementing the major version number. In our current state\nof development, we are allowing for some incompatible API changes in the feature-locked\nparts of the API without releasing a version `1.0.0`.\n\n### On commit to main\n\n1. New commit is merged to main\n2. [The tag-publish workflow](https://github.com/DataBiosphere/terra-workspace-manager/blob/dev/.github/workflows/tag-publish.yml)\n   is triggered. It builds the image, tags the image \u0026 commit, and pushes the image to GCR. It then sends\n   a [dispatch](https://help.github.com/en/actions/reference/events-that-trigger-workflows#external-events-repository_dispatch)\n   with the new version for the service to the [terra-helmfile repo](https://github.com/broadinstitute/terra-helmfile).\n3. This updates the\n   default [version mapping for the app in question](https://github.com/broadinstitute/terra-helmfile/blob/master/versions.yaml).\n4. [Our deployment of ArgoCD](https://ap-argocd.dsp-devops.broadinstitute.org/applications) monitors the above repo, and\n   any environments in which the app is set to auto-sync will immediately pick up the new version of the image. If the\n   app is not set to auto-sync in an environment, it can be manually synced via the ArgoCD UI or API.\n\n## Release to Upper Environments (Staging, Prod)\n\nWSM follows standards for deployment via DSP DevOps services\n([Beehive](https://beehive.dsp-devops.broadinstitute.org/)).\n\nFor more information on how to release WSM to upper environments (including on demand via independent release), please\nsee\n[Releasing our Code](https://broadworkbench.atlassian.net/wiki/spaces/WOR/pages/2974449675/Releasing+our+Code)\nin Confluence.\n\n## Setup\n\n### Prerequisites:\n\n#### JDK\n\nWe use the Adoptium JDK version 17. (At this writing, the Mac x86 version is 17.0.2+8).\nYou learn about it at [Adoptium](https://adoptium.net/).\n\nThe recommended process for installing on Mac is to first install [jEnv](https://www.jenv.be/) to\nmanage the active version:\n\n```sh\n    brew install jenv\n    # follow postinstall instructions to activate jenv...\n    \n    # to add previously installed versions of Java to jEnv, list them:\n    # /usr/libexec/java_home -V\n    # and then add them:\n    # jenv add /Library/Java/JavaVirtualMachines/\u003cJAVA VERSION HERE\u003e/Contents/Home\n```\n\nYou can install the JDK from IntelliJ::\n\n- File menu\n- Project Structure...\n- Under Project Settings select Project\n- Select Project SDK\n- Select Download SDK...\n- Select version 17\n- Select Eclipse Temurin\n\nIf you use homebrew, you can do this instead:\n\n```sh\n    brew tap homebrew/cask-versions\n    brew install --cask temurin17\n\n    # If ~/.jenv/versions directory doesn't exist:\n    mkdir -p ~/.jenv/versions\n    jenv add /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home\n ```\n\nYou may want to use jenv to set this JDK as your global setting to avoid surprises.\n\nTo switch between jdk versions, modify `.bash_profile` and run `jenv` as [described here](https://www.jenv.be/).\n\n#### Configuring IntelliJ SDK\n\nYou will need to configure IntelliJ to use a particular SDK. Here are settings you may need to\nmake:\n\n- IntelliJIdea | Preferences... | Build, Execution, Deployment | Build Tools | Gradle - set Gradle\n  JVM to your JDK 17\n- File | Project Structure... | Project Settings | Project | Project SDK\n    - set Project SDK to your JDK 17 SDK\n    - set Project Language Level to 17\n\n#### Python3\n\nWe are currently using Python 3.9.10\n\nThe build and the integration tests assume `python3` and its built-in module `venv` are available. The\nbuild of openapi creates a python virtual environment and activates it to run the script to merge\nthe various YAML files comprising the REST API.\n\nThe recommended practice is to install `pyenv` and use that to install and manage your versions\nof python. Follow the instructions [here](https://github.com/pyenv/pyenv#homebrew-in-macos) to\ninstall pyenv.\n\nThen use `pyenv` to install the right version of python like so:\n\n```\npyenv install 3.9.10\n```\n\nIn the automation, we use the `venv` to make virtual environments. The advantage of using `venv`\nis that it is built in so does not require extra installation steps.\nHowever, it is not so useful as a developer tools because it has no way to de-activate a virtual\nenvironment. Consider installing `virtualenv` for your developer use. It can be installed using\n`pip` like so:\n\n```\npython3 -m pip install virtualenv\n```\n\n#### jq\n\n`jq` is required and used to parse JSON-responses while running various shell scripts in this repo.\n\nIf you are using homebrew, you can install `jq` with the following:\n\n```shell\nbrew install jq\n```\n\nIf you are not using homebrew, `jq`'s site lists a variety of ways to\n[install the tool](https://jqlang.github.io/jq/download/) as well.\n\n### Database Configuration\n\nWorkspace Manager Service relies on a Postgresql database server containing two databases:\none for the service itself, and one for\n[Stairway](https://github.com/DataBiosphere/stairway). For unit and connected tests, this will default\nto using a Docker container. As long as you can run a container, you do not need to do any setup to run unit or\nconnected tests.\n\nFor running WSM locally, Docker is the preferred method for running the database.\n\n##### Running the Postgres Container\n\nTo start a postgres container configured with the necessary databases:\n\n```sh\n./service/local-dev/run_postgres.sh start\n```\n\nTo stop the container:\n\n```sh\n./service/local-dev/run_postgres.sh stop\n```\n\nNote that the contents of the database is not saved between container runs.\n\n##### Connecting to the Postgres Container\n\nUse `psql` to connect to databases within the started database container. For database `wsm_db` use user `dbuser` with\npassword `dbpwd` like this:\n\n```sh\nPGPASSWORD=dbpwd psql postgresql://127.0.0.1:5432/wsm_db -U dbuser\n```\n\nFor the Stairway database `wsm_stairway` use user `stairwayuser` with password `stairwaypwd`.\n\n### IntelliJ Setup\n\n1. Open the repo normally (File -\u003e Open)\n2. In project structure (the folder icon with a little tetromino over it in the upper\n   right corner), make sure the project SDK is set to Java 17. If not, IntelliJ should\n   detect it on your system in the dropdown, otherwise click \"Add JDK...\" and navigate to\n   the folder from the last step.\n3. Set up\n   [google-java-format](https://github.com/google/google-java-format#intellij-android-studio-and-other-jetbrains-ides).\n   We use the spotless checker to force code to a standard format. Installing the IntelliJ\n   plug-in and library makes it easier to get it in the right format from the start.\n4. See some optional tips below in the [\"Tips\"](#tips) section.\n\n## Running\n\n### Writing Configuration\n\nRunning WSM and the Test Runner integration tests requires many service accounts and database\ncoordinates. That information is stored in Broad's Vault server. We do not want the main\ncode to directly depend on Vault. So the code depends on files that hold the information.\n\nThe `scripts/write-config.sh` script is used to collect all of the needed data from vault and\nstore it in files in the gradle `${rootDir}` in the directory `config/`. Having a Broad Institute\naccount is the pre-requisite for fetching data from vault.\n\nOne advantage of having the config written in gradle `${rootDir}` is that it is visible to\ngithub actions that run our CI/CD process.\n\nView current usage information for `write-config.sh` by entering\n\n```sh\n./scripts/write-config.sh help\n```\n\n### Code Coverage\n\nWe use Jacoco to generate code coverage reports. Coverage information is written\nto `service/build/jacoco/{task_name}.exec`, and the `combinedJaCoCoReport`\ngradle task will generate a single combined report for all test tasks run on\nthe same machine. This task is run automatically after test tasks and can also\nbe run manually.\n\nTo get coverage from integration tests, start the server under test using\n`:service:jacocoBootRun` instead of `:service:bootRun`. See\n[Running Workspace Manager Locally](#Running-Workspace-Manager-Locally) for\nmore information.\n\n### Running Workspace Manager Locally\n\nTo run locally, you'll first need to write configs (if you haven't already)\nand then launch the application:\n\n```sh\n./scripts/write-config.sh # First time only\n./gradlew :service:bootRun\n```\n\nThen navigate to the Swagger: http://localhost:8080/swagger-ui.html\n\nYou can also use the `:service:jacocoBootRun` task to run the server\ninstrumented with Jacoco coverage tracking. This is useful for tracking\nintegration test coverage, but may incur a small performance cost.\n\n### OpenAPI V3 - formerly swagger\n\nA swagger-ui page is available at /swagger-ui.html on any running instance. For\nlocal WSM:\n\n- http://localhost:8080/swagger-ui.html\n\nFor Broad deployment, those are:\n\n- dev: https://workspace.dsde-dev.broadinstitute.org/swagger-ui.html\n- staging: https://workspace.dsde-staging.broadinstitute.org/swagger-ui.html\n- prod: https://workspace.dsde-prod.broadinstitute.org/swagger-ui.html\n\nIf you can't load any of the swagger pages, check that you are on **non-split** VPN before troubleshooting further.\n\n## Code walk-through\n\nThis section describes the nuts and bolts of developing on WSM in the Broad\nenvironment. Processes here rely on access to the Broad Vault server to get secrets and to\nthe Broad Artifactory server to read and write libraries. There are dependencies on Broad\nDev Ops github repositories and practices. Some of those are locked down, because the\nBroad deployment of Terra needs to maintain a FedRamp approval level in order to host US\nGovernment data.\n\nWe use [gradle](https://gradle.org/) as our build tool. The repository is organized as a\ncomposite build, with common build logic pulled\ninto [convention plugins](https://docs.gradle.org/current/samples/sample_convention_plugins.html).\nThere are three mostly independent projects:\n\n- _service_ - the Workspace Manager Service\n- _client_ - the OpenAPI-generated client\n- _integration_ - the TestRunner-based integration test project\n\nThe build structure is:\n\n```\nterra-workspace-manager\n  |\n  + settings.gradle\n  + build.gradle\n  |\n  +-- buildSrc/src/main/groovy (convention plugins)\n  |    |\n  |    + terra-workspace-manager.java-conventions.gradle\n  |    + terra-workspace-manager.library-conventions.gradle\n  |\n  +-- service\n  |    |\n  |    + build.gradle (service build; test dependency on client)\n  |\n  +–- client\n  |    |\n  |    + build.gradle\n  |\n  +-- integration (formerly clienttest)\n       |\n       + build.gradle (dependency on client)\n```\n\nThis build, and others in MC Terra require access to the Broad Institute's\nArtifactory server. That is where supporting libraries are published and where we publish\nthe WSM client\n\n### Workspace Manager Service\n\nThe bulk of the code is in the `service` project. This section describes that project.\n\n#### Layering\n\nWorkspace Manager's logic for handling requests is broken into several layers. From highest to lowest:\n\n- Controllers (`app/controller/`): this layer performs access checks, validates input, invokes services to do the work,\n  and packages the service output into the response. Every controller endpoint should perform an authorization check as\n  one of the first steps, or else clearly document why it isn't doing so.\n- Services (`service/`): this layer is where most work is done. Each Service class wraps an external service or a\n  collection of related internal functions. These functions may\n  launch [Stairway](https://github.com/DataBiosphere/stairway) Flights to perform work as a transaction.\n  See [README](README.md#Service Code Structure) for a more detailed description of each service.\n- Flights (`service/**/flight`): collections of [Stairway](https://github.com/DataBiosphere/stairway) individual logical\n  steps which are performed in order as a transaction. Individual steps may call service or DAO methods.\n- Data Access Objects (DAOs) (`db/`): wrappers around the WSM database. Methods that interact with the database directly\n  live here, and Services call DAO methods rather than the database directly.\n\n#### REST API Class Usage\n\nIn general, API class objects are converted to and from internal WSM objects in the Controller layer.\nThere are two exceptions to this rule.\n\nFirst, we use API objects directly to pass cloud resource object parameters through create and update methods.\nThere is little utility in copying the API structure into an identical internal structure, simply to make the\ncloud call and discard the object.\n\nSecond, we create API response objects within flight steps. That allows the JobService to implement a generic\njob response.\n\n#### Spring Boot\n\nThe service project uses Spring Boot as the framework for REST servers. The objective is to use a minimal set\nof Spring features; there are many ways to do the same thing and we would like to constrain ourselves\nto a common set of techniques.\n\n##### Configuration\n\nWe only use YAML configuration. We never use XML or .properties files.\n\nIn general, we use type-safe configuration parameters as shown here:\n[Type-safe Configuration Properties](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties).\nThat allows proper typing of parameters read from property files or environment variables. Parameters are\nthen accessed with normal accessor methods. You should never need to use an `@Value` annotation.\n\nBe aware that environment variables will override values in our YAML configuration.\nThis should not be used for configuration as it makes the source of values harder to track,\nbut it may be useful for debugging unexpected configurations. See Spring Boot's\n[Externalized Configuration documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#boot-features-external-config)\nfor the exact priority order of configurations.\n\n##### Initialization\n\nWhen the applications starts, Spring wires up the components based on the profiles in place.\nSetting different profiles allows different components to be included. This technique is used\nas the way to choose the cloud platform (Google, Azure, AWS) code to include.\n\nWe use the Spring idiom of the `postSetupInitialization`, found in ApplicationConfiguration.java,\nto perform initialization of the application between the point of having the entire application initialized and\nthe point of opening the port to start accepting REST requests.\n\n##### Annotating Singletons\n\nThe typical pattern when using Spring is to make singleton classes for each service, controller, and DAO.\nYou do not have to write the class with its own singleton support. Instead, annotate the class with\nthe appropriate Spring annotation. Here are ones we use:\n\n- `@Component` Regular singleton class, like a service.\n- `@Repository` DAO component\n- `@Controller` REST Controller\n- `@Configuration` Definition of properties\n\n##### Common Annotations\n\nThere are other annotations that are handy to know about.\n\nUse `@Nullable` to mark method interface and return parameters that can be null.\n\n###### Autowiring\n\nSpring wires up the singletons and other beans when the application is launched.\nThat allows us to use Spring profiles to control the collection of code that is\nrun for different environments. Perhaps obviously, you can only autowire singletons to each other. You cannot autowire\ndynamically created objects.\n\nThere are two styles for declaring autowiring.\nThe preferred method of autowiring, is to put the annotation on the constructor\nof the class. Spring will autowire all the inputs to the constructor.\n\n```java\n\n@Component\npublic class Foo {\n    private final Bar bar;\n    private Fribble fribble;\n\n    @Autowired\n    public Foo(Bar bar, Fribble fribble) {\n        this.bar = bar;\n        this.foo = foo;\n    }\n}\n```\n\nSpring will pass in the instances of Bar and Fribble into the constructor.\nIt is possible to autowire a specific class member, but that is rarely necessary:\n\n```java\n\n@Component\npublic class Foo {\n    @Autowired\n    private Bar bar;\n}\n```\n\n###### REST Annotations\n\n- `@RequestBody` Marks the controller input parameter receiving the body of the request\n- `@PathVariable(\"x\")` Marks the controller input parameter receiving the parameter `x`\n- `@RequestParam(\"y\")` Marks the controller input parameter receiving the query parameter`y`\n\n###### JSON Annotations\n\nWe use the Jackson JSON library for serializing objects to and from JSON. Most of the time, you don't need to\nuse JSON annotations. It is sufficient to provide setter/getter methods for class members\nand let Jackson figure things out with introspection. There are cases where it needs help\nand you have to be specific.\n\nThe common JSON annotations are:\n\n- `@JsonValue` Marks a class member as data that should be (de)serialized to(from) JSON.\n  You can specify a name as a parameter to specify the JSON name for the member.\n- `@JsonIgnore`  Marks a class member that should not be (de)serialized\n- `@JsonCreator` Marks a constructor to be used to create an object from JSON.\n\nFor more details see [Jackson JSON Documentation](https://github.com/FasterXML/jackson-docs)\n\n### Service Code Structure\n\nThis section explains the code structure of the template. Here is the directory structure:\n\n```\nsrc/main/\n  java/\n    bio/terra/workspace/\n      app/\n        configuration/\n        controller/\n      common/\n        exception/\n        utils/\n      db/\n        exception/\n        model/\n      service/\n        buffer/\n        crl/\n        datarepo/\n        iam/\n        job/\n        resource/\n        spendprofile/\n        stage/\n        status/\n        workspace/\n  resources/\n```\n\n- `app/` For the top of the application, including Main and the StartupInitializer\n- `app/configuration/` For all of the bean and property definitions\n- `app/controller/` For the REST controllers. The controllers typically do very little.\n  They perform access checks and validate input, invoke a service to do the work, and package the service output into\n  the response. The\n  controller package also defines the global exception handling.\n- `common/` For common models, exceptions, and utilities.\n  shared by more than one service.\n- `common/exception/` A set of common abstract base classes that support the ErrorReport REST API\n  return structure live in the [Terra Common Library ](https://github.com/DataBiosphere/terra-common-lib).\n  All WSM exceptions derive from those. Exceptions common across services live here.\n- `service/` Each service gets a package within. We handle cloud-platform specializations\n  within each service.\n- `service/buffer/` Thin interface to access the\n  [Resource Buffer Service](https://github.com/DataBiosphere/terra-resource-buffer)\n  for allocating GCP projects: the cloud context for Google cloud.\n- `service/crl/` Thin interface to access the\n  [Terra Cloud Resource Library](https://github.com/DataBiosphere/terra-cloud-resource-lib)\n  used for allocating cloud resources.\n- `service/datarepo` Thin interface to access the\n  [Terra Data Repository](https://github.com/DataBiosphere/jade-data-repo) for making\n  _referenced resources_ pointing to TDR snapshots.\n- `service/iam` Methods for accessing [Sam](https://github.com/broadinstitute/sam) for\n  authorization definition and checking. This service provides retries and specific methods\n  for the WSM operations on Sam.\n- `service/job` Methods for launching Stairway flights, waiting on completion, and getting\n  flight results\n- `service/resource` One of the main services in WSM. Manages controlled and referenced resources.\n- `service/spendprofile` Temporary methods to use fake spend profiles. Eventually, it will\n  become a thin layer accessing the Spend Profile Manager when that arrives.\n- `service/stage` Feature locking service\n- `service/status` Implementation of the /status endpoint\n- `service/workspace` The other main service in WSM. Manages CRUD for workspaces and cloud\n  contexts.\n- `resources/` Properties definitions, database schema definitions, and the REST API definition\n\n### GitHub Actions\n\nWe currently have these workflows:\n\n| Workflow         | Triggers               | Work                                                                                                        |\n|------------------|------------------------|-------------------------------------------------------------------------------------------------------------|\n| _test_           | on PR and merge to dev | runs the unit, connected and azure tests                                                                    |\n| _pr-integration_ | on PR and merge to dev | runs the TestRunner-based integration test suite from the GHA host VM                                       |\n| _nightly-tests_  | nightly at 2am         | runs the TestRunner-based integration, perf, and resiliency test suites on the wsmtest personal environment |\n| _tag-publish_    | on merge to dev        | tags, version bumps, publishes client to artifactory, pushes image to GCR                                   |\n\n## Tests\n\n### Running tests\n\n```sh\n# Unit tests\n./gradlew :service:unitTest\n\n# Connected tests\n./gradlew :service:connectedTest\n\n# ConnectedPlus tests\n./gradlew :service:connectedPlusTest\n\n```\n\nFor integration tests, see [Integration README](integration/README.md).\n\n### Unit Tests\n\nThe unit tests are written using JUnit. The implementations are in\n`src/test/java/bio/terra/workspace/`.\nSome unit tests depend on the availability of a running Postgresql server.\n\nHere are common annotations that may be used when setting up a unit test suite:\n\n- `@BaseTest` - custom annotation to set active profiles used for all tests (including human-readable logging)\n- `@Unit` - custom annotation to set tags and active profiles used for all unit tests\n- `@ExtendWith(MockitoExtension.class)` - extension to use mocks in tests with strict stubbing by default\n- `@SpringBootTest(classes = {ClassToLoad.class, ...})` - annotation to run Spring Boot tests, limiting the\n  ApplicationContext to the component classes under test\n\nHistorically, we have used `@SpringBootTest` to load a full ApplicationContext for our application to use in testing.\nBut overusing this initialization when we only need to facilitate pure unit tests, mocking, and/or a small subset of\ncontext configuration leads to long-running test suites and disincentivizes writing unit-testable code.\n\nWhen adding new unit tests, consider the narrowest required slice of initialization. If the code you are testing is\ndifficult to unit test, it would likely benefit from modification.\n\n#### Spring Boot Tests\n\nEvery combination of `@MockBean` creates a distinct Spring application context. Each context holds several\ndatabase connection pools: (WSM db, WSM Stairway db, and any amalgam connection pools). We have run out of\ndatabase connections due to cached application contexts.\n\nTo reduce the number of unique combinations, we have put **ALL** `@MockBean` into test base classes. That helps\nlimit the unique combinations. You should **NEVER** code a naked `@MockBean` in a test. They should always be\nspecified in these bases. That helps us control the number of unique combinations we have.\n\nThe test base classes can be found in `src/test/java/bio/terra/workspace/common/`.\n\nThe current inheritance for Spring Boot unit test base classes looks like this:\n\n- `BaseSpringBootTest` - the base class for Spring Boot unit and connected tests\n    - `BaseSpringBootUnitTestMocks` - the base set of mocks shared by all Spring Boot unit tests\n        - `BaseSpringBootUnitTest` - enables the right test tags and profiles for Spring Boot unit tests\n            - `BaseSpringBootUnitTestMockDataRepoService` - adds one more mock; used by several tests\n            - `BaseSpringBootUnitTestMockGcpCloudContextService` - adds one more mock; used by several tests\n        - `BaseAzureSpringBootUnitTest` - adds mocks shared by azure Spring Boot unit tests and enables the right test\n          tags and profiles\n\nWe keep the Azure tests separated from the general tests, because the Azure feature is not live in all environments.\nThose tests will not successfully run in those environments.\n\n### Connected Tests\n\nThe connected tests are also written using JUnit.\nThe implementations are mixed in with the unit tests in\n`src/test/java/bio/terra/workspace/`. Connected tests derive from `common/BaseConnectedTest.java`.\nConnected tests depend on the availability of a running Postgresql server. They also rely\non a populated \"config\" directory containing service accounts and keys that allows the tests\nto use dependent services such as Sam, Buffer, and TDR. The config collecting process relies on\nsecrets maintained in Vault in the Broad Institute environment.\n\nIn general, developers writing new endpoints should add MockMVC-based unit or\nconnected tests to test their code (\nexample: [WorkspaceApiControllerTest](service/src/test/java/bio/terra/workspace/app/configuration/external/controller/WorkspaceApiControllerTest.java)).\nThese tests let us act as if we're making HTTP calls against a local server\nand validate the full request lifecycle through all\nthe [layers of WSM](#layering), whereas the previous style of\nservice-only tests did not cover code in the controller layer.\n\nSome connected tests are tagged as `connectedPlus` tests primarily to control\nthe subset of tests required to be run for pull requests. Typically,\nconnectedTests are run as part of PR checks and connectedPlus tests run longer\nand executed as part of nightly sanity tests.\n\n### Integration Tests\n\nIntegration testing is done using\n[Test Runner](https://github.com/DataBiosphere/terra-test-runner).\nThe integration tests live in the `integration` project. Consult the integration\n[README](integration/README.md) for more details.\n\nIn the early days of the project, there were JUnit-based integration tests. We are in\nprocess of migrating them to Test Runner.\n\n### Pact Tests\n\nPact testing ensures workspace manager's APIs are compatible with the assumptions made\nby its clients and that workspace manager's assumptions about its dependency APIs are\nalso correct.\n\nPact testing involves interacting with Pact Broker which requires a little setup.\n\nTo run pact tests locally:\n\n```\n# Get Pact Broker credentials\n./service/src/test/render-pact-configs.sh\n# Reload your environment variables, e.g. src ~/.zshrc\n\n# Pact contract test settings\nexport PACT_BROKER_USERNAME=$(cat /tmp/pact-ro-username.key)\nexport PACT_BROKER_PASSWORD=$(cat /tmp/pact-ro-password.key)\n\n./gradlew verifyPacts\n```\n\nIf you're working on adding new pacts and making local changes to them, it might be helpful to use a local\nPact Broker instead. To set up a local Pact Broker, see:\n[Contract Test Local Development](https://broadworkbench.atlassian.net/wiki/spaces/IRT/pages/2829680649/Contract+Test+Local+Development).\n\nOnce you have a local Pact Broker, you can override the `PACT_BROKER_URL` environment variable:\n\n```\nPACT_BROKER_URL=http://localhost:9292 ./gradlew verifyPacts\n```\n\n### Making tests fast\n\n#### Create one workspace/context/resource for entire test\n\nFor connected tests, creating workspace/context is slow. Use `@BeforeAll` /\n`@TestInstance(Lifecycle.PER_CLASS)` to create one workspace/context that every\ntest in the test file can use. [Example PR.](https://github.com/DataBiosphere/terra-workspace-manager/pull/941)\n\nIf your test changes the workspace, just undo the changes at the end of the test.\nFor example, if your test sets workspace properties, delete the properties at the\nend of your test.\n\nSimilarly, if a resource is used by multiple tests, initialize it in `setup()`.\nFor example, `ControlledGcpResourceApiControllerBqDatasetTest.java` has many\ntests that clone a BQ dataset. The source BQ dataset is initialized once in\n`setup()`, and reused for many tests.\n\n#### For local runs, skip workspace/context creation\n\nThis section is optional. For local development, it's useful to run tests\nagainst existing workspaces/contexts, so you don't have to wait for workspace/context\ncreation. These changes should not be merged.\n\n- [Comment out these lines](https://cs.github.com/DataBiosphere/terra-workspace-manager/blob/05dba30e7f597690c46c95a974d31bde532bcbbd/service/src/main/java/bio/terra/workspace/app/StartupInitializer.java?q=startupinitializer#L33-L37)\n  When test starts postgres, DB won't be wiped.\n- Comment\n  out [`cleanup()`](https://github.com/DataBiosphere/terra-workspace-manager/blob/0764301d03814ab13e6cce5291e191201bddd205/service/src/test/java/bio/terra/workspace/app/controller/ControlledGcpResourceApiControllerBqDatasetTest.java#L132-L133)\n  in your test,\n  so workspaces aren't deleted at the end of the test.\n- Run a test so that `setup()` creates workspace(s). Get workspace ID from\n  DB:\n  ```\n  PGPASSWORD=dbpwd psql postgresql://127.0.0.1:5432/wsm_db -U dbuser\n  wsm_db=\u003e select workspace_id from cloud_context;\n  \n  # If you need projectId\n  wsm_db=\u003e select workspace_id, context-\u003e\u003e'gcpProjectId' from cloud_context;\n  ```\n- In `setup()`, comment out workspace initialization and add:\n  ```\n  workspaceId = UUID.fromString(\"workspace-id\");\n  ```\n\nPre-requisites:\n\n- Your test must store `workspaceId`, not `workspace`.\n- Your test must be able to run more than once on the same workspace. For example,\n  instead of creating a resource named `resource-name`, call it `TestUtils.appendRandomNumber(\"resource-name\")`.\n\n### Cleaning up workspaces in tests\n\nWe have 2 ways of cleaning up resources (WSM workspace, SAM workspace, GCP project):\n\n1. Connected tests use Janitor. Janitor deletes GCP project and not SAM\n   workspace (see\n   [here](https://github.com/DataBiosphere/terra-workspace-manager/pull/755#discussion_r942717257) for details).\n2. Tests call WSM `deleteWorkspace()`. This deletes WSM workspace + SAM workspace +\n   GCP project.\n\nConnected tests that use mock SamService: Tests don't need to call\n`deleteWorkspace()` because there is no SAM workspace to clean up.\n\nConnected tests that use real SamService: Tests should call `deleteWorkspace()`\nto clean up SAM workspaces. Why not just call `deleteWorkspace()` and not use\njanitor? Janitor is useful in case test fails (or `deleteWorkspace()` fails).\n\nIntegration tests: Tests should call `deleteWorkspace()` because integration\ntests don't use janitor. Most tests don't need to worry about this because\n`WorkspaceAllocateTestScriptBase.java` deletes the workspace it creates. If\nyour test creates a workspace, it must delete the workspace.\n\n## SourceClear\n\n[SourceClear](https://srcclr.github.io) is a static analysis tool that scans a project's Java\ndependencies for known vulnerabilities. If you are working on addressing dependency vulnerabilities\nin response to a SourceClear finding, you may want to run a scan off of a feature branch and/or local code.\n\n### Github Action\n\nYou can trigger WSM's SCA scan on demand via its\n[Github Action](https://github.com/broadinstitute/dsp-appsec-sourceclear-github-actions/actions/workflows/java-python-wsm.yaml),\nand optionally specify a Github ref (branch, tag, or SHA) to check out from the repo to scan. By default,\nthe scan is run off of WSM's `main` branch.\n\nHigh-level results are outputted in the Github Actions run.\n\n### Running Locally\n\nYou will need to get the API token from Vault before running the Gradle `srcclr` task.\n\n```sh\nexport SRCCLR_API_TOKEN=$(vault read -field=api_token secret/secops/ci/srcclr/gradle-agent)\n./gradlew srcclr\n```\n\nHigh-level results are outputted to the terminal.\n\n### Veracode\n\nFull results including dependency graphs are uploaded to\n[Veracode](https://sca.analysiscenter.veracode.com/workspaces/jppForw/projects/204681/issues)\n(if running off of a feature branch, navigate to Project Details \u003e Selected Branch \u003e Change to select your feature\nbranch).\nYou can request a Veracode account to view full results from #dsp-infosec-champions.\n\n## Adding a new flight\n\nRefer to https://github.com/DataBiosphere/stairway for implementing a stairway flight.\nWhen a new flight is added, add a new Enum entry in\nservice/src/main/java/bio/terra/workspace/common/logging/model/ActivityFlight.java.\n\nIf the flight has `OperationType` `DELETE`, a flight might fail while the target\nis still deleted from the database. Check whether the target is deleted\nin the database and log a `DELETE` activity in `WorkspaceActivityLogHook`.\n\n## Logging During Test Runs\n\n### Seeing Log Output\n\nThe default logging set in the `build.gradle` file does not show the log messages written\nto the console. The default setup is:\n\n```\n    testLogging {\n        events = [\"passed\", \"failed\", \"skipped\", \"started\"]\n    }\n```\n\nYou can add events to the logging so that you see the log details:\n\n```\n        events = [\"passed\", \"failed\", \"skipped\", \"started\", \"standard_error\", \"standard_out\"]\n```\n\n### Human Readable Logging\n\nThe default log format is a JSON format that presents well in Log Explorer, but is not so\neasy to read. You get human readable logging by setting the `human-readable-logging`\nSpring profile.\n\nYou can do this in any of the many ways to provide properties to Spring. A simple way is\nby setting the environment variable as:\n\n```sh\nexport spring_profiles_include=human-readable-logging\n```\n\n### Controlling Log Level\n\nBy default logging at INFO level and above are displayed. You can change the logging level\nby setting yet another Spring property. For example, you can set all things terra to log\nat debug level by adding this to a property YAML file (such as application.yaml):\n\n```\nlogging.level.bio.terra: debug\n# Print SQL queries\nlogging.level.org.springframework.jdbc.core: trace\n# Print SQL transactions\nlogging.level.org.springframework.transaction.interceptor\n```\n\nYou can be more precise by putting more of the path in. You can use YAML syntax to include\nmultiple entries, something like (but I did not test this):\n\n```shell script\nlogging:\n  level:\n    bio:\n      terra:\n        workspace:\n          service:\n            resource:\n              controlled:\n                ControlledGcsBucketResource: debug\n                ControlledBigQueryDatasetResource: warn\n```\n\nAnd you can use an environment variable instead of editing a property file.\n\nIf you are also using human readable logging, then you can create the file\n`application-human-readable-logging.yml` and put the logging property in there.\nSpring auto-magically searches for properties files for the active profiles.\n\n## Update custom IAM role permissions for GCP projects.\n\nFor project level permissions, update `CloudSyncRoleMapping`. For resource\nlevel permissions, update `CustomGcpIamRoleMapping`.\n\nWhen the PR with new permissions is merged and released, it will only be applied\nto the workspace created **after** the WSM with the new release version. To\nallow existing workspaces to contains the new permissions, a SAM admin must\nrun the `Admin` api `syncIamRoles` endpoint.\n\nFor each environment:\n\n- dry run, check GCP logs to confirm new permissions/GCP projects as expected.\n- wet run.\n\n## Tips\n\n- Check out [gdub](https://github.com/gdubw/gdub), it'll save you typing `./gradlew` over\n  and over, and also takes care of knowing when you're not in the root directory, so you\n  don't have to figure out the appropriate number of `../`s.\n- In IntelliJ, instead of running the local server with `bootRun`, use the `Main` Spring\n  Boot configuration shared in the `.run` directory. This allows you to debug\n  service code while running integration tests or using Swagger UI. To edit it, click on\n  it (in the upper right of the window), and click `Edit Configurations`.\n\n    - The environment variable GOOGLE_APPLICATION_CREDENTIALS must be set to the path to wsm-sa.json\n      in the `config` directory. If this variable is missing, you may get a mysterious\n      Oauth2 error when setting up a spending profile (or have other undefined behavior).\n    - The Working Directory should be set to the full path to the `service` directory\n    - For readable logs, put `human-readable-logging` in the `Active Profiles` field.\n    - You can get live-ish reloading of for the local Swagger UI by adding the following\n      override parameter:\n      `spring.resources.static-locations:file:src/main/resources/api`. It's not true live\n      reloading, you still have to refresh the browser, but at least you don't have to\n      restart the server.\n      ![Main Run Configuration Dialog](docs/images/main_run_config.png)\n- To run unit and connected tests with a local DB (which can be helpful for examining DB contents after testing), set\n  the `TEST_LOCAL_DB` environment variable\n  to point to a local postgres URI, e.g `export TEST_LOCAL_DB='postgresql://127.0.0.1:5432'`. See [above](#postgres) for\n  setting up a local DB.\n    - Note that parallel tests using a shared database may interfere with each\n      other - [you can set the `TEST_SINGLE_THREAD` env var](service/gradle/testing.gradle) to restrict tests to a\n      single thread.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatabiosphere%2Fterra-workspace-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdatabiosphere%2Fterra-workspace-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatabiosphere%2Fterra-workspace-manager/lists"}