feat(deploy): 환경별 오버레이(requires/workloads override) — 0.24.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -220,6 +220,48 @@ def bind(dep_id: str | None, alias: str, service_id: str | None, source: str) ->
|
||||
log(f"bind '{alias}' → '{source}' 완료")
|
||||
|
||||
|
||||
# ── 환경 오버레이: base(requires/workloads) + environments.<env> 이름 매칭 병합 ─────────────────
|
||||
# 앱 binds 는 논리이름 고정 → 환경별로 소스 정체(type/mode/plan)·워크로드 필드만 덮어쓴다. promote 무편집.
|
||||
# ※ dev.py 의 동일 함수와 규약 일치(파리티) — 한쪽 바꾸면 다른 쪽도.
|
||||
def _merge_env_vars(base, ov):
|
||||
def norm(e):
|
||||
if isinstance(e, dict):
|
||||
return [{"key": k, "value": v} for k, v in e.items()]
|
||||
return [dict(x) for x in (e or [])]
|
||||
out = {x["key"]: x for x in norm(base)}
|
||||
for x in norm(ov):
|
||||
out[x["key"]] = x
|
||||
return list(out.values())
|
||||
|
||||
|
||||
def resolve_env(m: dict, env: str) -> tuple[list, list]:
|
||||
ec = (m.get("environments") or {}).get(env) or {}
|
||||
by: dict = {}
|
||||
order: list = []
|
||||
for r in (m.get("requires") or []):
|
||||
by[r["name"]] = dict(r); order.append(r["name"])
|
||||
for r in (ec.get("requires") or []):
|
||||
if r["name"] in by:
|
||||
by[r["name"]].update(r) # 필드 병합(type/mode/plan 덮어쓰기)
|
||||
else:
|
||||
by[r["name"]] = dict(r); order.append(r["name"])
|
||||
requires = [by[n] for n in order]
|
||||
wov = ec.get("workloads") or {} # {워크로드명: 패치}
|
||||
workloads = []
|
||||
for w in (m.get("workloads") or []):
|
||||
w2 = dict(w)
|
||||
ov = wov.get(w["name"]) or {}
|
||||
for k, v in ov.items():
|
||||
if k == "env":
|
||||
w2["env"] = _merge_env_vars(w.get("env"), v)
|
||||
elif k in ("resources", "expose") and isinstance(v, dict):
|
||||
merged = dict(w.get(k) or {}); merged.update(v); w2[k] = merged
|
||||
else:
|
||||
w2[k] = v # replicas/image/port/binds 등 교체
|
||||
workloads.append(w2)
|
||||
return requires, workloads
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global CLUSTER, CLUSTER_HOST, ENV_DOMAINS
|
||||
path = next((a for a in sys.argv[1:] if not a.startswith("--")), "yakcloud.yaml")
|
||||
@ -238,10 +280,11 @@ def main() -> None:
|
||||
CLUSTER_HOST = (unwrap(api("GET", f"/clusters/{CLUSTER}")) or {}).get("defaultHostname") or ""
|
||||
dom = f" +도메인 {ENV_DOMAINS}" if ENV_DOMAINS else ""
|
||||
log(f"[{ENV}] project '{m.get('project')}' → cluster '{cname}' ({CLUSTER}){dom} (tag {TAG}){' [DRY-RUN]' if DRY else ''}")
|
||||
requires, workloads = resolve_env(m, ENV) # base + environments.<ENV> 오버레이 병합
|
||||
src_ids: dict[str, str | None] = {}
|
||||
for req in m.get("requires", []):
|
||||
for req in requires:
|
||||
src_ids[req["name"]] = reconcile_source(req)
|
||||
for w in m.get("workloads", []):
|
||||
for w in workloads:
|
||||
dep_id, created = deploy_workload(w)
|
||||
if DRY or created: # 기존 배포는 PATCH(롤링)로 바인딩 유지 → 신규일 때만 바인딩
|
||||
for b in w.get("binds", []):
|
||||
|
||||
@ -629,12 +629,51 @@ def _ensure_agent() -> None:
|
||||
log(f"릴레이 에이전트 기동(pid {p.pid}) — 콘솔에서 노드·데이터·앱 라이브(자격은 노트북 밖으로 안 나감)")
|
||||
|
||||
|
||||
# ── 환경 오버레이: base(requires/workloads) + environments.<env> 이름 매칭 병합 ─────────────────
|
||||
# 앱 binds 는 논리이름 고정 → 환경별로 소스 정체(type/mode/plan)·워크로드 필드만. 로컬은 항상 env="dev".
|
||||
# ※ deploy.py 의 동일 함수와 규약 일치(파리티).
|
||||
def _merge_env_vars(base, ov):
|
||||
def norm(e):
|
||||
if isinstance(e, dict):
|
||||
return [{"key": k, "value": v} for k, v in e.items()]
|
||||
return [dict(x) for x in (e or [])]
|
||||
out = {x["key"]: x for x in norm(base)}
|
||||
for x in norm(ov):
|
||||
out[x["key"]] = x
|
||||
return list(out.values())
|
||||
|
||||
|
||||
def resolve_env(m: dict, env: str):
|
||||
ec = (m.get("environments") or {}).get(env) or {}
|
||||
by, order = {}, []
|
||||
for r in (m.get("requires") or []):
|
||||
by[r["name"]] = dict(r); order.append(r["name"])
|
||||
for r in (ec.get("requires") or []):
|
||||
if r["name"] in by:
|
||||
by[r["name"]].update(r)
|
||||
else:
|
||||
by[r["name"]] = dict(r); order.append(r["name"])
|
||||
requires = [by[n] for n in order]
|
||||
wov = ec.get("workloads") or {}
|
||||
workloads = []
|
||||
for w in (m.get("workloads") or []):
|
||||
w2 = dict(w)
|
||||
for k, v in (wov.get(w["name"]) or {}).items():
|
||||
if k == "env":
|
||||
w2["env"] = _merge_env_vars(w.get("env"), v)
|
||||
elif k in ("resources", "expose") and isinstance(v, dict):
|
||||
merged = dict(w.get(k) or {}); merged.update(v); w2[k] = merged
|
||||
else:
|
||||
w2[k] = v
|
||||
workloads.append(w2)
|
||||
return requires, workloads
|
||||
|
||||
|
||||
def cmd_deploy(m: dict) -> None:
|
||||
context_ok()
|
||||
project = sanitize(m.get("project", "app"))
|
||||
ns = project
|
||||
reqs = m.get("requires", []) or []
|
||||
wls = m.get("workloads", []) or []
|
||||
reqs, wls = resolve_env(m, "dev") # base + environments.dev 오버레이(로컬은 항상 dev 환경)
|
||||
|
||||
# 충돌 가드 — 서로 다른 소스명/워크로드명이 같은 서비스명으로 뭉개지면 자격 오배선 → 거부.
|
||||
svc_owner: dict[str, str] = {}
|
||||
|
||||
Reference in New Issue
Block a user