From d85e4929fdd36760b2877b0cd6a6d0845448c3a4 Mon Sep 17 00:00:00 2001 From: yakenator Date: Fri, 28 Aug 2026 05:22:32 +0900 Subject: [PATCH] =?UTF-8?q?cli=20v0.12.0:=20'yakcloud=20cluster=20create/l?= =?UTF-8?q?s'=20=E2=80=94=20=EB=B0=B0=ED=8F=AC=ED=86=A0=ED=81=B0=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20POST=20/clusters(=ED=85=9C=ED=94=8C=EB=A6=BF=20S/M)?= =?UTF-8?q?=20+=20=ED=94=84=EB=A1=9C=EB=B9=84=EC=A0=80=EB=8B=9D=20?= =?UTF-8?q?=ED=8F=B4.=20=ED=81=B4=EB=9F=AC=EC=8A=A4=ED=84=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EB=B6=80=ED=84=B0=20=EB=B0=B0=ED=8F=AC=EA=B9=8C?= =?UTF-8?q?=EC=A7=80=20=EC=99=84=EC=A0=84=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/yakcloud.md | 1 + .claude/skills/yakcloud-deploy/SKILL.md | 1 + bin/yakcloud | 82 ++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/.claude/commands/yakcloud.md b/.claude/commands/yakcloud.md index b624aba..8033e3e 100644 --- a/.claude/commands/yakcloud.md +++ b/.claude/commands/yakcloud.md @@ -19,6 +19,7 @@ allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion - **`project init [name]`** → §2 스캐폴딩(마법사 대체). - **`login`** → 비밀 입력이라 여기서 실행하지 말고, 터미널에서 `yakcloud login` 하라고 한 줄 안내. (사용자가 값을 직접 주면 `YAKCLOUD_URL=… YAKCLOUD_TOKEN=… GITEA_USER=… GITEA_TOKEN=… yakcloud config` 로 저장하되 **값은 화면에 출력 금지**.) +- **`cluster create …`** → 배포토큰으로 클러스터 프로비저닝(5~8분 대기). 실행 전 무엇을(템플릿·노드 수) 만드는지 한 줄 알리고 진행. `cluster ls` = 목록. - **`project promote [vX.Y.Z]`** → 개발에서 검증한 그 이미지를 **재빌드 없이 운영 클러스터로** 승격 + 운영 도메인. 파괴적/외부노출이므로 실행 전 무엇을(어느 태그를 어느 운영 클러스터로) 하는지 한 줄 알리고 진행. - **그 외 전부**(`project deploy|info|check|update`, `datasource ls`, `domain`, `scale`, `set`, `env`, diff --git a/.claude/skills/yakcloud-deploy/SKILL.md b/.claude/skills/yakcloud-deploy/SKILL.md index ea5885d..e6efb1a 100644 --- a/.claude/skills/yakcloud-deploy/SKILL.md +++ b/.claude/skills/yakcloud-deploy/SKILL.md @@ -23,6 +23,7 @@ YakCloud 콘솔 API로 **데이터소스 리컨실 + 워크로드 배포 + 바 > 설치 시 **Claude Code 전역 슬래시 명령 `/yakcloud`** 도 함께 생성된다(`~/.claude/commands/yakcloud.md`). 예: `/yakcloud project init my-app`, `/yakcloud datasource ls`, `/yakcloud project deploy`. 이후 `yakcloud upgrade` 가 CLI와 슬래시 명령을 함께 최신화. **프로젝트 라이프사이클 (개발·운영 모두 yakcloud 클러스터)** +- **"클러스터 생성"** → `yakcloud cluster create [--template S|M] [--nodes N] [--golden]` (배포토큰으로 프로비저닝, 완료까지 대기). `yakcloud cluster ls` 로 목록. 개발/운영 각각 만들어 매니페스트 environments 에 지정. - **"프로젝트 초기화"** → 빈 폴더에서 `yakcloud project init [name]` (스타터+CI+매니페스트+이 스킬) - **"개발 배포"** → `yakcloud project deploy` (자동 버전업 → 태그 push → CI 빌드 → **개발 클러스터**). 기본 도메인으로 확인. - **"운영 승격"** → `yakcloud project promote [vX.Y.Z]` (개발에서 검증한 **그 이미지를 재빌드 없이 운영 클러스터로** + 운영 도메인) diff --git a/bin/yakcloud b/bin/yakcloud index 7110bff..c11626b 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.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 [--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 [--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 < [--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 "; exit 1 ;; esac ;; + cluster) shift; cmd_cluster "$@" ;; login) cmd_login ;; config) cmd_config ;; logout) cmd_logout ;;