From 208393b185be28b6c4ec5f322a1464787eea08f0 Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Sat, 29 Aug 2026 15:17:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(relay):=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20kube=20=ED=95=B8=EB=93=A4=EB=9F=AC(pods/describe/wo?= =?UTF-8?q?rkloads)=20=E2=80=94=20=EB=A1=9C=EC=BB=AC=20=EC=95=B1=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .yakcloud/agent.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/.yakcloud/agent.py b/.yakcloud/agent.py index 0a9816a..f3feb9c 100644 --- a/.yakcloud/agent.py +++ b/.yakcloud/agent.py @@ -261,6 +261,57 @@ def _h_redis(sec: dict, action: str, params: dict) -> dict: 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: """ds-actions 액션 1건 실행 → {ok, data}|{ok:false, error:{code}} + nonce.""" nonce = a.get("nonce") @@ -269,6 +320,8 @@ def dispatch(a: dict) -> dict: service = a.get("service", "") action = a.get("action", "") params = a.get("params") or {} + if stype == "KUBE": # 앱(워크로드) 라이브 조회 — 소스 Secret 불필요 + return {"nonce": nonce, "ok": True, "data": _h_kube(action, params)} sec = _read_secret(service) if stype in ("POSTGRESQL", "MYSQL", "MARIADB"): data = _h_sql(stype, sec, action, params)