From a4a11304702b369121d74da63ef96dfddaaaf107 Mon Sep 17 00:00:00 2001 From: Gregory Rushton Date: Fri, 23 Oct 2026 10:32:31 -0400 Subject: [PATCH] [DT-4348][risk=no] Render the local compose files and .env (3/5) Co-Authored-By: Claude Opus 5.5 --- DEVNOTES.md | 130 ++++++++-------------- scripts/lib/common.sh | 30 ++++++ scripts/render-chart-configs.sh | 20 +++- scripts/render-configs.sh | 117 ++++++++++++++++++-- scripts/templates/docker-compose.yaml | 149 ++++++++++++++++++++++++++ 5 files changed, 351 insertions(+), 95 deletions(-) create mode 100644 scripts/templates/docker-compose.yaml diff --git a/DEVNOTES.md b/DEVNOTES.md index 4e96c2dbb..cd25c022c 100644 --- a/DEVNOTES.md +++ b/DEVNOTES.md @@ -44,6 +44,7 @@ local stack needs. Do not edit these files by hand. The next render replaces the |---|---|---| | `server.crt`, `server.key`, `ca-bundle.crt` | The `local-dev-cert` secret in the dev cluster | Always | | `consent.yaml`, `site.conf`, `oauth2.conf` | The consent chart in `terra-helmfile`, with the dev values and [local-values.yaml](scripts/templates/local-values.yaml) | `--write_chart_configs true` | +| `docker-compose.yaml`, `.env` | [docker-compose.yaml](scripts/templates/docker-compose.yaml) in this repo, and values from the dev cluster | `--write_compose true` | You need `gcloud`, `kubectl`, `helm`, `git` and `openssl`, and the non-split Broad VPN for the cluster steps. The chart configs need read access to `broadinstitute/terra-helmfile`, with git @@ -51,7 +52,9 @@ HTTPS credentials or a GitHub SSH key. The script tries HTTPS first, then SSH. T the first step that fails, and that step leaves the old files as they were. The first time, run: ```bash -./scripts/render-configs.sh --write_chart_configs true +./scripts/render-configs.sh --all true +mvn clean compile +docker compose -f config/docker-compose.yaml -f config/docker-compose.override.yaml up -d ``` * After a cert rotation, run `./scripts/render-configs.sh` with no options. @@ -61,7 +64,20 @@ the first step that fails, and that step leaves the old files as they were. The [render-chart-configs.sh](scripts/render-chart-configs.sh) on its own. The rendered files are the same as the files that dev runs. The only local changes are in -`local-values.yaml`. See [the plan](docs/plans/render-local-configs-plan.md#all-local-changes). +`local-values.yaml` and in the `-Ddw.*` flags of the compose template. See +[the plan](docs/plans/render-local-configs-plan.md#all-local-changes). Put changes for your own +machine in `config/docker-compose.override.yaml`. The script never changes that file. Name both +compose files in each `docker compose` command: Compose does not load the override file on its own +when a command has `-f`. + +`config/.env` holds the local secrets, with mode 600. Do not put a secret in a tracked file. The +script keeps each value in an old `.env`, and it copies the lines that it does not manage with no +change. The local database uses the dev database user and password from `consent-secrets`, the +same values that `duos-ui` writes to its `.env.local`. So the `duos-ui` compose mode against a +local consent stack (`docker-compose.consent.yaml`) connects with no change. + +The local database loads the dump that `CONSENT_DB_DUMP` in `config/.env` names. Put a dump file in +`config/`, and set `CONSENT_DB_DUMP` to its file name. #### Liquibase @@ -69,9 +85,9 @@ The rendered files are the same as the files that dev runs. The only local chang startup only when the flag is `true`. If a config omits the flag or the whole `liquibase` block, the flag is `false`. Environments that run the Liquibase migration Job omit it. In `terra-helmfile`, that Job is a separate Kubernetes Job that Argo CD runs before the deployment -(sync-wave `-1`). So the rendered `consent.yaml` has no `liquibase` block. A local database -restored from an older dump needs the newer changesets, so pass -`-Ddw.liquibase.runOnStartup=true`, or add this block to your config: +(sync-wave `-1`). So the rendered `consent.yaml` has no `liquibase` block. The compose template +passes `-Ddw.liquibase.runOnStartup=true`, because a local database restored from an older dump +needs the newer changesets. If you run the app another way, add this block to your config: ```yaml liquibase: @@ -96,12 +112,8 @@ gcloud auth application-default login The access tokens are short-lived, but Application Default Credentials may store a refresh token locally. To remove the local ADC credential, run `gcloud auth application-default revoke`. Do not create or request a JSON key for `consent-dev`: keys on that account are managed by Yale, which disables keys it did not create. -```yaml -googleStore: - # password: intentionally unset locally -> Application Default Credentials - endpoint: https://storage.googleapis.com/ - bucket: broad-dsde-dev-consent # shared with the deployed dev environment -``` +The compose template sets `-Ddw.googleStore.password=` (blank), so the app uses these +credentials. The bucket is `broad-dsde-dev-consent`, which the deployed dev environment also uses. If GCS calls fail with a quota-project warning when running as yourself, set one: @@ -114,34 +126,22 @@ Deployed environments are unchanged: the Helm chart mounts a Yale-managed key at #### Running in Docker Compose -The container has no `gcloud` login of its own, so mount your ADC file and point the SDK at it: - -```yaml - consent: - environment: - - GOOGLE_APPLICATION_CREDENTIALS=/etc/gcloud/application_default_credentials.json - volumes: - - ~/.config/gcloud/application_default_credentials.json:/etc/gcloud/application_default_credentials.json:ro -``` +The app container has no `gcloud` login of its own. The compose template mounts +`~/.config/gcloud/application_default_credentials.json` read-only, and sets +`GOOGLE_APPLICATION_CREDENTIALS`. If the file does not exist, compose stops with an error. Run +`gcloud auth application-default login` first. ### Spin up application -```bash -mvn clean package -java -jar target/consent-*.jar server config/consent.yaml -``` - -Or with docker compose, using your own `config/docker-compose.yaml`: +Make the config first (see "Render Configs"), then: ```bash mvn clean compile -docker-compose -p consent -f config/docker-compose.yaml up +docker compose -f config/docker-compose.yaml -f config/docker-compose.override.yaml up -d ``` -The compose command starts the app from the classpath, not with `java -jar`, so it does not read -the `Enable-Native-Access` jar manifest entry. Add `"--enable-native-access=ALL-UNNAMED"` right -after `"java"` in the compose `command` array. Without it, Java warns when Conscrypt loads its -native library, and a future Java release will block that call. +The compose template starts the app from `target/classes`, so run `mvn clean compile` after each +code change, and restart the `app` service. Visit the local swagger page at the URL configured for your setup (the compose setup used at Broad serves https://local.dsde-dev.broadinstitute.org:27443). @@ -155,19 +155,18 @@ and the defaults should be correct. ### Developing with a local Elastic Search instance: #### Running with X-Pack Security enabled -`config/` is not version-controlled — it is rendered per developer (see "Render Configs" above) — so -none of this arrives with a `git pull`; it has to go into your own `config/docker-compose.yaml`. The -example stanza below turns security **on**, which is what the security work -(`GET /api/elasticSearch/capabilities` and native DLS/FLS) needs, and keeps a -`${ES_SECURITY_ENABLED:-true}` gate so you can drop back to an unauthenticated cluster for a single -run. Two things make it work, and they have to agree: +The `elastic` service in the [compose template](scripts/templates/docker-compose.yaml) turns +security **on**, which is what the security work (`GET /api/elasticSearch/capabilities` and native +DLS/FLS) needs, and keeps a `${ES_SECURITY_ENABLED:-true}` gate so you can drop back to an +unauthenticated cluster for a single run. Two things make it work, and they have to agree: -* `ELASTIC_PASSWORD` in the compose `elastic` service bootstraps the `elastic` superuser. -* `authUser` / `authPassword` in `config/consent.yaml` are the credentials Consent sends. They are - harmless when security is off — the ES client only sends credentials in response to a 401 - challenge, which a security-disabled cluster never issues. +* `ELASTIC_PASSWORD` in `config/.env` bootstraps the `elastic` superuser. +* The `-Ddw.elasticSearch.authUser` and `-Ddw.elasticSearch.authPassword` flags on the app are the + credentials Consent sends. They use the same `ELASTIC_PASSWORD`. They are harmless when security + is off — the ES client only sends credentials in response to a 401 challenge, which a + security-disabled cluster never issues. -DLS and FLS are Platinum features, so the stanza self-generates a 30-day **trial** license via +DLS and FLS are Platinum features, so the service self-generates a 30-day **trial** license via `xpack.license.self_generated.type`. That setting only applies the first time a cluster forms. If your `elastic` data volume predates it, the cluster keeps its `basic` license and DLS/FLS come back `LICENSE_BLOCKED`. Activating the trial is a separate, deliberate step — never something a @@ -195,49 +194,8 @@ correct here — transport SSL is only required for multi-node clusters. To get an unauthenticated cluster back for a run, without editing your compose file again: ```bash -ES_SECURITY_ENABLED=false docker-compose -p consent -f config/docker-compose.yaml up -``` - -An example docker-compose stanza for elastic: - ``` - elastic: - image: docker.elastic.co/elasticsearch/elasticsearch:9.5.1 - ports: - - "9200:9200" - container_name: elastic - volumes: - - elastic:/usr/share/elasticsearch/data - deploy: - resources: - limits: - memory: 4gb - environment: - - "ES_JAVA_OPTS=-Xms2g -Xmx2g" - # X-Pack Security is ON here: the capability endpoint and Epic D (native DLS/FLS) need it. - # Epics A-C and E (application-layer fallback) do not, so to run one session unauthenticated: - # - # ES_SECURITY_ENABLED=false docker-compose -p consent -f config/docker-compose.yaml up - # - # See DEVNOTES.md ("Developing with a local Elastic Search instance") for the full workflow. - - xpack.security.enabled=${ES_SECURITY_ENABLED:-true} - # DLS/FLS is a Platinum feature, so self-generate a trial rather than the default basic - # license. Only applies the first time a cluster forms — on an existing `elastic` volume, - # activate it by hand instead, once per major version per cluster, either through Consent's - # admin endpoint or straight at the cluster: - # - # curl -X POST 'localhost:8000/api/elasticSearch/license/trial?acknowledge=true' - # curl -u elastic:devpassword -XPOST 'localhost:9200/_license/start_trial?acknowledge=true' - - xpack.license.self_generated.type=${ES_LICENSE_TYPE:-trial} - # Bootstraps the `elastic` superuser password when security is on; ignored when it is off. - # Must match authUser/authPassword in consent.yaml. - - ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-devpassword} - # Correct in both modes: transport SSL is only required for multi-node clusters. - - xpack.security.transport.ssl.enabled=false - # Keep the HTTP layer on plain http so consent's `protocol: http` client keeps working. - - xpack.security.http.ssl.enabled=false +ES_SECURITY_ENABLED=false docker compose -f config/docker-compose.yaml -f config/docker-compose.override.yaml up -d ``` -I also suggest changing the default bucket location so uploaded -ontology files do not interfere with other dev environments. #### Which work actually needs security enabled @@ -278,7 +236,7 @@ Caveats: as `trial_available`, as does `GET /_license/trial_status` on the cluster). After 30 days the license reverts to basic and DLS/FLS stops working. For another trial on the same major version, wipe the cluster's data volume; after a major-version upgrade, check - trial_available because the cluster may be eligible again: `docker-compose -p consent -f config/docker-compose.yaml down` then + trial_available because the cluster may be eligible again: `docker compose -f config/docker-compose.yaml -f config/docker-compose.override.yaml down` then `docker volume rm consent_elastic` (this deletes local indices — they must be re-indexed). - Authentication, role-based access control, and API keys all work fine on the basic license. Only the DLS/FLS grants require trial/Platinum. @@ -292,7 +250,7 @@ upgrade does not need to be re-verified by hand: ```bash # 1. bump ElasticSearchTestCluster.IMAGE (the only version pin in the test tree) ./mvnw test -Dgroups=elasticsearch -# 2. then match it here: config/docker-compose.yaml, pom.xml (elasticsearch-rest-client), this file +# 2. then match it here: scripts/templates/docker-compose.yaml, pom.xml (elasticsearch-rest-client) ``` These four classes carry the JUnit tag `elasticsearch` and are **not** run by CI — see diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index 60170ceb3..9fde04801 100644 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -4,6 +4,36 @@ # Written for bash 3.2, the bash that macOS has in /bin/bash. # +# Print the value of KEY from an env file, or nothing. One layer of surrounding quotes is +# removed, because the scripts write values in single quotes. The file is never sourced: +# config/.env holds secrets, and a value must not run as shell code. +env_get() { + local file="$1" key="$2" val + [[ -f "$file" ]] || return 0 + val=$(grep -E "^${key}=" "$file" | tail -1 | cut -d= -f2- || true) + if [[ $val == \'*\' || $val == \"*\" ]]; then + val=${val:1:${#val}-2} + fi + echo "$val" +} + +# Set KEY to VALUE in an env file, and keep the other lines. The new file replaces the old one +# with a rename, so a failure leaves the old file as it was. The file gets mode 600. +env_set() { + local file="$1" key="$2" value="$3" tmp + if [[ "$value" == *"'"* ]]; then + echo "ERROR: the value for $key has a single quote, which config/.env cannot hold." >&2 + return 1 + fi + tmp=$(mktemp "$(dirname "$file")/.env.XXXXXX") + chmod 600 "$tmp" + if [[ -f "$file" ]]; then + grep -vE "^${key}=" "$file" > "$tmp" || true + fi + printf "%s='%s'\n" "$key" "$value" >> "$tmp" + mv "$tmp" "$file" +} + # The active gcloud account, or nothing. gcloud_account() { gcloud auth list --filter=status:ACTIVE --format='value(account)' 2> /dev/null | head -1 diff --git a/scripts/render-chart-configs.sh b/scripts/render-chart-configs.sh index ecfad14f3..2cdaf2e18 100755 --- a/scripts/render-chart-configs.sh +++ b/scripts/render-chart-configs.sh @@ -4,6 +4,9 @@ # the dev values and scripts/templates/local-values.yaml. The rendered files are the reference # for local config: do not edit them. Put a local change in local-values.yaml, with its reason. # +# The script also sets PROXY_IMAGE in the .env file of the output folder to the proxy image of the +# rendered deployment, so the local proxy is the same as the dev proxy. +# # You MUST have helm and git installed. Without --helmfile_dir, you MUST also have git read # access to broadinstitute/terra-helmfile, over HTTPS or SSH. This script does not need the VPN. # @@ -26,7 +29,8 @@ OUTPUT_DIR="$REPO_ROOT/config" usage() { cat << EOF Usage: $0 [OPTION]... -Render consent.yaml, site.conf and oauth2.conf from the consent chart in terra-helmfile. +Render consent.yaml, site.conf and oauth2.conf from the consent chart in terra-helmfile, and +set PROXY_IMAGE in .env. --helmfile_ref REF The terra-helmfile branch or tag to render. Defaults to master. --helmfile_dir DIR Render a local terra-helmfile checkout as it is, with changes that are @@ -163,7 +167,8 @@ render() { -f "$SOURCE_DIR/values/app/consent/live/dev.yaml" \ -f "$LOCAL_VALUES" \ --show-only templates/configmap.yaml \ - --show-only templates/proxy-configmap.yaml > "$WORK_DIR/rendered.yaml" \ + --show-only templates/proxy-configmap.yaml \ + --show-only templates/deployment.yaml > "$WORK_DIR/rendered.yaml" \ || error "helm template failed for the consent chart." local stage="$WORK_DIR/out" @@ -181,10 +186,19 @@ render() { done grep -q 'ProxyPass' "$stage/site.conf" || error "The rendered site.conf has no ProxyPass. Update scripts/render-chart-configs.sh." + # The image of the consent-proxy container in the rendered deployment, with all value overrides. + local proxy_image + proxy_image=$(awk ' + /^ *- name: consent-proxy *$/ { on = 1; next } + on && /^ *image:/ { gsub(/"/, ""); print $2; exit } + ' "$WORK_DIR/rendered.yaml") + [[ -n "$proxy_image" ]] || error "The rendered deployment has no consent-proxy image. Update scripts/render-chart-configs.sh." + # Move the files into place only after all three are good. mkdir -p "$OUTPUT_DIR" mv "$stage/consent.yaml" "$stage/oauth2.conf" "$stage/site.conf" "$OUTPUT_DIR/" - echo "Wrote consent.yaml, site.conf and oauth2.conf to $OUTPUT_DIR" + env_set "$OUTPUT_DIR/.env" PROXY_IMAGE "$proxy_image" + echo "Wrote consent.yaml, site.conf and oauth2.conf to $OUTPUT_DIR, and set PROXY_IMAGE to $proxy_image" } parse_cli_args "$@" diff --git a/scripts/render-configs.sh b/scripts/render-configs.sh index 065a9b7a1..c10952129 100755 --- a/scripts/render-configs.sh +++ b/scripts/render-configs.sh @@ -4,11 +4,11 @@ # ("Render Configs") and docs/plans/render-local-configs-plan.md. # # A run with no options writes only the certs, which rotate every 3 months. Other options turn -# on the other files. +# on the other files. --all turns on all of them. # -# You MUST be on the non-split Broad VPN for the cert step, which reads the dev cluster. You MUST -# be logged in with gcloud. The chart configs need git read access to -# broadinstitute/terra-helmfile, over HTTPS or SSH. +# You MUST be on the non-split Broad VPN for the steps that read the dev cluster: the certs, and +# the .env values that an old .env does not have. You MUST be logged in with gcloud. The chart +# configs need git read access to broadinstitute/terra-helmfile, over HTTPS or SSH. # # Each step writes to a staging folder first, and moves its files into place only when the step # succeeds. The script stops at the first step that fails, and that step leaves the old files as @@ -23,6 +23,7 @@ REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) # shellcheck source=lib/common.sh source "$SCRIPT_DIR/lib/common.sh" +COMPOSE_TEMPLATE="$SCRIPT_DIR/templates/docker-compose.yaml" ZONE="us-central1-a" CLUSTER="terra-dev" @@ -33,18 +34,25 @@ HELMFILE_REF_SET="false" HELMFILE_DIR="" CONFIG_DIR="$REPO_ROOT/config" WRITE_CHART_CONFIGS="false" +WRITE_COMPOSE="false" +FETCH_SENDGRID="false" +ALL="false" usage() { cat << EOF Usage: $0 [OPTION]... Make the local config files in config/. With no options, write only the certs. - --project PROJECT Google project of the dev cluster, for the certs. - Defaults to broad-dsde-dev. + --project PROJECT Google project of the dev cluster, for the certs and dev + secrets. Defaults to broad-dsde-dev. --helmfile_ref REF The terra-helmfile branch or tag to render. Defaults to master. --helmfile_dir DIR Render a local terra-helmfile checkout as it is, with changes that are not committed. Cannot be used with --helmfile_ref. --write_chart_configs true|false Write consent.yaml, site.conf and oauth2.conf. Defaults to false. + --write_compose true|false Write docker-compose.yaml and .env. Defaults to false. + --fetch_sendgrid true|false Put the dev SendGrid key in .env. Defaults to false. + --all true|false Write the certs, the chart configs and the compose files. + Defaults to false. --config_dir DIR Where to write the files. Defaults to config/ in the repo root. --help Show this help. EOF @@ -65,6 +73,9 @@ parse_cli_args() { --helmfile_ref) HELMFILE_REF=$2; HELMFILE_REF_SET="true" ;; --helmfile_dir) HELMFILE_DIR=$2 ;; --write_chart_configs) check_bool "$1" "$2"; WRITE_CHART_CONFIGS=$2 ;; + --write_compose) check_bool "$1" "$2"; WRITE_COMPOSE=$2 ;; + --fetch_sendgrid) check_bool "$1" "$2"; FETCH_SENDGRID=$2 ;; + --all) check_bool "$1" "$2"; ALL=$2 ;; --config_dir) CONFIG_DIR=$2 ;; *) error "Unknown option: $1. Try --help to see a list of all options." ;; esac @@ -97,6 +108,14 @@ use_cluster() { CLUSTER_READY="true" } +# Print a value from a secret in the terra-dev namespace. Fail if it is empty. +dev_secret() { + local value + value=$(kubectl --request-timeout=30s -n terra-dev get secret "$1" -o "go-template={{ index .data \"$2\" | base64decode }}") + [[ -n "$value" ]] || error "$1 in the terra-dev namespace has no $2." + echo "$value" +} + write_certs() { local stage="$STAGE_DIR/certs" f use_cluster @@ -121,6 +140,76 @@ write_chart_configs() { fi "$SCRIPT_DIR/render-chart-configs.sh" --output_dir "$stage" "${source_args[@]}" mv "$stage/consent.yaml" "$stage/site.conf" "$stage/oauth2.conf" "$CONFIG_DIR/" + env_set "$ENV_FILE" PROXY_IMAGE "$(env_get "$stage/.env" PROXY_IMAGE)" +} + +write_compose() { + local stage="$STAGE_DIR/compose" pg_user pg_password es sendgrid b2c + echo "Writing docker-compose.yaml and .env" + mkdir -p "$stage" + + # Values in an old .env carry forward, so a new render never loses them. The script reads the + # cluster only for values that the old .env does not have. + pg_user=$(env_get "$ENV_FILE" POSTGRES_USER) + pg_password=$(env_get "$ENV_FILE" POSTGRES_PASSWORD) + if [[ -z "$pg_user" || -z "$pg_password" ]]; then + # The dev database credentials, which duos-ui also writes to its .env.local. With them, the + # duos-ui compose mode against a local consent stack connects with no change. + use_cluster + pg_user=$(dev_secret consent-secrets databaseUser) + pg_password=$(dev_secret consent-secrets databasePassword) + fi + b2c=$(env_get "$ENV_FILE" B2C_APPLICATION_ID) + if [[ -z "$b2c" ]]; then + use_cluster + b2c=$(dev_secret consent-proxy-b2c-secrets application-id) + fi + if [[ "$FETCH_SENDGRID" == "true" ]]; then + use_cluster + sendgrid=$(dev_secret consent-secrets sendgridApiKey) + else + sendgrid=$(env_get "$ENV_FILE" SENDGRID_API_KEY) + [[ -n "$sendgrid" ]] || sendgrid="placeholder" + fi + es=$(env_get "$ENV_FILE" ELASTIC_PASSWORD) + [[ -n "$es" ]] || es="devpassword" + + # Start from the old .env, so the lines that this script does not manage carry forward, as in + # duos-ui. PROXY_IMAGE is one of them: the chart step sets it. + if [[ -f "$ENV_FILE" ]]; then + cp "$ENV_FILE" "$stage/.env" + else + cat > "$stage/.env" << EOF +# Written by scripts/render-configs.sh. Values in an old .env carry forward. This file holds +# secrets: do not share it. docker-compose.yaml reads it. +EOF + fi + chmod 600 "$stage/.env" + env_set "$stage/.env" POSTGRES_USER "$pg_user" + env_set "$stage/.env" POSTGRES_PASSWORD "$pg_password" + env_set "$stage/.env" SENDGRID_API_KEY "$sendgrid" + env_set "$stage/.env" ELASTIC_PASSWORD "$es" + env_set "$stage/.env" B2C_APPLICATION_ID "$b2c" + cp "$COMPOSE_TEMPLATE" "$stage/docker-compose.yaml" + + # Back up the old files, then move the new ones into place. + if [[ -f "$ENV_FILE" ]]; then + cp -p "$ENV_FILE" "$ENV_FILE.bak" + chmod 600 "$ENV_FILE.bak" + fi + if [[ -f "$CONFIG_DIR/docker-compose.yaml" ]] && ! cmp -s "$COMPOSE_TEMPLATE" "$CONFIG_DIR/docker-compose.yaml"; then + cp -p "$CONFIG_DIR/docker-compose.yaml" "$CONFIG_DIR/docker-compose.yaml.bak" + fi + mv "$stage/.env" "$ENV_FILE" + mv "$stage/docker-compose.yaml" "$CONFIG_DIR/docker-compose.yaml" + + # Compose stops if a -f file does not exist. Never change an override that exists. + if [[ ! -e "$CONFIG_DIR/docker-compose.override.yaml" ]]; then + cat > "$CONFIG_DIR/docker-compose.override.yaml" << 'EOF' +# Changes to docker-compose.yaml for your own machine. render-configs.sh never changes this file. +services: {} +EOF + fi } parse_cli_args "$@" @@ -128,6 +217,7 @@ mkdir -p "$CONFIG_DIR" || error "Could not make $CONFIG_DIR." # config/ holds secrets. Mode 700 keeps other users on this machine out of it. chmod 700 "$CONFIG_DIR" CONFIG_DIR=$(cd "$CONFIG_DIR" && pwd) +ENV_FILE="$CONFIG_DIR/.env" # WORK_DIR holds the kubeconfig. STAGE_DIR is in config/, so each move into place is a rename on # the same file system. The trap removes both on each exit, also on Ctrl-C. @@ -139,7 +229,22 @@ cleanup() { trap cleanup EXIT trap 'exit 130' INT TERM +if [[ "$ALL" == "true" ]]; then + WRITE_CHART_CONFIGS="true" + WRITE_COMPOSE="true" +fi + write_certs if [[ "$WRITE_CHART_CONFIGS" == "true" ]]; then write_chart_configs fi +if [[ "$WRITE_COMPOSE" == "true" ]]; then + write_compose +fi + +if [[ -s "$CONFIG_DIR/docker-compose.yaml" ]]; then + echo + echo "Next steps:" + echo " mvn clean compile" + echo " docker compose -f config/docker-compose.yaml -f config/docker-compose.override.yaml up -d" +fi diff --git a/scripts/templates/docker-compose.yaml b/scripts/templates/docker-compose.yaml new file mode 100644 index 000000000..6851fe39b --- /dev/null +++ b/scripts/templates/docker-compose.yaml @@ -0,0 +1,149 @@ +# Local Consent stack. scripts/render-configs.sh copies this file to config/docker-compose.yaml. +# Do not edit the copy: the next render replaces it. Put changes for your own machine in +# config/docker-compose.override.yaml, and start the stack with both files: +# +# mvn clean compile +# docker compose -f config/docker-compose.yaml -f config/docker-compose.override.yaml up -d +# +# Values in ${...} come from config/.env, which render-configs.sh writes. Do not put a secret in +# this file. It is tracked. + +name: consent + +services: + + app: + image: us.gcr.io/broad-dsp-gcr-public/base/jre:25-jre + container_name: consent + depends_on: + sqlproxy: + condition: service_healthy + elastic: + condition: service_healthy + command: + - java + # The classpath start does not read the Enable-Native-Access manifest entry of the jar. + - --enable-native-access=ALL-UNNAMED + # The chart has placeholders for these values. Dev replaces them with -Ddw flags too. + - -Ddw.database.user=${POSTGRES_USER} + - -Ddw.database.password=${POSTGRES_PASSWORD} + - -Ddw.mailConfiguration.sendGridApiKey=${SENDGRID_API_KEY} + # Blank, so GCSService uses Application Default Credentials. Dev mounts a Yale key here. + - -Ddw.googleStore.password= + - -Ddw.elasticSearch.authUser=elastic + - -Ddw.elasticSearch.authPassword=${ELASTIC_PASSWORD} + # Dev runs the changesets in a separate Liquibase migration Job, so the chart omits this + # flag. The local database comes from an older dump and needs the newer changesets. + - -Ddw.liquibase.runOnStartup=true + # No -Dsentry.dsn: local errors must not go to the shared Sentry project. + - -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:7777 + - -Xms2G + - -Xmx6G + - -classpath + - /opt/target/classes:/opt/target/lib/* + - org.broadinstitute.consent.http.ConsentApplication + - server + - /etc/consent.yaml + environment: + GOOGLE_APPLICATION_CREDENTIALS: /etc/gcloud/application_default_credentials.json + ports: + - 8080:8080 + - 8081:8081 # admin connector + - 7777:7777 # remote debugging + # The proxy shares this network, so the proxy ports are published here. + - 27080:80 + - 27443:443 + volumes: + - ../target:/opt/target + - ./consent.yaml:/etc/consent.yaml:ro + # Run `gcloud auth application-default login` if this file does not exist. Set + # GCLOUD_ADC_FILE in config/.env to use another file, as in duos-ui. + - type: bind + source: ${GCLOUD_ADC_FILE:-~/.config/gcloud/application_default_credentials.json} + target: /etc/gcloud/application_default_credentials.json + read_only: true + bind: + create_host_path: false + restart: unless-stopped + + proxy: + # The proxy image of the dev deployment. The chart step sets it. + image: ${PROXY_IMAGE:?No proxy image. Run ./scripts/render-configs.sh --write_chart_configs true} + container_name: proxy + # The same network as the app, as in the pod. The rendered site.conf sends traffic to + # localhost:8080, so it works with no change. + network_mode: service:app + depends_on: + - app + environment: + # The rendered site.conf reads this value. Dev sets it from consent-proxy-b2c-secrets. + B2C_APPLICATION_ID: ${B2C_APPLICATION_ID} + volumes: + - ./site.conf:/etc/httpd/conf.d/site.conf:ro + - ./oauth2.conf:/etc/httpd/conf.d/oauth2.conf:ro + - ./server.crt:/etc/ssl/certs/server.crt:ro + - ./server.key:/etc/ssl/private/server.key:ro + - ./ca-bundle.crt:/etc/ssl/certs/server-ca-bundle.crt:ro + restart: unless-stopped + + # The rendered consent.yaml connects to sqlproxy:5432, so this service keeps that name. + sqlproxy: + image: postgres:16.14-alpine + container_name: localdb + ports: + - 5432:5432 + volumes: + - ./${CONSENT_DB_DUMP:?No database dump. Set CONSENT_DB_DUMP in config/.env to a dump file in config/}:/docker-entrypoint-initdb.d/consent.sql.gz:ro + environment: + PGGSSENCMODE: disable + POSTGRES_DB: consent + # The dev database credentials. The duos-ui compose mode against a local consent stack + # (docker-compose.consent.yaml) connects with the same values from its .env.local. + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + # TCP, not the socket: the server that loads the dump at first start has no TCP listener. + test: ["CMD", "pg_isready", "-h", "127.0.0.1", "-U", "${POSTGRES_USER}", "-d", "consent"] + interval: 5s + timeout: 5s + retries: 12 + # A dump load can take several minutes. + start_period: 20m + start_interval: 5s + + elastic: + image: docker.elastic.co/elasticsearch/elasticsearch:9.5.1 + container_name: elastic + ports: + - 9200:9200 + volumes: + - elastic:/usr/share/elasticsearch/data + deploy: + resources: + limits: + memory: 4gb + environment: + - "ES_JAVA_OPTS=-Xms2g -Xmx2g" + # X-Pack security is on by default. For one run without it: + # ES_SECURITY_ENABLED=false docker compose -f ... up + # See DEVNOTES.md, "Developing with a local Elastic Search instance". + - xpack.security.enabled=${ES_SECURITY_ENABLED:-true} + # DLS/FLS needs a Platinum feature. This applies only when the cluster forms the first time. + - xpack.license.self_generated.type=${ES_LICENSE_TYPE:-trial} + # Must match -Ddw.elasticSearch.authPassword on the app. + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + # Transport SSL is needed only for clusters with more than one node. + - xpack.security.transport.ssl.enabled=false + # Plain http, so the consent client (protocol: http) works. + - xpack.security.http.ssl.enabled=false + - discovery.type=single-node + - cluster.routing.allocation.disk.threshold_enabled=false + healthcheck: + # curl exits 0 on a 401, so this works with security on or off. + test: ["CMD-SHELL", "curl -s http://localhost:9200 > /dev/null"] + interval: 5s + timeout: 5s + retries: 60 + +volumes: + elastic: