diff --git a/deploy_dashboard_api_staging.sh b/deploy_dashboard_api_staging.sh new file mode 100755 index 0000000..c411aed --- /dev/null +++ b/deploy_dashboard_api_staging.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# deploy_dashboard_api_staging.sh — STAGING twin of ~/deploy_dashboard_api.sh +# ───────────────────────────────────────────────────────────────────────────── +# Stands up a SECOND dashboard_api bridge for the staging umbrella +# (fleetapi.fivetitude.com). It mirrors the prod script but with four +# deliberate differences: +# +# 1. Container name dashboard_api_staging (prod: dashboard_api) +# 2. Port / Traefik 8891 + Host(fleetapi.fivetitude.com) (prod: 8890 + rahamafresh) +# 3. DB role READ-ONLY grafana_ro DATABASE_URL, derived on-host from the +# webhook env (prod: the app's read/write DATABASE_URL). +# 4. Refresher OFF VTRIPS_REFRESH_INTERVAL_S=0 — prod owns the v_trips refresh; +# a read-only instance must never attempt REFRESH. +# +# Staging reads the SAME production DB (over the internal Docker network) as +# grafana_ro, so it is physically incapable of writing. See +# docs/STAGING_FLEETOPS_ARCHITECTURE.md §6. +# +# Like prod, this is a STANDALONE bridge container (NOT Coolify-managed): it +# reuses the webhook_receiver image + app network, bind-mounts the WIP API file, +# and an env/CORS change needs a container RECREATE (this script does that). +# +# Deploy: +# scp dashboard_api_rev.py kianiadee@twala.rahamafresh.com:~/dashboard_api_staging_rev.py +# scp deploy_dashboard_api_staging.sh kianiadee@twala.rahamafresh.com:~/ +# ssh kianiadee@twala.rahamafresh.com 'bash ~/deploy_dashboard_api_staging.sh' +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +NAME=dashboard_api_staging +PORT=8891 +MOUNT_DIR=/home/kianiadee/dashboard_api_staging +ENV_FILE="$MOUNT_DIR/dapi.staging.env" +STAGED_SRC=/home/kianiadee/dashboard_api_staging_rev.py +CORS='https://fleetnow.fivetitude.com,https://fleetops.fivetitude.com' + +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 "$MOUNT_DIR" +# Stage a fresh copy only if one was scp'd to ~; otherwise keep the existing mount. +if [ -f "$STAGED_SRC" ]; then + mv -f "$STAGED_SRC" "$MOUNT_DIR/dashboard_api_rev.py" +fi +test -f "$MOUNT_DIR/dashboard_api_rev.py" \ + || { echo "ERROR: dashboard_api_rev.py missing in $MOUNT_DIR; scp it to ~/dashboard_api_staging_rev.py first"; exit 1; } + +# Derive a READ-ONLY DATABASE_URL on the host (never printed): take the app's +# DATABASE_URL host:port/dbname and swap the credentials for grafana_ro. +SRC_DB_URL=$(docker inspect "$WH" --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^DATABASE_URL=//p' | head -1) +RO_PW=$(docker inspect "$WH" --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^GRAFANA_DB_RO_PASSWORD=//p' | head -1) +[ -n "$SRC_DB_URL" ] || { echo "ERROR: DATABASE_URL not found in $WH env"; exit 1; } +[ -n "$RO_PW" ] || { echo "ERROR: GRAFANA_DB_RO_PASSWORD not found in $WH env"; exit 1; } +HOSTPART="${SRC_DB_URL#*@}" # host:port/dbname[?params] +RO_DB_URL="postgresql://grafana_ro:${RO_PW}@${HOSTPART}" + +# Reuse the webhook env, stripping runtime noise AND anything we override below +# (DATABASE_URL -> read-only, CORS -> staging origins, refresher -> disabled). +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=|VTRIPS_REFRESH_INTERVAL_S=)' \ + > "$ENV_FILE" +{ + echo "DATABASE_URL=${RO_DB_URL}" + echo "DASHBOARD_CORS_ORIGINS=${CORS}" + echo "VTRIPS_REFRESH_INTERVAL_S=0" +} >> "$ENV_FILE" +chmod 600 "$ENV_FILE" + +docker rm -f "$NAME" 2>/dev/null || true +docker run -d --name "$NAME" --restart unless-stopped \ + --network "$APPNET" \ + --env-file "$ENV_FILE" \ + -v "$MOUNT_DIR/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-staging.entryPoints=http' \ + --label 'traefik.http.routers.http-0-fleetapi-staging.middlewares=redirect-to-https' \ + --label 'traefik.http.routers.http-0-fleetapi-staging.rule=Host(`fleetapi.fivetitude.com`)' \ + --label 'traefik.http.routers.https-0-fleetapi-staging.entryPoints=https' \ + --label 'traefik.http.routers.https-0-fleetapi-staging.rule=Host(`fleetapi.fivetitude.com`)' \ + --label 'traefik.http.routers.https-0-fleetapi-staging.tls=true' \ + --label 'traefik.http.routers.https-0-fleetapi-staging.tls.certresolver=letsencrypt' \ + --label "traefik.http.services.fleetapi-staging.loadbalancer.server.port=${PORT}" \ + "$IMG" sh -c "uvicorn dashboard_api_rev:app --host 0.0.0.0 --port ${PORT} --workers 2" + +docker network connect coolify "$NAME" 2>/dev/null || true +sleep 5 +echo "== container =="; docker ps --filter name="$NAME" --format "{{.Names}} | {{.Status}}" +echo "== CORS origins in effect =="; docker exec "$NAME" printenv DASHBOARD_CORS_ORIGINS +echo "== refresher (expect 0 = disabled) =="; docker exec "$NAME" printenv VTRIPS_REFRESH_INTERVAL_S +echo "== DB role (expect grafana_ro) =="; docker exec "$NAME" sh -lc 'printenv DATABASE_URL | sed -E "s#://([^:]+):[^@]+@#://\1:@#"' +echo "== internal health =="; docker exec "$NAME" sh -lc "curl -s http://localhost:${PORT}/health" 2>&1 | head