163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
"""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())
|