[object Object]

← back to Dw Pairs Well

Add idempotent fleet-wide fix-all-stale dw_admin password repair script (dry-run default, tests candidate pw before any write/restart)

ffc4b860098d365b3d87509ae1893a3101f2b794 · 2026-06-15 07:57:56 -0700 · Steve Abrams

Files touched

Diff

commit ffc4b860098d365b3d87509ae1893a3101f2b794
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 15 07:57:56 2026 -0700

    Add idempotent fleet-wide fix-all-stale dw_admin password repair script (dry-run default, tests candidate pw before any write/restart)
---
 scripts/fix-all-stale-dw-admin.sh | 152 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 152 insertions(+)

diff --git a/scripts/fix-all-stale-dw-admin.sh b/scripts/fix-all-stale-dw-admin.sh
new file mode 100755
index 0000000..c946315
--- /dev/null
+++ b/scripts/fix-all-stale-dw-admin.sh
@@ -0,0 +1,152 @@
+#!/usr/bin/env bash
+# fix-all-stale-dw-admin.sh — idempotent fleet-wide repair of the stale dw_admin
+# Postgres password across pm2 services on Kamatera.
+#
+# Root problem (2026-06): the dw_admin password was rotated, but many service
+# .env files still carry the OLD password, so every DB query fails with
+# `password authentication failed for user "dw_admin"`. (First seen breaking
+# the Pairs-Well coordinates widget; same rot logged in memory
+# dw-unified-backup-broken.md — nightly pg_dump 0-byte since 2026-06-13.)
+#
+# This script: for every pm2 process whose .env DATABASE_URL uses dw_admin,
+#   1. test the CURRENT url with `SELECT 1` — if it AUTHENTICATES, skip (idempotent);
+#   2. else build a candidate url with the CANONICAL password from secrets-manager
+#      and test THAT with `SELECT 1`;
+#   3. ONLY if the candidate authenticates: back up .env, swap *only* the password
+#      segment, `pm2 restart NAME --update-env`, then re-verify (psql + healthz).
+#   4. if neither the current nor the candidate authenticates, report STILL-FAILING
+#      and touch nothing (it's not a stale-password problem — host down, db gone,
+#      or canon itself wrong).
+#
+# DRY-RUN by default. Pass --apply to actually write + restart.
+# The password is NEVER echoed; it only ever lives in process env vars.
+#
+# Usage (run ON Kamatera):
+#   bash fix-all-stale-dw-admin.sh            # dry-run: show what's stale + the plan
+#   bash fix-all-stale-dw-admin.sh --apply    # repair every stale service, one at a time
+
+set -u
+
+APPLY=0
+[ "${1:-}" = "--apply" ] && APPLY=1
+
+SECRETS_ENV="${SECRETS_ENV:-/root/Projects/secrets-manager/.env}"
+# Fall back to the Mac2 path if the Kamatera one isn't present.
+[ -f "$SECRETS_ENV" ] || SECRETS_ENV="$HOME/Projects/secrets-manager/.env"
+
+red()  { printf '\033[31m%s\033[0m\n' "$*"; }
+grn()  { printf '\033[32m%s\033[0m\n' "$*"; }
+ylw()  { printf '\033[33m%s\033[0m\n' "$*"; }
+mask() { sed -E 's#(://[^:]+:)[^@]+(@)#\1***\2#g'; }
+
+command -v jq   >/dev/null 2>&1 || { red "jq not found"; exit 1; }
+command -v psql >/dev/null 2>&1 || { red "psql not found"; exit 1; }
+command -v pm2  >/dev/null 2>&1 || { red "pm2 not found"; exit 1; }
+
+CANON="$(grep -m1 '^DW_ADMIN_PG_PASSWORD=' "$SECRETS_ENV" 2>/dev/null | cut -d= -f2-)"
+if [ -z "${CANON:-}" ]; then
+  red "FATAL: DW_ADMIN_PG_PASSWORD not found in $SECRETS_ENV — cannot proceed."
+  exit 1
+fi
+export CANON
+
+if [ "$APPLY" = "1" ]; then ylw "=== MODE: APPLY (will edit .env + pm2 restart stale services) ==="
+else                        grn  "=== MODE: DRY-RUN (no writes) — pass --apply to repair ==="; fi
+echo "canonical pw source: $SECRETS_ENV  (loaded, value hidden)"
+echo
+
+# Build "<name>\t<cwd>" for every pm2 process (one per line).
+mapfile -t ROWS < <(pm2 jlist 2>/dev/null | jq -r '.[] | [.name, .pm2_env.pm_cwd] | @tsv')
+
+OK=0; FIXED=0; WOULDFIX=0; SKIP=0; FAILED=0; SEEN_ENV=""
+
+# Swap ONLY the password between "://dw_admin:" and the next "@". Password is
+# treated as a literal string (no regex interpretation) by reading it from env.
+build_url() {  # $1 = old url ; uses $CANON
+  OLDURL="$1" python3 - <<'PY'
+import os, re, sys
+url = os.environ["OLDURL"]; pw = os.environ["CANON"]
+m = re.match(r'^(.*?://dw_admin:)([^@]*)(@.*)$', url, re.S)
+if not m:
+    sys.exit(3)
+sys.stdout.write(m.group(1) + pw + m.group(3))
+PY
+}
+
+write_env() {  # $1 = envfile ; $2 = new url
+  F="$1" NEWURL="$2" python3 - <<'PY'
+import os
+f = os.environ["F"]; nl = os.environ["NEWURL"]
+out = []
+for ln in open(f).read().splitlines():
+    out.append("DATABASE_URL=" + nl if ln.startswith("DATABASE_URL=") else ln)
+open(f, "w").write("\n".join(out) + "\n")
+PY
+}
+
+healthz_of() {  # $1 = cwd -> echo HEALTH_URL from .deploy.conf if present
+  [ -f "$1/.deploy.conf" ] && grep -m1 '^HEALTH_URL=' "$1/.deploy.conf" | cut -d= -f2-
+}
+
+for row in "${ROWS[@]}"; do
+  NAME="${row%%$'\t'*}"
+  CWD="${row#*$'\t'}"
+  ENVF="$CWD/.env"
+  [ -f "$ENVF" ] || continue
+
+  URL="$(grep -m1 '^DATABASE_URL=' "$ENVF" 2>/dev/null | cut -d= -f2-)"
+  [ -n "$URL" ] || continue
+  case "$URL" in *dw_admin:*) ;; *) continue ;; esac   # only password'd dw_admin urls
+
+  # de-dupe: several pm2 apps may share one .env
+  case "$SEEN_ENV" in *"|$ENVF|"*) continue ;; esac
+  SEEN_ENV="$SEEN_ENV|$ENVF|"
+
+  # 1) current password good?  -> idempotent skip
+  if psql "$URL" -tAc 'SELECT 1' >/dev/null 2>&1; then
+    grn "OK     $NAME  ($ENVF)"; OK=$((OK+1)); continue
+  fi
+
+  # 2) build + test the canonical-password candidate
+  NEWURL="$(build_url "$URL")" || { red "SKIP   $NAME  — unparseable DATABASE_URL shape"; SKIP=$((SKIP+1)); continue; }
+  if [ "$NEWURL" = "$URL" ]; then
+    red "FAIL   $NAME  — already on canonical pw but auth still fails (host/db issue, not stale pw)"; FAILED=$((FAILED+1)); continue
+  fi
+  if ! psql "$NEWURL" -tAc 'SELECT 1' >/dev/null 2>&1; then
+    red "FAIL   $NAME  — canonical pw ALSO fails (host down / db gone / canon wrong) — leaving untouched"; FAILED=$((FAILED+1)); continue
+  fi
+
+  # 3) candidate authenticates — stale password confirmed
+  if [ "$APPLY" = "0" ]; then
+    ylw "STALE  $NAME  — WOULD fix ($ENVF) + pm2 restart"; WOULDFIX=$((WOULDFIX+1)); continue
+  fi
+
+  BAK="$ENVF.bak.$(date +%Y%m%d-%H%M%S)"
+  cp "$ENVF" "$BAK"
+  write_env "$ENVF" "$NEWURL"
+  # verify the write took and now authenticates before bouncing the process
+  CHK="$(grep -m1 '^DATABASE_URL=' "$ENVF" | cut -d= -f2-)"
+  if ! psql "$CHK" -tAc 'SELECT 1' >/dev/null 2>&1; then
+    red "ERROR  $NAME  — post-write psql failed; rolling back"; cp "$BAK" "$ENVF"; FAILED=$((FAILED+1)); continue
+  fi
+  pm2 restart "$NAME" --update-env >/dev/null 2>&1
+  sleep 2
+  HU="$(healthz_of "$CWD")"
+  if [ -n "$HU" ]; then
+    CODE="$(curl -s -o /dev/null -m 15 -w '%{http_code}' "$HU")"
+    if [ "$CODE" = "200" ]; then grn "FIXED  $NAME  — restarted, healthz 200 ($BAK)"; FIXED=$((FIXED+1))
+    else ylw "FIXED? $NAME  — restarted + db OK, but healthz=$CODE ($HU) — check app log"; FIXED=$((FIXED+1)); fi
+  else
+    grn "FIXED  $NAME  — restarted, db auth OK (no HEALTH_URL to probe) ($BAK)"; FIXED=$((FIXED+1))
+  fi
+done
+
+echo
+echo "──────── summary ────────"
+echo "already OK : $OK"
+[ "$APPLY" = "0" ] && echo "stale      : $WOULDFIX  (run with --apply to fix)" || echo "fixed      : $FIXED"
+echo "failed     : $FAILED  (host/db/canon issue — NOT touched)"
+echo "skipped    : $SKIP  (unparseable url shape)"
+[ "$FAILED" -gt 0 ] && ylw "Note: 'failed' services have a non-password problem — investigate before any further action."
+[ "$APPLY" = "0" ] && [ "$WOULDFIX" -gt 0 ] && echo "Next: re-run with  --apply  to repair the $WOULDFIX stale service(s), one at a time."
+exit 0

← 6d446b9 Add per-site favicon (kills /favicon.ico 404)  ·  back to Dw Pairs Well  ·  Add /api/similar (visually-similar designs) + similar-design 5e32b37 →