cli v0.12.0: 'yakcloud cluster create/ls' — 배포토큰으로 POST /clusters(템플릿 S/M) + 프로비저닝 폴. 클러스터 생성부터 배포까지 완전 CLI
This commit is contained in:
82
bin/yakcloud
82
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 <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 ;;
|
||||
|
||||
Reference in New Issue
Block a user