Staging environment + FleetOps split #17
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
|
||||
# thread so the async event loop never blocks on the ~9s REFRESH.
|
||||
_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.
|
||||
# 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
|
||||
|
|
@ -82,10 +88,11 @@ def _refresh_v_trips_once() -> str:
|
|||
|
||||
Uses a dedicated autocommit connection: REFRESH ... CONCURRENTLY cannot run
|
||||
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
|
||||
even though reporting_refresher owns it.
|
||||
do). Connects via _REFRESH_DB_URL (REFRESH_DATABASE_URL or DATABASE_URL) — a
|
||||
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:
|
||||
conn.autocommit = True
|
||||
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