diff --git a/docs/development.md b/docs/development.md index 9d6c087..0de249b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -99,7 +99,7 @@ cd apps/portal-bff && pnpm exec prisma generate && cd ../.. pnpm nx run-many -t lint test build ``` -For the BFF to actually run end-to-end, you'll also need the local infrastructure stack — Postgres, Redis, OpenTelemetry Collector — provisioned via Docker Compose: +For the BFF to actually run end-to-end, you'll also need the local infrastructure stack — Postgres, Redis, OpenTelemetry Collector — provisioned via Docker Compose. A thin wrapper script ([`infra/local/dev.sh`](../infra/local/dev.sh)) hides Compose-profile quirks and gives ergonomic verbs: ```bash # 1. Configure infra secrets (copy template, edit, do not commit). @@ -108,11 +108,12 @@ $EDITOR infra/local/.env # Set strong dev values for POSTGRES_PASSWORD and REDIS_PASSWORD. # 2. Bring up the core stack (postgres + redis + otel-collector). -docker compose -f infra/local/dev.compose.yml up -d +./infra/local/dev.sh up # 3. (Optional) Activate viewers when debugging: -docker compose -f infra/local/dev.compose.yml --profile dbtools up -d # pgweb -docker compose -f infra/local/dev.compose.yml --profile observability up -d # Jaeger UI +./infra/local/dev.sh up dbtools # adds pgweb (http://localhost:8081) +./infra/local/dev.sh up observability # adds Jaeger UI (http://localhost:16686) +./infra/local/dev.sh up all # core + every profile # 4. App-side env from .env.example, then fill in DATABASE_URL pointing # to the compose-managed Postgres (matches the values you set in @@ -120,7 +121,7 @@ docker compose -f infra/local/dev.compose.yml --profile observability up -d # cp apps/portal-bff/.env.example apps/portal-bff/.env ``` -Full reference for the local stack — service inventory, port table, persistence, bootstrap re-run procedure — lives in [`infra/README.md`](../infra/README.md) → "Local-dev stack". +`./infra/local/dev.sh help` lists the rest of the verbs (`down`, `status`, `logs`, `stop`, `restart`, `exec`). Full reference — service inventory, port table, persistence, bootstrap re-run procedure — lives in [`infra/README.md`](../infra/README.md) → "Local-dev stack". --- diff --git a/infra/README.md b/infra/README.md index e4d14d1..5ff0b4d 100644 --- a/infra/README.md +++ b/infra/README.md @@ -127,6 +127,7 @@ A Docker Compose recipe spinning up the runtime services the BFF and ADRs assume | File | Role | | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| [`local/dev.sh`](local/dev.sh) | Convenience wrapper around `docker compose` — see "Convenience script" below | | [`local/dev.compose.yml`](local/dev.compose.yml) | Service definitions: postgres, redis, otel-collector, plus pgweb and jaeger behind profiles | | [`local/.env.example`](local/.env.example) | Credentials + ports template (copy to `.env`, which is git-ignored) | | [`local/init/postgres/01-init.sql`](local/init/postgres/01-init.sql) | Bootstrap SQL for ADR-0013: audit roles + schema, applied on first boot only | @@ -135,27 +136,51 @@ A Docker Compose recipe spinning up the runtime services the BFF and ADRs assume ### First-time setup ```bash -cd infra/local - # 1. Configure local secrets (copy template, edit, do not commit). -cp .env.example .env -$EDITOR .env +cp infra/local/.env.example infra/local/.env +$EDITOR infra/local/.env # Set strong dev values for POSTGRES_PASSWORD and REDIS_PASSWORD # (defaults in the template are placeholders that the compose # rejects with `must be set in infra/local/.env` if left as-is). # 2. Bring up the core stack (postgres + redis + otel-collector). -docker compose -f dev.compose.yml up -d +./infra/local/dev.sh up # 3. (Optional) Activate viewers when needed: -docker compose -f dev.compose.yml --profile dbtools up -d # pgweb -docker compose -f dev.compose.yml --profile observability up -d # Jaeger UI -docker compose -f dev.compose.yml --profile dbtools --profile observability up -d # both +./infra/local/dev.sh up dbtools # adds pgweb +./infra/local/dev.sh up observability # adds Jaeger UI +./infra/local/dev.sh up all # core + every profile # 4. Verify health. -docker compose -f dev.compose.yml ps +./infra/local/dev.sh status ``` +### Convenience script — `dev.sh` + +[`local/dev.sh`](local/dev.sh) is a thin wrapper around `docker compose -f dev.compose.yml ...` with two reasons to exist: + +1. **Hides the Compose-profile gotcha.** `docker compose down` only operates on services whose profile is currently active — anything started under `--profile X` keeps running unless the same flag is on `down`. The script always passes every profile in scope on teardown / status / log commands, so profile-gated services (pgweb, Jaeger) are never accidentally orphaned. +2. **Ergonomic verbs** for the common workflows. `./dev.sh up all`, `./dev.sh stop pgweb`, `./dev.sh logs otel-collector`, etc. + +Run `./infra/local/dev.sh help` for the full reference. Cheat-sheet: + +| Command | Effect | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| `./infra/local/dev.sh up` | Core only (postgres + redis + otel-collector) | +| `./infra/local/dev.sh up all` | Core + every profile | +| `./infra/local/dev.sh up dbtools` | Core + pgweb | +| `./infra/local/dev.sh up observability` | Core + Jaeger | +| `./infra/local/dev.sh down` | Tear down the whole stack (every profile in scope) | +| `./infra/local/dev.sh down -v` | Tear down + wipe named volumes (incl. audit-roles bootstrap) | +| `./infra/local/dev.sh stop pgweb` | Stop one service (containers stay around) | +| `./infra/local/dev.sh status` | `docker compose ps`, with every profile visible | +| `./infra/local/dev.sh logs otel-collector` | Follow logs | +| `./infra/local/dev.sh exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"` | Run a command inside a service | + +Anything not matching one of the named verbs is passed through to `docker compose -f dev.compose.yml ...` (with every profile flagged in), so you keep the full Compose surface available — `./dev.sh config`, `./dev.sh top`, `./dev.sh inspect …`, etc. + +If you prefer to call `docker compose` directly, every example below shows the raw command alongside the script form. + ### Service endpoints (defaults) | Service | Host port | Purpose | @@ -172,17 +197,7 @@ All ports are overridable via `.env` if the host machine has conflicts. ### Operational tips - **Persistence** — state lives in named Docker volumes (`apf-portal-postgres-data`, `apf-portal-redis-data`). Survives `docker compose down`. Use `docker compose -f dev.compose.yml down -v` to wipe (also wipes the audit-roles bootstrap, which re-runs on the next fresh boot). -- **Profiles must match on `down` as on `up`** — `docker compose down` only acts on services whose profile is currently active. If you brought the stack up with `--profile dbtools --profile observability`, those same flags are required on `down` to also stop pgweb and Jaeger; otherwise they keep running until the next reboot. Two pragmatic patterns: - - ```bash - # Either pass the same flags on each command: - docker compose -f dev.compose.yml --profile dbtools --profile observability down -v - - # Or set COMPOSE_PROFILES once (e.g. in your shell or in infra/local/.env) - # so every `up` / `down` / `ps` sees the right scope: - export COMPOSE_PROFILES=dbtools,observability - docker compose -f dev.compose.yml down -v - ``` +- **Profile symmetry** — `dev.sh down` (and `status`, `logs`, …) always include every profile in scope, so profile-gated services are caught. If you bypass the script and call `docker compose down` directly, you must pass the same `--profile` flags as on `up`, otherwise pgweb and Jaeger keep running silently. Either pass them again, or `export COMPOSE_PROFILES=dbtools,observability` in your shell or `infra/local/.env`. - **Bootstrap re-run** — the SQL in `local/init/postgres/` only runs on a **fresh** Postgres data volume. To replay after editing the file, `down -v` (loses all dev data) or run the SQL manually with `docker compose exec postgres psql -U portal -d portal_dev -f /docker-entrypoint-initdb.d/01-init.sql`. - **Logs** — `docker compose -f dev.compose.yml logs -f ` to follow a single service. `otel-collector` is the loudest — its `debug` exporter prints every span / metric / log it receives. diff --git a/infra/local/dev.sh b/infra/local/dev.sh new file mode 100755 index 0000000..d66bedb --- /dev/null +++ b/infra/local/dev.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# Convenience wrapper around infra/local/dev.compose.yml. Documented +# in infra/README.md → "Local-dev stack" → "Convenience script". +# +# Hides the Compose-profile gotcha: `docker compose down` only acts +# on services whose profile is currently active, so anything brought +# up with `--profile X` keeps running unless the same flag is on +# `down`. Every teardown / status / log command in this script +# always includes every profile in scope, so profile-gated services +# are visible and stoppable. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="${SCRIPT_DIR}/dev.compose.yml" + +# Profiles defined in dev.compose.yml. Keep in sync if a new profile +# is added. +ALL_PROFILES=(dbtools observability) + +# Build "--profile p1 --profile p2 …" as separate arguments. +build_all_profile_flags() { + local p + ALL_PROFILE_FLAGS=() + for p in "${ALL_PROFILES[@]}"; do + ALL_PROFILE_FLAGS+=(--profile "$p") + done +} + +# Run docker compose with every profile flagged in. +dc_all() { + build_all_profile_flags + docker compose -f "$COMPOSE_FILE" "${ALL_PROFILE_FLAGS[@]}" "$@" +} + +# Run docker compose with no profiles (core services only). +dc_core() { + docker compose -f "$COMPOSE_FILE" "$@" +} + +# Run `up -d` with a custom set of profiles. +up_with_profiles() { + local p + local -a flags=() + for p in "$@"; do + flags+=(--profile "$p") + done + docker compose -f "$COMPOSE_FILE" "${flags[@]}" up -d +} + +usage() { + cat <<'USAGE' +Usage: ./infra/local/dev.sh [args] + +Commands: + up [target...] Bring up the stack. + Targets: + (none) core only (postgres + redis + otel-collector) + all core + all profiles + dbtools core + pgweb + observability core + jaeger + Multiple targets allowed (e.g. `up dbtools observability`). + + down [-v] Tear the stack down. Always runs with every + profile in scope, so profile-gated services + (pgweb, jaeger) are caught too. -v also wipes + the named Docker volumes. + + stop Stop a single service (containers stay around). + Use `up ` (or restart) to bring it back. + + restart Restart a single service. + + status docker compose ps, with every profile in scope. + + logs [service] Follow logs (one service or all of them). + + exec Run a command inside a running service container. + + help Show this help. + + [args...] Anything else is passed through to + `docker compose -f dev.compose.yml ...` with + every profile in scope. + +Examples: + ./infra/local/dev.sh up + ./infra/local/dev.sh up all + ./infra/local/dev.sh up observability + ./infra/local/dev.sh down -v + ./infra/local/dev.sh stop pgweb + ./infra/local/dev.sh logs otel-collector + ./infra/local/dev.sh exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" +USAGE +} + +require_args() { + local verb="$1" + local n="$2" + local hint="$3" + if [[ $# -lt $((3 + n)) ]]; then + echo "Error: '$verb' requires $hint." >&2 + echo "Try: ./infra/local/dev.sh help" >&2 + exit 64 + fi +} + +cmd="${1:-help}" +[[ $# -gt 0 ]] && shift + +case "$cmd" in + up | start) + if [[ $# -eq 0 ]]; then + dc_core up -d + elif [[ "$1" == "all" ]]; then + dc_all up -d + else + up_with_profiles "$@" + fi + ;; + down) + dc_all down "$@" + ;; + stop) + require_args stop 1 "a service name (use 'down' to tear down the whole stack)" "$@" + dc_all stop "$@" + ;; + restart) + require_args restart 1 "a service name" "$@" + dc_all restart "$@" + ;; + status | ps) + dc_all ps "$@" + ;; + logs) + dc_all logs -f "$@" + ;; + exec) + require_args exec 2 "a service name and a command" "$@" + dc_all exec "$@" + ;; + help | -h | --help | "") + usage + ;; + *) + # Pass-through to docker compose, with profiles in scope. + dc_all "$cmd" "$@" + ;; +esac