feat(db): split refresher onto REFRESH_DATABASE_URL; prod reads via dashboard_ro
Enables stage-2: the prod dashboard_api request pool connects as the READ-ONLY dashboard_ro role (DATABASE_URL) while the v_trips refresher keeps a privileged connection via REFRESH_DATABASE_URL (falls back to DATABASE_URL when unset, so single-role/staging deploys are unchanged). Avoids the FIX-D02 trap (a read-only role cannot REFRESH). Adds deploy_dashboard_api.sh (the prod bridge deploy, now version-controlled): strips inherited DATABASE_URL, sets REFRESH_DATABASE_URL=<app role> + DATABASE_URL=dashboard_ro, CORS incl. fleetops.rahamafresh.com. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
54a714e0cd
commit
aea226cc74
2 changed files with 80 additions and 3 deletions
|
|
@ -69,6 +69,12 @@ _ALLOWED_ORIGINS = [
|
||||||
# uvicorn workers (only one worker refreshes per tick); the work runs in a
|
# uvicorn workers (only one worker refreshes per tick); the work runs in a
|
||||||
# thread so the async event loop never blocks on the ~9s REFRESH.
|
# thread so the async event loop never blocks on the ~9s REFRESH.
|
||||||
_DATABASE_URL = os.environ["DATABASE_URL"]
|
_DATABASE_URL = os.environ["DATABASE_URL"]
|
||||||
|
# The request pool (get_conn / DATABASE_URL) can be a READ-ONLY role
|
||||||
|
# (dashboard_ro) — least privilege for serving. The v_trips refresher needs write
|
||||||
|
# perms (it owns the REFRESH), so it connects via a SEPARATE privileged URL:
|
||||||
|
# REFRESH_DATABASE_URL if set, else DATABASE_URL (single-role / legacy deploys).
|
||||||
|
# So a prod bridge runs DATABASE_URL=dashboard_ro + REFRESH_DATABASE_URL=<app role>.
|
||||||
|
_REFRESH_DB_URL = os.getenv("REFRESH_DATABASE_URL") or _DATABASE_URL
|
||||||
# VTRIPS_REFRESH_INTERVAL_S <= 0 disables the in-process refresher entirely.
|
# VTRIPS_REFRESH_INTERVAL_S <= 0 disables the in-process refresher entirely.
|
||||||
# Staging sets it to 0: it connects read-only and prod owns the refresh, so a
|
# Staging sets it to 0: it connects read-only and prod owns the refresh, so a
|
||||||
# staging instance must never attempt REFRESH (it would only log permission
|
# staging instance must never attempt REFRESH (it would only log permission
|
||||||
|
|
@ -82,10 +88,11 @@ def _refresh_v_trips_once() -> str:
|
||||||
|
|
||||||
Uses a dedicated autocommit connection: REFRESH ... CONCURRENTLY cannot run
|
Uses a dedicated autocommit connection: REFRESH ... CONCURRENTLY cannot run
|
||||||
inside a transaction block (so the pooled get_conn, which wraps a txn, won't
|
inside a transaction block (so the pooled get_conn, which wraps a txn, won't
|
||||||
do). DATABASE_URL connects as a superuser, which may REFRESH the matview
|
do). Connects via _REFRESH_DB_URL (REFRESH_DATABASE_URL or DATABASE_URL) — a
|
||||||
even though reporting_refresher owns it.
|
privileged role that may REFRESH the matview, distinct from a read-only
|
||||||
|
request pool.
|
||||||
"""
|
"""
|
||||||
conn = psycopg2.connect(_DATABASE_URL, connect_timeout=10)
|
conn = psycopg2.connect(_REFRESH_DB_URL, connect_timeout=10)
|
||||||
try:
|
try:
|
||||||
conn.autocommit = True
|
conn.autocommit = True
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
|
|
|
||||||
70
deploy_dashboard_api.sh
Executable file
70
deploy_dashboard_api.sh
Executable file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy_dashboard_api.sh — PROD dashboard_api bridge (fleetapi.rahamafresh.com).
|
||||||
|
# Standalone Traefik-labelled bridge (NOT Coolify-managed): reuses the
|
||||||
|
# webhook_receiver image + app network, bind-mounts the WIP API file. An env/CORS
|
||||||
|
# change needs a container RECREATE (this script does that).
|
||||||
|
#
|
||||||
|
# Stage-2 least privilege: the request pool connects as the READ-ONLY dashboard_ro
|
||||||
|
# role (DATABASE_URL), while the v_trips refresher keeps the privileged app role
|
||||||
|
# (REFRESH_DATABASE_URL) — REFRESH needs write perms that dashboard_ro lacks.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WH=$(docker ps --filter name=webhook_receiver --format "{{.Names}}" | head -1)
|
||||||
|
IMG=$(docker inspect "$WH" --format "{{.Image}}")
|
||||||
|
APPNET=$(docker inspect "$WH" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}')
|
||||||
|
echo "Reusing image $IMG on network $APPNET (from $WH)"
|
||||||
|
|
||||||
|
mkdir -p /home/kianiadee/dashboard_api
|
||||||
|
# Stage a fresh copy only if one was scp'd to ~; otherwise keep the existing mount.
|
||||||
|
if [ -f /home/kianiadee/dashboard_api_rev.py ]; then
|
||||||
|
mv -f /home/kianiadee/dashboard_api_rev.py /home/kianiadee/dashboard_api/dashboard_api_rev.py
|
||||||
|
fi
|
||||||
|
test -f /home/kianiadee/dashboard_api/dashboard_api_rev.py \
|
||||||
|
|| { echo "ERROR: dashboard_api_rev.py missing in mount dir; scp it to ~ first"; exit 1; }
|
||||||
|
|
||||||
|
# Reuse the webhook container's env, stripping runtime noise AND any inherited
|
||||||
|
# DATABASE_URL + DASHBOARD_CORS_ORIGINS (both set explicitly below).
|
||||||
|
docker inspect "$WH" --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
|
| grep -vE '^(PATH=|HOSTNAME=|HOME=|PWD=|TERM=|SHLVL=|_=|LANG=|GPG_KEY=|PYTHON_VERSION=|PYTHON_PIP_VERSION=|PYTHONUNBUFFERED=|DATABASE_URL=|DASHBOARD_CORS_ORIGINS=)' \
|
||||||
|
> /home/kianiadee/dashboard_api/dapi.env
|
||||||
|
echo 'DASHBOARD_CORS_ORIGINS=https://liveposition.rahamafresh.com,https://fleetintelligence.rahamafresh.com,https://fleetnow.rahamafresh.com,https://fleetops.rahamafresh.com' \
|
||||||
|
>> /home/kianiadee/dashboard_api/dapi.env
|
||||||
|
|
||||||
|
# Split roles: REFRESH_DATABASE_URL = the inherited privileged URL (for the
|
||||||
|
# refresher); DATABASE_URL = read-only dashboard_ro (for request handling).
|
||||||
|
SRC_DB_URL=$(docker inspect "$WH" --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^DATABASE_URL=//p' | head -1)
|
||||||
|
RO_PW=$(cat /home/kianiadee/.dashboard_ro.pw 2>/dev/null || true)
|
||||||
|
[ -n "$SRC_DB_URL" ] || { echo "ERROR: DATABASE_URL not found in $WH env"; exit 1; }
|
||||||
|
[ -n "$RO_PW" ] || { echo "ERROR: ~/.dashboard_ro.pw missing — run bootstrap_dashboard_ro.sh first"; exit 1; }
|
||||||
|
HOSTPART="${SRC_DB_URL#*@}"
|
||||||
|
{
|
||||||
|
echo "REFRESH_DATABASE_URL=${SRC_DB_URL}"
|
||||||
|
echo "DATABASE_URL=postgresql://dashboard_ro:${RO_PW}@${HOSTPART}"
|
||||||
|
} >> /home/kianiadee/dashboard_api/dapi.env
|
||||||
|
chmod 600 /home/kianiadee/dashboard_api/dapi.env
|
||||||
|
|
||||||
|
docker rm -f dashboard_api 2>/dev/null || true
|
||||||
|
docker run -d --name dashboard_api --restart unless-stopped \
|
||||||
|
--network "$APPNET" \
|
||||||
|
--env-file /home/kianiadee/dashboard_api/dapi.env \
|
||||||
|
-v /home/kianiadee/dashboard_api/dashboard_api_rev.py:/app/dashboard_api_rev.py:ro \
|
||||||
|
--label 'traefik.enable=true' \
|
||||||
|
--label 'traefik.docker.network=coolify' \
|
||||||
|
--label 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https' \
|
||||||
|
--label 'traefik.http.routers.http-0-fleetapi.entryPoints=http' \
|
||||||
|
--label 'traefik.http.routers.http-0-fleetapi.middlewares=redirect-to-https' \
|
||||||
|
--label 'traefik.http.routers.http-0-fleetapi.rule=Host(`fleetapi.rahamafresh.com`)' \
|
||||||
|
--label 'traefik.http.routers.https-0-fleetapi.entryPoints=https' \
|
||||||
|
--label 'traefik.http.routers.https-0-fleetapi.rule=Host(`fleetapi.rahamafresh.com`)' \
|
||||||
|
--label 'traefik.http.routers.https-0-fleetapi.tls=true' \
|
||||||
|
--label 'traefik.http.routers.https-0-fleetapi.tls.certresolver=letsencrypt' \
|
||||||
|
--label 'traefik.http.services.fleetapi.loadbalancer.server.port=8890' \
|
||||||
|
"$IMG" sh -c 'uvicorn dashboard_api_rev:app --host 0.0.0.0 --port 8890 --workers 2'
|
||||||
|
|
||||||
|
docker network connect coolify dashboard_api 2>/dev/null || true
|
||||||
|
sleep 8
|
||||||
|
echo "== container =="; docker ps --filter name=dashboard_api --format "{{.Names}} | {{.Status}}"
|
||||||
|
echo "== CORS origins in effect =="; docker exec dashboard_api printenv DASHBOARD_CORS_ORIGINS
|
||||||
|
echo "== request role (expect dashboard_ro) =="; docker exec dashboard_api sh -lc 'printenv DATABASE_URL | sed -E "s#://([^:]+):[^@]+@#://\1:<pw>@#"'
|
||||||
|
echo "== refresh role set? =="; docker exec dashboard_api sh -lc 'printenv REFRESH_DATABASE_URL | sed -E "s#://([^:]+):[^@]+@#://\1:<pw>@#"'
|
||||||
|
echo "== internal health =="; docker exec dashboard_api sh -lc 'curl -s http://localhost:8890/health' 2>&1 | head || true
|
||||||
Loading…
Reference in a new issue