|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Auto-wire SENTRY_DSN + VITE_SENTRY_DSN from GlitchTip into compose/.env. |
| 3 | +# |
| 4 | +# What it does: |
| 5 | +# 1. Waits for the `glitchtip-web` container to be healthy. |
| 6 | +# 2. Pulls the DSN for the auto-created `API` and `Frontend` projects |
| 7 | +# via `manage.py shell` (no API auth dance — talks to Django direct). |
| 8 | +# 3. Writes / updates SENTRY_DSN and VITE_SENTRY_DSN in compose/.env. |
| 9 | +# 4. Restarts api-dev + ui-dev so they pick up the new env on next request. |
| 10 | +# |
| 11 | +# Idempotent: skips writing if the .env values already match the live DSNs. |
| 12 | +# Safe to invoke from dev.sh on every `up` — does nothing on subsequent |
| 13 | +# runs once .env is wired. |
| 14 | +# |
| 15 | +# Usage: |
| 16 | +# ./scripts/glitchtip-fetch-dsn.sh # foreground, verbose |
| 17 | +# ./scripts/glitchtip-fetch-dsn.sh --quiet # background-friendly, less output |
| 18 | +# |
| 19 | +# DSNs are public keys (they ship in the browser bundle), not secrets — safe |
| 20 | +# to write to compose/.env even if that file is committed. |
| 21 | + |
| 22 | +set -euo pipefail |
| 23 | + |
| 24 | +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 25 | +COMPOSE_DIR="$ROOT/compose" |
| 26 | +ENV_FILE="$COMPOSE_DIR/.env" |
| 27 | + |
| 28 | +QUIET=0 |
| 29 | +if [[ "${1:-}" == "--quiet" ]]; then |
| 30 | + QUIET=1 |
| 31 | +fi |
| 32 | + |
| 33 | +log() { |
| 34 | + if [[ "$QUIET" == "0" ]]; then |
| 35 | + echo "[glitchtip-dsn] $*" |
| 36 | + fi |
| 37 | +} |
| 38 | + |
| 39 | +warn() { |
| 40 | + echo "[glitchtip-dsn] $*" >&2 |
| 41 | +} |
| 42 | + |
| 43 | +cd "$COMPOSE_DIR" |
| 44 | + |
| 45 | +# Project name is fixed via docker-compose.yml's top-level `name:` directive. |
| 46 | +# Don't hard-code that string — `docker compose config` is the source of |
| 47 | +# truth (also handles COMPOSE_PROJECT_NAME overrides). Falls back to the |
| 48 | +# directory name if config is unparseable for some reason. |
| 49 | +PROJECT_NAME="$(docker compose config --format json 2>/dev/null \ |
| 50 | + | grep -o '"name":[[:space:]]*"[^"]*"' | head -n1 \ |
| 51 | + | sed 's/.*"name":[[:space:]]*"\([^"]*\)".*/\1/' \ |
| 52 | + || true)" |
| 53 | +PROJECT_NAME="${PROJECT_NAME:-$(basename "$COMPOSE_DIR")}" |
| 54 | + |
| 55 | +# Resolve glitchtip-web by Docker compose labels rather than `docker compose |
| 56 | +# ps -q`, because `ps` validates the service against the *loaded* compose |
| 57 | +# files and the base docker-compose.yml doesn't define glitchtip-web — the |
| 58 | +# overlay does. Label lookup is project-scoped and overlay-agnostic. |
| 59 | +WEB_CONTAINER="$(docker ps -q \ |
| 60 | + --filter "label=com.docker.compose.project=${PROJECT_NAME}" \ |
| 61 | + --filter "label=com.docker.compose.service=glitchtip-web" \ |
| 62 | + | head -n1)" |
| 63 | + |
| 64 | +if [[ -z "$WEB_CONTAINER" ]]; then |
| 65 | + log "glitchtip-web is not running — skipping auto-wire." |
| 66 | + exit 0 |
| 67 | +fi |
| 68 | + |
| 69 | +# GlitchTip Celery/Django boot takes ~30–60s on a cold start. Poll the |
| 70 | +# container's `manage.py check --database default` until it succeeds — that |
| 71 | +# proves migrations have applied and the ORM is reachable. |
| 72 | +log "Waiting for glitchtip-web to be ready..." |
| 73 | +DEADLINE=$(( $(date +%s) + 180 )) |
| 74 | +while (( $(date +%s) < DEADLINE )); do |
| 75 | + if docker exec "$WEB_CONTAINER" ./manage.py check --database default >/dev/null 2>&1; then |
| 76 | + break |
| 77 | + fi |
| 78 | + sleep 3 |
| 79 | +done |
| 80 | + |
| 81 | +if ! docker exec "$WEB_CONTAINER" ./manage.py check --database default >/dev/null 2>&1; then |
| 82 | + warn "glitchtip-web didn't become ready within 180s — skipping auto-wire." |
| 83 | + warn "Re-run later with: ./scripts/glitchtip-fetch-dsn.sh" |
| 84 | + exit 0 |
| 85 | +fi |
| 86 | + |
| 87 | +# Fetch DSNs via the Django ORM. Two import paths are tried because GlitchTip |
| 88 | +# moved its `projects` app between releases. The shell script prints |
| 89 | +# `<ProjectName>=<dsn>` lines or `__missing__=<ProjectName>` if a project |
| 90 | +# doesn't exist yet (it usually does — GLITCHTIP_DEFAULT_PROJECTS creates |
| 91 | +# `API` and `Frontend` on first boot of an empty DB). |
| 92 | +log "Fetching project DSNs from GlitchTip..." |
| 93 | +PYTHON_PROBE=$(cat <<'PYEOF' |
| 94 | +import sys |
| 95 | +try: |
| 96 | + from apps.projects.models import Project |
| 97 | +except Exception: |
| 98 | + try: |
| 99 | + from glitchtip.projects.models import Project |
| 100 | + except Exception: |
| 101 | + try: |
| 102 | + from projects.models import Project |
| 103 | + except Exception as e: |
| 104 | + sys.stderr.write(f"could not import Project model: {e}\n") |
| 105 | + sys.exit(2) |
| 106 | +
|
| 107 | +wanted = ["API", "Frontend"] |
| 108 | +for name in wanted: |
| 109 | + p = Project.objects.filter(name=name).first() |
| 110 | + if not p: |
| 111 | + print(f"__missing__={name}") |
| 112 | + continue |
| 113 | + key = p.projectkey_set.first() |
| 114 | + if not key: |
| 115 | + print(f"__nokey__={name}") |
| 116 | + continue |
| 117 | + print(f"{name}={key.get_dsn()}") |
| 118 | +PYEOF |
| 119 | +) |
| 120 | + |
| 121 | +OUTPUT="$(docker exec "$WEB_CONTAINER" ./manage.py shell -c "$PYTHON_PROBE" 2>/dev/null || true)" |
| 122 | + |
| 123 | +if [[ -z "$OUTPUT" ]]; then |
| 124 | + warn "manage.py shell returned no output — model import probably failed." |
| 125 | + warn "Open an issue or run manually:" |
| 126 | + warn " docker compose exec glitchtip-web ./manage.py shell" |
| 127 | + exit 0 |
| 128 | +fi |
| 129 | + |
| 130 | +API_DSN="" |
| 131 | +UI_DSN="" |
| 132 | +while IFS= read -r line; do |
| 133 | + case "$line" in |
| 134 | + API=*) API_DSN="${line#API=}" ;; |
| 135 | + Frontend=*) UI_DSN="${line#Frontend=}" ;; |
| 136 | + __missing__=*) warn "GlitchTip project '${line#__missing__=}' not found — bootstrap may not have completed." ;; |
| 137 | + __nokey__=*) warn "GlitchTip project '${line#__nokey__=}' exists but has no key — odd, inspect manually." ;; |
| 138 | + esac |
| 139 | +done <<< "$OUTPUT" |
| 140 | + |
| 141 | +if [[ -z "$API_DSN" || -z "$UI_DSN" ]]; then |
| 142 | + warn "Could not resolve both DSNs (API=${API_DSN:-<missing>} UI=${UI_DSN:-<missing>})." |
| 143 | + warn "Re-run later once GlitchTip has finished its first-boot bootstrap." |
| 144 | + exit 0 |
| 145 | +fi |
| 146 | + |
| 147 | +# Policy: only write when the .env value is currently empty / missing. A |
| 148 | +# non-empty existing value is the operator's choice — they may have pointed |
| 149 | +# the stack at hosted Sentry or a different GlitchTip instance, and the |
| 150 | +# auto-wire shouldn't silently overwrite that. The trade-off: once a DSN is |
| 151 | +# in place, this script never touches it again, even if it has gone stale |
| 152 | +# (e.g. the GlitchTip Postgres volume was wiped). In that case clear the |
| 153 | +# line in compose/.env and re-run; the script will repopulate it. |
| 154 | +touch "$ENV_FILE" |
| 155 | +CUR_API_DSN="$(grep -E '^SENTRY_DSN=' "$ENV_FILE" | tail -n1 | sed 's/^SENTRY_DSN=//' || true)" |
| 156 | +CUR_UI_DSN="$(grep -E '^VITE_SENTRY_DSN=' "$ENV_FILE" | tail -n1 | sed 's/^VITE_SENTRY_DSN=//' || true)" |
| 157 | + |
| 158 | +WROTE_ANY=0 |
| 159 | +upsert_env_if_empty() { |
| 160 | + local key="$1" value="$2" cur="$3" file="$4" |
| 161 | + if [[ -n "$cur" ]]; then |
| 162 | + log "$key already set — leaving as-is (operator override)." |
| 163 | + return |
| 164 | + fi |
| 165 | + if grep -qE "^${key}=" "$file"; then |
| 166 | + # Key is present but empty — replace the empty value in-place. |
| 167 | + awk -v k="$key" -v v="$value" 'BEGIN{FS=OFS="="} $1==k {$0=k"="v} {print}' "$file" > "${file}.tmp" \ |
| 168 | + && mv "${file}.tmp" "$file" |
| 169 | + else |
| 170 | + printf '\n%s=%s\n' "$key" "$value" >> "$file" |
| 171 | + fi |
| 172 | + WROTE_ANY=1 |
| 173 | +} |
| 174 | + |
| 175 | +log "Wiring DSNs in ${ENV_FILE#"$ROOT"/}" |
| 176 | +upsert_env_if_empty SENTRY_DSN "$API_DSN" "$CUR_API_DSN" "$ENV_FILE" |
| 177 | +upsert_env_if_empty VITE_SENTRY_DSN "$UI_DSN" "$CUR_UI_DSN" "$ENV_FILE" |
| 178 | + |
| 179 | +if [[ "$WROTE_ANY" == "0" ]]; then |
| 180 | + log "Both DSNs already set — nothing to do." |
| 181 | + exit 0 |
| 182 | +fi |
| 183 | + |
| 184 | +# Recreate api-dev + ui-dev so they pick up the new env interpolation. |
| 185 | +# `docker compose restart` is NOT enough: it stops + starts the existing |
| 186 | +# container with the env block that was interpolated from .env when the |
| 187 | +# container was first `up`-ed. Only `up -d --force-recreate` re-resolves |
| 188 | +# the env at recreate time. |
| 189 | +# |
| 190 | +# Both services live in the base docker-compose.yml (profiles [dev] / |
| 191 | +# [dev, smoke]), so no overlay is needed — `--profile dev` is enough. |
| 192 | +RECREATABLE=() |
| 193 | +for svc in api-dev ui-dev; do |
| 194 | + cid="$(docker ps -q \ |
| 195 | + --filter "label=com.docker.compose.project=${PROJECT_NAME}" \ |
| 196 | + --filter "label=com.docker.compose.service=${svc}" \ |
| 197 | + | head -n1)" |
| 198 | + if [[ -n "$cid" ]]; then |
| 199 | + RECREATABLE+=("$svc") |
| 200 | + fi |
| 201 | +done |
| 202 | + |
| 203 | +if (( ${#RECREATABLE[@]} > 0 )); then |
| 204 | + log "Recreating ${RECREATABLE[*]} to pick up the new DSNs..." |
| 205 | + if docker compose --profile dev up -d --no-deps --force-recreate "${RECREATABLE[@]}" >/dev/null 2>&1; then |
| 206 | + log "Recreated: ${RECREATABLE[*]}" |
| 207 | + else |
| 208 | + warn "Recreate failed — run manually:" |
| 209 | + warn " docker compose --profile dev up -d --no-deps --force-recreate ${RECREATABLE[*]}" |
| 210 | + fi |
| 211 | +else |
| 212 | + log "(api-dev / ui-dev not running — they'll read the new env on next up)" |
| 213 | +fi |
| 214 | + |
| 215 | +log "Done. Sentry / GlitchTip wiring is live." |
0 commit comments