feat(relay): 에이전트 kube 핸들러(pods/describe/workloads) — 로컬 앱 라이브 조회

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 15:17:36 +09:00
parent 3c3724b091
commit 208393b185

View File

@ -261,6 +261,57 @@ def _h_redis(sec: dict, action: str, params: dict) -> dict:
raise DsErr("FORBIDDEN_ACTION") raise DsErr("FORBIDDEN_ACTION")
# ── 앱(워크로드) 라이브 조회 — 로컬 kind 에 kubectl(호스트). 원격 앱 화면 파리티(P2). ───────────
def _kubectl(argv: list[str]) -> str:
r = subprocess.run(["kubectl", "--context", CTX, "-n", NS, *argv], capture_output=True, text=True)
if r.returncode != 0:
err = (r.stderr or "").lower()
if "notfound" in err.replace(" ", "") or "not found" in err:
raise DsErr("NOT_FOUND")
raise DsErr("UNREACHABLE")
return r.stdout
def _h_kube(action: str, params: dict) -> dict:
if action == "pods":
app = params.get("app") or ""
sel = ["-l", f"app={app}"] if app else []
items = json.loads(_kubectl(["get", "pods", *sel, "-o", "json"]) or "{}").get("items", [])
pods = []
for p in items:
st = p.get("status", {}) or {}
cs = st.get("containerStatuses", []) or []
pods.append({"name": p["metadata"]["name"], "phase": st.get("phase", ""),
"ready": bool(cs) and all(c.get("ready") for c in cs),
"restarts": sum(int(c.get("restartCount", 0)) for c in cs),
"node": (p.get("spec", {}) or {}).get("nodeName", ""),
"ip": st.get("podIP", ""), "startedAt": st.get("startTime", "")})
return {"app": app, "pods": pods[:100]}
if action == "describe":
pod = params["pod"]
p = json.loads(_kubectl(["get", "pod", pod, "-o", "json"]) or "{}")
st = p.get("status", {}) or {}
containers = [{"name": c.get("name", ""), "image": c.get("image", ""),
"ready": bool(c.get("ready")), "restarts": int(c.get("restartCount", 0)),
"state": next(iter((c.get("state", {}) or {}).keys()), "")}
for c in (st.get("containerStatuses", []) or [])]
conds = [{"type": c.get("type", ""), "status": c.get("status", "")} for c in (st.get("conditions", []) or [])]
return {"name": pod, "phase": st.get("phase", ""), "node": (p.get("spec", {}) or {}).get("nodeName", ""),
"ip": st.get("podIP", ""), "startedAt": st.get("startTime", ""),
"containers": containers, "conditions": conds}
if action == "workloads":
items = json.loads(_kubectl(["get", "deploy", "-o", "json"]) or "{}").get("items", [])
wls = []
for d in items:
sp = d.get("spec", {}) or {}
conts = (sp.get("template", {}).get("spec", {}) or {}).get("containers", []) or []
wls.append({"name": d["metadata"]["name"], "replicas": sp.get("replicas", 0) or 0,
"ready": (d.get("status", {}) or {}).get("readyReplicas", 0) or 0,
"image": conts[0]["image"] if conts else ""})
return {"workloads": wls}
raise DsErr("FORBIDDEN_ACTION")
def dispatch(a: dict) -> dict: def dispatch(a: dict) -> dict:
"""ds-actions 액션 1건 실행 → {ok, data}|{ok:false, error:{code}} + nonce.""" """ds-actions 액션 1건 실행 → {ok, data}|{ok:false, error:{code}} + nonce."""
nonce = a.get("nonce") nonce = a.get("nonce")
@ -269,6 +320,8 @@ def dispatch(a: dict) -> dict:
service = a.get("service", "") service = a.get("service", "")
action = a.get("action", "") action = a.get("action", "")
params = a.get("params") or {} params = a.get("params") or {}
if stype == "KUBE": # 앱(워크로드) 라이브 조회 — 소스 Secret 불필요
return {"nonce": nonce, "ok": True, "data": _h_kube(action, params)}
sec = _read_secret(service) sec = _read_secret(service)
if stype in ("POSTGRESQL", "MYSQL", "MARIADB"): if stype in ("POSTGRESQL", "MYSQL", "MARIADB"):
data = _h_sql(stype, sec, action, params) data = _h_sql(stype, sec, action, params)