Staging environment + FleetOps split #17

Open
kianiadee wants to merge 23 commits from feat/staging-fleetops-architecture into main
Showing only changes of commit 478fb0e707 - Show all commits

95
deploy_dashboard_api_staging.sh Executable file
View file

@ -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:<pw>@#"'
echo "== internal health =="; docker exec "$NAME" sh -lc "curl -s http://localhost:${PORT}/health" 2>&1 | head