From ef287cbfd4bf09c5010cac60ab16083c5c188013 Mon Sep 17 00:00:00 2001 From: yakenator Date: Fri, 28 Aug 2026 05:39:59 +0900 Subject: [PATCH] =?UTF-8?q?cli=20v0.12.1:=20=EB=B0=B0=ED=8F=AC=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=B6=A9=EB=8F=8C=20=EA=B0=80=EB=93=9C=20=E2=80=94?= =?UTF-8?q?=20=EA=B0=99=EC=9D=80=20=EC=9D=B4=EB=A6=84=C2=B7=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20repo=20=EB=A9=B4=20?= =?UTF-8?q?=EB=8D=AE=EC=96=B4=EC=93=B0=EA=B8=B0=20=EA=B1=B0=EB=B6=80(fail-?= =?UTF-8?q?fast).=20=EB=B0=B1=EC=97=94=EB=93=9C=20create=5Fdeployment=20?= =?UTF-8?q?=EB=8F=84=20409=20=EB=A1=9C=20=EA=B0=95=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/yakcloud | 2 +- scripts/yakcloud_deploy.py | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/bin/yakcloud b/bin/yakcloud index c11626b..cbd38cf 100755 --- a/bin/yakcloud +++ b/bin/yakcloud @@ -14,7 +14,7 @@ set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때 REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}" BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}" -VERSION="0.12.0" +VERSION="0.12.1" CONFIG_DIR="${YAKCLOUD_CONFIG_DIR:-$HOME/.config/yakcloud}" CONFIG_FILE="$CONFIG_DIR/config" diff --git a/scripts/yakcloud_deploy.py b/scripts/yakcloud_deploy.py index 75f3a3e..0917ac0 100644 --- a/scripts/yakcloud_deploy.py +++ b/scripts/yakcloud_deploy.py @@ -153,6 +153,12 @@ def reconcile_domain(fqdn: str) -> None: 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]: image = w["image"].replace("${TAG}", TAG) 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()] body["env"] = [{"key": e["key"], "value": str(e.get("value", "")), "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: reconcile_domain(h) 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')}") return None, False - # 멱등: 기존 배포면 PATCH(이미지 갱신 → kubectl apply = 무중단 롤링). 없으면 신규 생성. - existing = next((d for d in cluster_deployments() if d.get("name") == w["name"]), None) 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) return existing["id"], False log(f"deploy '{w['name']}' 신규 생성 image={image}")