cli v0.12.0: 'yakcloud cluster create/ls' — 배포토큰으로 POST /clusters(템플릿 S/M) + 프로비저닝 폴. 클러스터 생성부터 배포까지 완전 CLI
This commit is contained in:
@ -19,6 +19,7 @@ allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion
|
|||||||
- **`project init [name]`** → §2 스캐폴딩(마법사 대체).
|
- **`project init [name]`** → §2 스캐폴딩(마법사 대체).
|
||||||
- **`login`** → 비밀 입력이라 여기서 실행하지 말고, 터미널에서 `yakcloud login` 하라고 한 줄 안내.
|
- **`login`** → 비밀 입력이라 여기서 실행하지 말고, 터미널에서 `yakcloud login` 하라고 한 줄 안내.
|
||||||
(사용자가 값을 직접 주면 `YAKCLOUD_URL=… YAKCLOUD_TOKEN=… GITEA_USER=… GITEA_TOKEN=… yakcloud config` 로 저장하되 **값은 화면에 출력 금지**.)
|
(사용자가 값을 직접 주면 `YAKCLOUD_URL=… YAKCLOUD_TOKEN=… GITEA_USER=… GITEA_TOKEN=… yakcloud config` 로 저장하되 **값은 화면에 출력 금지**.)
|
||||||
|
- **`cluster create <name> …`** → 배포토큰으로 클러스터 프로비저닝(5~8분 대기). 실행 전 무엇을(템플릿·노드 수) 만드는지 한 줄 알리고 진행. `cluster ls` = 목록.
|
||||||
- **`project promote [vX.Y.Z]`** → 개발에서 검증한 그 이미지를 **재빌드 없이 운영 클러스터로** 승격 + 운영 도메인.
|
- **`project promote [vX.Y.Z]`** → 개발에서 검증한 그 이미지를 **재빌드 없이 운영 클러스터로** 승격 + 운영 도메인.
|
||||||
파괴적/외부노출이므로 실행 전 무엇을(어느 태그를 어느 운영 클러스터로) 하는지 한 줄 알리고 진행.
|
파괴적/외부노출이므로 실행 전 무엇을(어느 태그를 어느 운영 클러스터로) 하는지 한 줄 알리고 진행.
|
||||||
- **그 외 전부**(`project deploy|info|check|update`, `datasource ls`, `domain`, `scale`, `set`, `env`,
|
- **그 외 전부**(`project deploy|info|check|update`, `datasource ls`, `domain`, `scale`, `set`, `env`,
|
||||||
|
|||||||
@ -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와 슬래시 명령을 함께 최신화.
|
> 설치 시 **Claude Code 전역 슬래시 명령 `/yakcloud`** 도 함께 생성된다(`~/.claude/commands/yakcloud.md`). 예: `/yakcloud project init my-app`, `/yakcloud datasource ls`, `/yakcloud project deploy`. 이후 `yakcloud upgrade` 가 CLI와 슬래시 명령을 함께 최신화.
|
||||||
|
|
||||||
**프로젝트 라이프사이클 (개발·운영 모두 yakcloud 클러스터)**
|
**프로젝트 라이프사이클 (개발·운영 모두 yakcloud 클러스터)**
|
||||||
|
- **"클러스터 생성"** → `yakcloud cluster create <name> [--template S|M] [--nodes N] [--golden]` (배포토큰으로 프로비저닝, 완료까지 대기). `yakcloud cluster ls` 로 목록. 개발/운영 각각 만들어 매니페스트 environments 에 지정.
|
||||||
- **"프로젝트 초기화"** → 빈 폴더에서 `yakcloud project init [name]` (스타터+CI+매니페스트+이 스킬)
|
- **"프로젝트 초기화"** → 빈 폴더에서 `yakcloud project init [name]` (스타터+CI+매니페스트+이 스킬)
|
||||||
- **"개발 배포"** → `yakcloud project deploy` (자동 버전업 → 태그 push → CI 빌드 → **개발 클러스터**). 기본 도메인으로 확인.
|
- **"개발 배포"** → `yakcloud project deploy` (자동 버전업 → 태그 push → CI 빌드 → **개발 클러스터**). 기본 도메인으로 확인.
|
||||||
- **"운영 승격"** → `yakcloud project promote [vX.Y.Z]` (개발에서 검증한 **그 이미지를 재빌드 없이 운영 클러스터로** + 운영 도메인)
|
- **"운영 승격"** → `yakcloud project promote [vX.Y.Z]` (개발에서 검증한 **그 이미지를 재빌드 없이 운영 클러스터로** + 운영 도메인)
|
||||||
|
|||||||
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}"
|
REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}"
|
||||||
BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}"
|
BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}"
|
||||||
VERSION="0.11.1"
|
VERSION="0.12.0"
|
||||||
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"
|
||||||
|
|
||||||
@ -292,6 +292,83 @@ cmd_project_promote() {
|
|||||||
echo "✓ 승격 완료: $tag → 운영 클러스터 + 운영 도메인(기본 도메인은 자동)"
|
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 자기 갱신 — 버전이 같아도 항상 최신 파일을 강제로 다시 받아 덮어쓴다(캐시 우회).
|
# 전역 CLI 자기 갱신 — 버전이 같아도 항상 최신 파일을 강제로 다시 받아 덮어쓴다(캐시 우회).
|
||||||
# Claude Code 전역 슬래시 명령(/yakcloud) 설치/최신화 — 설치·업그레이드 시 함께 실행.
|
# Claude Code 전역 슬래시 명령(/yakcloud) 설치/최신화 — 설치·업그레이드 시 함께 실행.
|
||||||
# opt-out: YAKCLOUD_NO_CLAUDE_CMD=1. 실패해도 CLI 동작엔 지장 없음(비치명적).
|
# opt-out: YAKCLOUD_NO_CLAUDE_CMD=1. 실패해도 CLI 동작엔 지장 없음(비치명적).
|
||||||
@ -351,6 +428,8 @@ cmd_project_update() {
|
|||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
yakcloud $VERSION — YakCloud 프로젝트 CLI (개발·운영 모두 yakcloud 클러스터)
|
yakcloud $VERSION — YakCloud 프로젝트 CLI (개발·운영 모두 yakcloud 클러스터)
|
||||||
|
cluster create <name> [--template S|M] [--nodes N] [--golden] 클러스터 생성(프로비저닝 완료까지 대기)
|
||||||
|
cluster ls 내 클러스터 목록(이름·상태·노드·기본 도메인)
|
||||||
project init [name] 빈 폴더에 스타터 스캐폴딩(대화형 마법사) + 자격 프롬프트
|
project init [name] 빈 폴더에 스타터 스캐폴딩(대화형 마법사) + 자격 프롬프트
|
||||||
project deploy [vX.Y.Z] 커밋 + 태그 push → CI 빌드 → **개발 클러스터** 배포(버전 생략=자동 버전업)
|
project deploy [vX.Y.Z] 커밋 + 태그 push → CI 빌드 → **개발 클러스터** 배포(버전 생략=자동 버전업)
|
||||||
project deploy --local 러너 없이 로컬 build/push/리컨실(CI 대안, 개발 클러스터)
|
project deploy --local 러너 없이 로컬 build/push/리컨실(CI 대안, 개발 클러스터)
|
||||||
@ -389,6 +468,7 @@ case "${1:-help}" in
|
|||||||
update) shift; cmd_project_update "$@" ;;
|
update) shift; cmd_project_update "$@" ;;
|
||||||
*) echo "yakcloud project <init|deploy|promote|info|check|update>"; exit 1 ;;
|
*) echo "yakcloud project <init|deploy|promote|info|check|update>"; exit 1 ;;
|
||||||
esac ;;
|
esac ;;
|
||||||
|
cluster) shift; cmd_cluster "$@" ;;
|
||||||
login) cmd_login ;;
|
login) cmd_login ;;
|
||||||
config) cmd_config ;;
|
config) cmd_config ;;
|
||||||
logout) cmd_logout ;;
|
logout) cmd_logout ;;
|
||||||
|
|||||||
Reference in New Issue
Block a user