cli v0.12.0: 'yakcloud cluster create/ls' — 배포토큰으로 POST /clusters(템플릿 S/M) + 프로비저닝 폴. 클러스터 생성부터 배포까지 완전 CLI

This commit is contained in:
2026-08-28 05:22:32 +09:00
parent 2517615913
commit d85e4929fd
3 changed files with 83 additions and 1 deletions

View File

@ -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.11.1"
VERSION="0.12.0"
CONFIG_DIR="${YAKCLOUD_CONFIG_DIR:-$HOME/.config/yakcloud}"
CONFIG_FILE="$CONFIG_DIR/config"
@ -292,6 +292,83 @@ cmd_project_promote() {
echo "✓ 승격 완료: $tag → 운영 클러스터 + 운영 도메인(기본 도메인은 자동)"
}
# 클러스터 — 계정 배포토큰으로 생성/조회(프로젝트 폴더 불필요). POST /clusters(템플릿) + 프로비저닝 폴.
cmd_cluster() {
: "${YAKCLOUD_URL:?YAKCLOUD_URL 필요 — 'yakcloud login'}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요 — 'yakcloud login'}"
local sub="${1:-ls}"; [ "$#" -gt 0 ] && shift
case "$sub" in
ls|list)
YAKCLOUD_URL="$YAKCLOUD_URL" YAKCLOUD_TOKEN="$YAKCLOUD_TOKEN" python3 - <<'PY'
import json,os,urllib.request,urllib.error,sys
U=os.environ["YAKCLOUD_URL"].rstrip("/");T=os.environ["YAKCLOUD_TOKEN"]
def api(m,p):
r=urllib.request.Request(U+"/api/v1"+p,method=m,headers={"Authorization":"Bearer "+T})
try:
with urllib.request.urlopen(r,timeout=30) as x: return json.loads(x.read().decode() or "{}")
except urllib.error.HTTPError as e: sys.exit(" ✗ %s -> %s: %s"%(p,e.code,e.read().decode()[:200]))
d=api("GET","/clusters"); items=d.get("data",d) or []
print("클러스터 (%d)"%len(items))
print(" %-18s %-10s %-6s %s"%("이름","상태","노드","기본 도메인"))
for c in items:
print(" %-18s %-10s %-6s %s"%(c.get("name","?"),c.get("status","?"),c.get("desiredNodes","?"),c.get("defaultHostname") or ""))
PY
;;
create|new)
local name="" tmpl="S" nodes="" golden=0
while [ "$#" -gt 0 ]; do
case "$1" in
-t|--template) tmpl="${2:-S}"; shift 2 || shift ;;
-n|--nodes) nodes="${2:-}"; shift 2 || shift ;;
--golden|--golden-path) golden=1; shift ;;
-*) shift ;;
*) [ -z "$name" ] && name="$1"; shift ;;
esac
done
[ -n "$name" ] || die "클러스터 이름 필요 — 'yakcloud cluster create <name> [--template S|M] [--nodes N] [--golden]'"
YC_NAME="$name" YC_TMPL="$tmpl" YC_NODES="$nodes" YC_GOLDEN="$golden" \
YAKCLOUD_URL="$YAKCLOUD_URL" YAKCLOUD_TOKEN="$YAKCLOUD_TOKEN" python3 - <<'PY'
import json,os,sys,time,urllib.request,urllib.error
U=os.environ["YAKCLOUD_URL"].rstrip("/");T=os.environ["YAKCLOUD_TOKEN"]
def api(m,p,b=None,to=30):
data=json.dumps(b).encode() if b is not None else None
r=urllib.request.Request(U+"/api/v1"+p,data=data,method=m,
headers={"Authorization":"Bearer "+T,"content-type":"application/json"})
try:
with urllib.request.urlopen(r,timeout=to) as x: raw=x.read().decode(); return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e: sys.exit(" ✗ %s %s -> %s: %s"%(m,p,e.code,e.read().decode()[:300]))
uw=lambda r:(r.get("data",r) if isinstance(r,dict) else r)
name=os.environ["YC_NAME"];tmpl=os.environ["YC_TMPL"].upper()
nodes=os.environ.get("YC_NODES","");golden=os.environ.get("YC_GOLDEN")=="1"
tmap={t["code"]:t for t in uw(api("GET","/templates")) or []}
if tmpl not in tmap: sys.exit(" ✗ 템플릿 '%s' 없음 (있음: %s)"%(tmpl,", ".join(tmap)))
if tmpl=="C": sys.exit(" ✗ Custom(C)은 사양 상세가 필요 — 콘솔에서 생성하세요")
tt=tmap[tmpl];mn,mx=tt.get("minNodes",1),tt.get("maxNodes",9)
n=int(nodes) if nodes else mn
n=max(mn,min(n,mx))
print("▸ 클러스터 생성: %s (템플릿 %s '%s', 노드 %d%s)"%(name,tmpl,tt.get("name",""),n," +골든패스" if golden else ""))
c=uw(api("POST","/clusters",{"name":name,"templateCode":tmpl,"nodeCount":n,"goldenPath":golden}))
cid=c.get("id") or name
print(" 요청됨(status=%s, id=%s). 프로비저닝 대기(보통 5~8분)…"%(c.get("status"),cid))
last=None
for _ in range(200): # ~50분
time.sleep(15)
s=uw(api("GET","/clusters/%s"%cid))
st=s.get("status","?");joined=len([x for x in (s.get("nodes") or []) if x.get("joined")])
cur=(st,joined)
if cur!=last: print(" … %s (노드 %d/%s)"%(st,joined,s.get("desiredNodes","?")));last=cur
if st=="ACTIVE":
print("✓ '%s' ACTIVE — 기본 도메인 %s"%(name,s.get("defaultHostname") or "?"))
print(" 다음: 매니페스트 environments 에 '%s' 지정 후 'yakcloud project deploy'"%name)
break
if st in ("ERROR","FAILED","DELETED"): sys.exit(" ✗ 프로비저닝 실패(status=%s)"%st)
else:
sys.exit(" ⏳ 대기 초과 — 'yakcloud cluster ls' 로 상태 확인")
PY
;;
*) echo "yakcloud cluster <ls|create <name> [--template S|M] [--nodes N] [--golden]>"; exit 1 ;;
esac
}
# 전역 CLI 자기 갱신 — 버전이 같아도 항상 최신 파일을 강제로 다시 받아 덮어쓴다(캐시 우회).
# Claude Code 전역 슬래시 명령(/yakcloud) 설치/최신화 — 설치·업그레이드 시 함께 실행.
# opt-out: YAKCLOUD_NO_CLAUDE_CMD=1. 실패해도 CLI 동작엔 지장 없음(비치명적).
@ -351,6 +428,8 @@ cmd_project_update() {
usage() {
cat <<EOF
yakcloud $VERSION — YakCloud 프로젝트 CLI (개발·운영 모두 yakcloud 클러스터)
cluster create <name> [--template S|M] [--nodes N] [--golden] 클러스터 생성(프로비저닝 완료까지 대기)
cluster ls 내 클러스터 목록(이름·상태·노드·기본 도메인)
project init [name] 빈 폴더에 스타터 스캐폴딩(대화형 마법사) + 자격 프롬프트
project deploy [vX.Y.Z] 커밋 + 태그 push → CI 빌드 → **개발 클러스터** 배포(버전 생략=자동 버전업)
project deploy --local 러너 없이 로컬 build/push/리컨실(CI 대안, 개발 클러스터)
@ -389,6 +468,7 @@ case "${1:-help}" in
update) shift; cmd_project_update "$@" ;;
*) echo "yakcloud project <init|deploy|promote|info|check|update>"; exit 1 ;;
esac ;;
cluster) shift; cmd_cluster "$@" ;;
login) cmd_login ;;
config) cmd_config ;;
logout) cmd_logout ;;