"""Turn apiGateway back on, prove chat still works, and self-revert if it does not. Enabling the gateway is the step that broke chat on 2026-08-19: web.yaml sets API_GATEWAY_URL whenever apiGateway is enabled, and every RAG route used to prefer it over AI_SERVICE_URL even though the gateway proxies `/auth/*` only. That preference is gone (PR #37), so this should now affect auth alone -- but "should" is exactly what was believed last time, so this verifies instead of assuming. Chat is the product and takes absolute priority over login. So the revert is automatic and unconditional: if chat does not answer correctly after the rollout, apiGateway goes straight back off without waiting for a human. Login returning to a broken state is an acceptable outcome; chat being down is not. Verification drives the real user path -- POST /api/chat, the same route the browser calls -- rather than pod health, which stayed green throughout the outage while every data route was dead. Required env: ARGOCD_PRACTICE_URL, ARGOCD_PRACTICE_PASSWORD. """ from __future__ import annotations import json import os import sys import time import urllib.error import urllib.request APP_NAME = "medical-chatbot-app" SITE = "https://realvuxbaro.me" ROLLOUT_WAIT = 20 ROLLOUT_ROUNDS = 12 def call(base, method, path, token=None, body=None): req = urllib.request.Request( f"{base}{path}", data=json.dumps(body).encode() if body is not None else None, method=method, headers={"Content-Type": "application/json"}, ) if token: req.add_header("Authorization", f"Bearer {token}") with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read() return json.loads(raw) if raw else {} def set_api_gateway(values: str, enabled: bool) -> str: """Flip `enabled` under the apiGateway key, and nowhere else.""" want_from = "enabled: false" if enabled else "enabled: true" want_to = "enabled: true" if enabled else "enabled: false" out, in_gateway, changed = [], False, 0 for line in values.splitlines(): stripped = line.strip() if line and not line[0].isspace(): in_gateway = stripped == "apiGateway:" elif in_gateway and stripped == want_from: indent = line[: len(line) - len(line.lstrip())] line = f"{indent}{want_to}" changed += 1 out.append(line) if changed != 1: raise SystemExit(f"Expected exactly one `{want_from}` under apiGateway, changed {changed}.") return "\n".join(out) + "\n" def apply(base, token, enabled: bool) -> None: app = call(base, "GET", f"/api/v1/applications/{APP_NAME}", token=token) app["spec"]["source"]["helm"]["values"] = set_api_gateway( app["spec"]["source"]["helm"].get("values", ""), enabled ) call(base, "PUT", f"/api/v1/applications/{APP_NAME}", token=token, body=app) try: call(base, "POST", f"/api/v1/applications/{APP_NAME}/sync", token=token, body={}) except urllib.error.HTTPError as exc: if exc.code != 400: raise print(f"apiGateway.enabled -> {enabled}") def chat_works() -> bool: """Drive the real user path. True only on a genuine grounded answer.""" req = urllib.request.Request( f"{SITE}/api/chat", data=json.dumps({"content": "Chống chỉ định của Metformin là gì?"}).encode(), method="POST", headers={"Content-Type": "application/json"}, ) try: with urllib.request.urlopen(req, timeout=120) as resp: body = resp.read().decode(errors="replace") ok = resp.status == 200 and "disclaimer" in body and "gặp sự cố" not in body print(f" chat: HTTP {resp.status}, grounded={ok}") return ok except Exception as exc: print(f" chat: FAILED -- {exc}") return False def login_works() -> bool: req = urllib.request.Request( f"{SITE}/api/auth/login", data=json.dumps({"username": "demo", "password": "1"}).encode(), method="POST", headers={"Content-Type": "application/json"}, ) try: with urllib.request.urlopen(req, timeout=60) as resp: print(f" login: HTTP {resp.status}") return resp.status == 200 except Exception as exc: print(f" login: {exc}") return False def main() -> int: base = os.environ["ARGOCD_PRACTICE_URL"].rstrip("/") password = os.environ["ARGOCD_PRACTICE_PASSWORD"] token = call(base, "POST", "/api/v1/session", body={"username": "admin", "password": password})["token"] print("=== baseline (gateway off) ===") if not chat_works(): print("Chat is already broken before any change -- refusing to touch anything.", file=sys.stderr) return 1 print("=== enabling apiGateway ===") apply(base, token, True) for i in range(ROLLOUT_ROUNDS): time.sleep(ROLLOUT_WAIT) app = call(base, "GET", f"/api/v1/applications/{APP_NAME}", token=token) status = app.get("status", {}) health = status.get("health", {}).get("status") print(f"poll {i + 1}/{ROLLOUT_ROUNDS}: sync={status.get('sync', {}).get('status')} health={health}") if health == "Healthy" and i >= 2: break print("=== verifying the real user path ===") chat_ok = chat_works() login_ok = login_works() if not chat_ok: print("CHAT IS DOWN -- reverting apiGateway immediately.", file=sys.stderr) apply(base, token, False) for _ in range(6): time.sleep(ROLLOUT_WAIT) if chat_works(): print("Chat restored. apiGateway left OFF.") return 1 print("Chat still down after revert -- needs a human.", file=sys.stderr) return 1 print(f"\nchat=OK login={'OK' if login_ok else 'STILL BROKEN'}; apiGateway left ON.") return 0 if __name__ == "__main__": raise SystemExit(main())