96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
"""Point the k3s ArgoCD Application at an image tag, then trigger an
|
|
immediate sync.
|
|
|
|
`medical-chatbot-app` (argocd.realvuxbaro.me) is the production Application —
|
|
it serves both realvuxbaro.me and readytochat.realvuxbaro.me — since the
|
|
2026-08-17 cutover. This script does not distinguish "forward" from
|
|
"rollback": it points the Application at whatever IMAGE_TAG it is given and
|
|
syncs, so `rollback-k3s.yml` reuses it unchanged with an older tag rather than
|
|
duplicating this logic.
|
|
|
|
Required env: ARGOCD_PRACTICE_URL, ARGOCD_PRACTICE_PASSWORD, IMAGE_TAG.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
APP_NAME = "medical-chatbot-app"
|
|
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):
|
|
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}")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
raw = resp.read()
|
|
return json.loads(raw) if raw else {}
|
|
except urllib.error.HTTPError as exc:
|
|
print(f"{method} {path} -> {exc.code}: {exc.read().decode(errors='replace')}", file=sys.stderr)
|
|
raise
|
|
|
|
|
|
def main() -> int:
|
|
base = os.environ["ARGOCD_PRACTICE_URL"].rstrip("/")
|
|
password = os.environ["ARGOCD_PRACTICE_PASSWORD"]
|
|
tag = os.environ["IMAGE_TAG"]
|
|
|
|
session = call(base, "POST", "/api/v1/session", body={"username": "admin", "password": password})
|
|
token = session["token"]
|
|
|
|
app = call(base, "GET", f"/api/v1/applications/{APP_NAME}", token=token)
|
|
values = app["spec"]["source"]["helm"]["values"]
|
|
|
|
for image in IMAGES:
|
|
pattern = re.compile(
|
|
rf"(repository:\s*ghcr\.io/baovu2k4/{re.escape(image)}\s*\n\s*tag:\s*)\S+"
|
|
)
|
|
values, count = pattern.subn(rf"\g<1>{tag}", values)
|
|
if count != 1:
|
|
print(f"Expected exactly one tag: line after {image}'s repository line, found {count}", file=sys.stderr)
|
|
return 1
|
|
|
|
app["spec"]["source"]["helm"]["values"] = values
|
|
call(base, "PUT", f"/api/v1/applications/{APP_NAME}", token=token, body=app)
|
|
|
|
# selfHeal (syncPolicy.automated) reacts to the PUT above on its own —
|
|
# often before this explicit call lands, which then 400s with "another
|
|
# operation is already in progress". That race means the sync we wanted
|
|
# is already happening; only a genuinely different failure is fatal.
|
|
try:
|
|
call(base, "POST", f"/api/v1/applications/{APP_NAME}/sync", token=token, body={})
|
|
except urllib.error.HTTPError as exc:
|
|
if exc.code == 400:
|
|
print(f"Explicit sync raced with autosync (expected under selfHeal) — continuing.")
|
|
else:
|
|
raise
|
|
|
|
print(f"{APP_NAME} pointed at tag {tag}; sync in progress.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|