Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 44 additions & 86 deletions DEVNOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@ 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
HTTPS credentials or a GitHub SSH key. The script tries HTTPS first, then SSH. The script stops at
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.
Expand All @@ -61,17 +64,30 @@ 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

`liquibase.runOnStartup` is an optional flag. The application runs the Liquibase changesets at
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:
Expand All @@ -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:

Expand All @@ -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).
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions scripts/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 17 additions & 3 deletions scripts/render-chart-configs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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 "$@"
Expand Down
Loading
Loading