Stop the slugifier from deleting the letter D-stroke
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"""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())
|
||||
@@ -21,7 +21,17 @@ import urllib.error
|
||||
import urllib.request
|
||||
|
||||
APP_NAME = "medical-chatbot-app"
|
||||
IMAGES = ("vsf-duocthu-ai-service", "vsf-duocthu-web")
|
||||
IMAGES = (
|
||||
"vsf-duocthu-ai-service",
|
||||
"vsf-duocthu-web",
|
||||
# Added 2026-08-19. These were excluded because this script fails the
|
||||
# whole run if a tag it expects to rewrite is not already inline on the
|
||||
# Application, and their image blocks were not there yet. They are now,
|
||||
# so leaving these out would instead pin auth-service and api-gateway
|
||||
# at whatever SHA they were first deployed with, silently, forever.
|
||||
"vsf-duocthu-auth-service",
|
||||
"vsf-duocthu-api-gateway",
|
||||
)
|
||||
|
||||
|
||||
def call(base: str, method: str, path: str, token: str | None = None, body=None):
|
||||
|
||||
Reference in New Issue
Block a user