cli v0.12.1: 배포 이름 충돌 가드 — 같은 이름·다른 이미지 repo 면 덮어쓰기 거부(fail-fast). 백엔드 create_deployment 도 409 로 강제

This commit is contained in:
2026-08-28 05:39:59 +09:00
parent d85e4929fd
commit ef287cbfd4
2 changed files with 21 additions and 6 deletions

View File

@ -14,7 +14,7 @@ set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때
REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}" REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}"
BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}" BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}"
VERSION="0.12.0" VERSION="0.12.1"
CONFIG_DIR="${YAKCLOUD_CONFIG_DIR:-$HOME/.config/yakcloud}" CONFIG_DIR="${YAKCLOUD_CONFIG_DIR:-$HOME/.config/yakcloud}"
CONFIG_FILE="$CONFIG_DIR/config" CONFIG_FILE="$CONFIG_DIR/config"

View File

@ -153,6 +153,12 @@ def reconcile_domain(fqdn: str) -> None:
log(f' 외부 도메인 — DNS 에 TXT 추가 후 검증: {v["host"]} TXT "{v["value"]}"') log(f' 외부 도메인 — DNS 에 TXT 추가 후 검증: {v["host"]} TXT "{v["value"]}"')
def _repo(img: str) -> str:
"""이미지에서 :tag 제거한 repo(레지스트리 포트 host:5000/… 의 콜론은 보존)."""
seg = img.rsplit("/", 1)[-1]
return img.rsplit(":", 1)[0] if ":" in seg else img
def deploy_workload(w: dict) -> tuple[str | None, bool]: def deploy_workload(w: dict) -> tuple[str | None, bool]:
image = w["image"].replace("${TAG}", TAG) image = w["image"].replace("${TAG}", TAG)
ex = w.get("expose", {}) or {} ex = w.get("expose", {}) or {}
@ -178,17 +184,26 @@ def deploy_workload(w: dict) -> tuple[str | None, bool]:
env = [{"key": k, "value": v} for k, v in env.items()] env = [{"key": k, "value": v} for k, v in env.items()]
body["env"] = [{"key": e["key"], "value": str(e.get("value", "")), body["env"] = [{"key": e["key"], "value": str(e.get("value", "")),
"secret": bool(e.get("secret", False))} for e in env] "secret": bool(e.get("secret", False))} for e in env]
# 도메인 리컨실 — 워크로드 host(들) + 환경(운영) 도메인을 레지스트리에 등록(없으면). # 충돌 가드 — 같은 이름의 기존 배포가 '다른 앱'(다른 이미지 repo)이면 덮어쓰기 거부(무조건 배포 금지).
# 같은 repo(태그만 다름)=정상 재배포는 PATCH 로 진행. (백엔드 create_deployment 도 동일하게 409 로 강제.)
existing = next((d for d in cluster_deployments() if d.get("name") == w["name"]), None)
if existing:
ex_repo = _repo(existing.get("image") or "")
if ex_repo and ex_repo != _repo(image):
raise SystemExit(
f"배포명 '{w['name']}' 충돌 — 이 클러스터에 이미 다른 앱이 그 이름을 쓰고 있습니다.\n"
f" 기존 image: {existing.get('image')}\n 내 image: {image}\n"
f" → 워크로드 이름을 바꾸거나(yakcloud set 로 name 변경) 기존 배포를 먼저 정리하세요.")
# 도메인 리컨실 — 워크로드 host(들) + 환경(운영) 도메인 등록(없으면). 충돌 통과 뒤에만.
for h in custom: for h in custom:
reconcile_domain(h) reconcile_domain(h)
if DRY: if DRY:
log(f"deploy '{w['name']}' 예정: image={image} port={body['port']} path={body['path']} " note = "기존 동일 앱 → PATCH 롤링" if existing else "신규 생성"
log(f"deploy '{w['name']}' 예정({note}): image={image} port={body['port']} path={body['path']} "
f"replicas={body['replicasDesired']} hosts={body.get('exposeHosts') or body.get('exposeHost')}") f"replicas={body['replicasDesired']} hosts={body.get('exposeHosts') or body.get('exposeHost')}")
return None, False return None, False
# 멱등: 기존 배포면 PATCH(이미지 갱신 → kubectl apply = 무중단 롤링). 없으면 신규 생성.
existing = next((d for d in cluster_deployments() if d.get("name") == w["name"]), None)
if existing: if existing:
log(f"deploy '{w['name']}' 기존 존재 → PATCH 롤링 업데이트 image={image}") log(f"deploy '{w['name']}' 기존(동일 앱) → PATCH 롤링 업데이트 image={image}")
api("PATCH", f"/deployments/{existing['id']}", body) api("PATCH", f"/deployments/{existing['id']}", body)
return existing["id"], False return existing["id"], False
log(f"deploy '{w['name']}' 신규 생성 image={image}") log(f"deploy '{w['name']}' 신규 생성 image={image}")