"""Read-only diagnostic: print the structure of `medical-chatbot-app`'s inline `spec.source.helm.values` on the live ArgoCD Application, with any line that looks like it holds a credential redacted. Also prints per-resource health from the resource tree, so a Degraded app health can be traced to the specific Deployment/Pod causing it. Never mutates anything. Exists to let us see the real inline-values layout and live resource health before writing a script that edits the Application (see the note in infra/argocd/applications/medical-chatbot-app.yaml about what stays inline). Required env: ARGOCD_PRACTICE_URL, ARGOCD_PRACTICE_PASSWORD. """ from __future__ import annotations import json import os import re import sys import urllib.error import urllib.request APP_NAME = "medical-chatbot-app" SECRET_LINE = re.compile(r"(password|secret|token|key)", re.IGNORECASE) 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}") with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read() return json.loads(raw) if raw else {} def redact(line: str) -> str: if ":" in line and SECRET_LINE.search(line.split(":", 1)[0]): key = line.split(":", 1)[0] return f"{key}: " return line def main() -> int: base = os.environ["ARGOCD_PRACTICE_URL"].rstrip("/") password = os.environ["ARGOCD_PRACTICE_PASSWORD"] 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"].get("values", "") print(f"sync.status={app.get('status', {}).get('sync', {}).get('status')}") print(f"health.status={app.get('status', {}).get('health', {}).get('status')}") # The raw string, escaped -- a manual UI edit used a folded (`values: >`) # block instead of a literal one, which silently joins same-indent lines. # Only an escaped dump shows where the newlines really are; the pretty # print below looks almost right and hides it. print("--- spec.source.helm.values RAW (first line + newline positions) ---") raw = app["spec"]["source"]["helm"].get("values", "") for i, line in enumerate(raw.splitlines()): safe = re.sub(r"(?i)(password|secret|jwt)[^\s]*:.*", r"\1", line) print(f"{i:>3}: {safe!r}") print("--- end raw ---") print("--- spec.source.helm.values (secrets redacted) ---") for line in values.splitlines(): print(redact(line)) print("--- end values ---") print("--- status.resources (per-resource health) ---") for r in app.get("status", {}).get("resources", []): health = r.get("health", {}) print( f"{r.get('kind'):<12} {r.get('name'):<45} " f"status={r.get('status')} health={health.get('status')} " f"msg={health.get('message', '')}" ) print("--- end resources ---") print("--- status.conditions ---") for c in app.get("status", {}).get("conditions", []): print(f"{c.get('type')}: {c.get('message')}") print("--- end conditions ---") op = app.get("status", {}).get("operationState", {}) print("--- status.operationState (last sync operation) ---") print(f"phase={op.get('phase')}") print(f"message={op.get('message')}") print(f"startedAt={op.get('startedAt')} finishedAt={op.get('finishedAt')}") sync_res = op.get("syncResult") or {} for r in sync_res.get("resources", []): print( f" {r.get('kind'):<12} {r.get('name'):<45} " f"status={r.get('status')} hookPhase={r.get('hookPhase')} message={r.get('message')}" ) print("--- end operationState ---") print("--- resource tree (nodes with non-empty health/status) ---") try: tree = call(base, "GET", f"/api/v1/applications/{APP_NAME}/resource-tree", token=token) for n in tree.get("nodes", []): h = n.get("health", {}) if h.get("status") not in (None, "Healthy") or n.get("kind") == "Pod": print( f"{n.get('kind'):<12} {n.get('name'):<45} " f"health={h.get('status')} msg={h.get('message', '')}" ) except urllib.error.HTTPError as exc: print(f"resource-tree fetch failed: {exc.code} {exc.read().decode(errors='replace')}", file=sys.stderr) print("--- end tree ---") return 0 if __name__ == "__main__": raise SystemExit(main())