feat(relay): 로컬 클러스터 데이터툴 라이브 릴레이 — 콘솔에서 로컬 소스 관리(원격 파리티)
dev deploy 시 릴레이 에이전트(.yakcloud/agent.py) 기동 — 콘솔에서 ds-token 받아 프로덕션 백엔드 ds-actions 롱폴 → 로컬 kind Secret 읽고 공유 컨테이너의 격리 DB 에 접속(docker exec, 무설치) → 결과만 반환. 자격은 노트북 밖으로 안 나감. dev.py 가 소스 secret 리포트 + 데몬 라이프사이클(dev down 중지). 콘솔의 SQL/Mongo/Redis 매니저를 로컬 소스에 그대로 사용. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -25,6 +25,7 @@ import json
|
||||
import os
|
||||
import secrets
|
||||
import hashlib
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@ -373,7 +374,9 @@ def _report_to_console(project: str, reqs: list, wls: list) -> None:
|
||||
return
|
||||
report = {
|
||||
"project": project,
|
||||
"sources": [{"name": r["name"], "type": str(r.get("type", "")).upper()} for r in reqs],
|
||||
# secret = 노트북 kind 커넥션 Secret 이름 → 콘솔이 connSecretRef 로 저장, 에이전트가 이 이름으로 읽음.
|
||||
"sources": [{"name": r["name"], "type": str(r.get("type", "")).upper(),
|
||||
"secret": f"yak-dev-src-{sanitize(r['name'])}"} for r in reqs],
|
||||
"workloads": [{"name": sanitize(w["name"]), "image": w["image"].replace("${TAG}", TAG),
|
||||
"hosts": _wl_hosts(project, w), "port": int(w.get("port", 8080))} for w in wls],
|
||||
}
|
||||
@ -387,6 +390,66 @@ def _report_to_console(project: str, reqs: list, wls: list) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# ── 릴레이 에이전트 라이프사이클: 콘솔에서 로컬 소스를 원격처럼 라이브 관리(ds-actions 롱폴) ─────
|
||||
def _agent_dir() -> str:
|
||||
d = os.path.join(os.path.expanduser(os.environ.get("YAK_CONFIG_DIR", "~/.config/yakcloud")), "agent")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def _agent_pidfile(ns: str) -> str:
|
||||
return os.path.join(_agent_dir(), f"{ns}.pid")
|
||||
|
||||
|
||||
def _stop_agent(ns: str) -> None:
|
||||
pf = _agent_pidfile(ns)
|
||||
if not os.path.exists(pf):
|
||||
return
|
||||
try:
|
||||
pid = int(open(pf).read().strip())
|
||||
os.kill(pid, signal.SIGTERM) # 그룹장(start_new_session) → 자식까지
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
try:
|
||||
os.remove(pf)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _ensure_agent(project: str, ns: str, reqs: list) -> None:
|
||||
"""콘솔에서 ds-token 취득 → 릴레이 에이전트 데몬 기동(콘솔에서 로컬 소스 라이브 관리). best-effort.
|
||||
로그인(URL+TOKEN)+등록명(YAK_REG_NAME) 없거나 소스 없으면 스킵(오프라인 dev 불변식 — 스냅샷만)."""
|
||||
url = os.environ.get("YAKCLOUD_URL")
|
||||
tok = os.environ.get("YAKCLOUD_TOKEN")
|
||||
name = os.environ.get("YAK_REG_NAME")
|
||||
if not (url and tok and name and reqs):
|
||||
return
|
||||
_stop_agent(ns) # 재기동(토큰/키 갱신) — 멱등
|
||||
try:
|
||||
req = urllib.request.Request(url.rstrip("/") + f"/api/v1/clusters/local/{quote(name)}/agent-token",
|
||||
data=b"{}", method="POST",
|
||||
headers={"Authorization": "Bearer " + tok, "content-type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=8) as r:
|
||||
info = json.loads(r.read() or b"{}")
|
||||
data = info.get("data") or info
|
||||
except Exception: # noqa: BLE001
|
||||
warn("릴레이 토큰 취득 실패 — 콘솔의 로컬 소스 라이브 관리 비활성(스냅샷만).")
|
||||
return
|
||||
relay_key, ds_token, api_base = data.get("relayKey"), data.get("dsToken"), data.get("apiBase")
|
||||
if not (relay_key and ds_token and api_base):
|
||||
return
|
||||
agent_py = os.path.join(os.path.dirname(os.path.abspath(__file__)), "agent.py")
|
||||
if not os.path.exists(agent_py):
|
||||
return
|
||||
env = dict(os.environ, YAK_RELAY_KEY=relay_key, YAK_DS_TOKEN=ds_token, YAK_API_BASE=api_base,
|
||||
YAK_DEV_NS=ns, YAK_DEV_CONTEXT=CTX, YAK_DEV_RUNTIME=RUNTIME, YAK_DEV_NET=SHARED_NET)
|
||||
logf = open(os.path.join(_agent_dir(), f"{ns}.log"), "a")
|
||||
p = subprocess.Popen([sys.executable, agent_py], env=env, stdout=logf, stderr=logf,
|
||||
stdin=subprocess.DEVNULL, start_new_session=True)
|
||||
open(_agent_pidfile(ns), "w").write(str(p.pid))
|
||||
log(f"릴레이 에이전트 기동(pid {p.pid}) — 콘솔에서 로컬 소스를 원격처럼 관리(자격은 노트북 밖으로 안 나감)")
|
||||
|
||||
|
||||
def cmd_deploy(m: dict) -> None:
|
||||
context_ok()
|
||||
project = sanitize(m.get("project", "app"))
|
||||
@ -420,6 +483,7 @@ def cmd_deploy(m: dict) -> None:
|
||||
_prune(ns, "workload", "app", {sanitize(w["name"]) for w in wls})
|
||||
|
||||
_report_to_console(project, reqs, wls) # 콘솔 LOCAL 클러스터 탭(데이터소스·앱) 리포트
|
||||
_ensure_agent(project, ns, reqs) # 릴레이 에이전트 기동 — 콘솔에서 로컬 소스 라이브 관리(원격 파리티)
|
||||
|
||||
failed = [h for h, ok in results if not ok]
|
||||
print()
|
||||
@ -449,6 +513,7 @@ def cmd_down(m: dict) -> None:
|
||||
"""네임스페이스(이 프로젝트의 dev 리소스)만 정리 — kind 클러스터 자체는 'yakcloud dev down' 이 지운다."""
|
||||
context_ok()
|
||||
ns = sanitize(m.get("project", "app"))
|
||||
_stop_agent(ns) # 릴레이 에이전트 데몬 중지
|
||||
log(f"네임스페이스 '{ns}' 삭제(로컬 dev 리소스 정리)")
|
||||
sh(["kubectl", "--context", CTX, "delete", "namespace", ns, "--ignore-not-found"], capture=False, check=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user