310 lines
16 KiB
Bash
Executable File
310 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# yakcloud — YakCloud 프로젝트 CLI. 빈 폴더 초기화(스타터+스킬), 배포, 배포환경 상세 설정.
|
|
#
|
|
# yakcloud project init [name] 빈 폴더에 스타터 스캐폴딩(앱+CI+매니페스트+Claude 스킬)
|
|
# yakcloud project deploy [vX.Y.Z] 커밋 + 태그 push → Gitea Actions 자동 배포
|
|
# yakcloud project info 프로젝트 개괄(매니페스트 + 라이브 상태)
|
|
# yakcloud project check dry-run(직접 API 계획만)
|
|
#
|
|
# yakcloud domain <fqdn> [wl] 도메인 등록 + 워크로드에 할당(관리형=즉시, 배포중이면 라이브)
|
|
# yakcloud scale <wl> <n> replicas 변경
|
|
# yakcloud set <wl> --image/--port/--health/--cpu/--mem/--path/--rewrite
|
|
# yakcloud env <wl> KEY=VAL … [--secret KEY] [--unset KEY]
|
|
# yakcloud source add <name> <type> [plan] | source rm <name>
|
|
# yakcloud bind <wl> <source> <alias> | unbind <wl> <alias>
|
|
#
|
|
# env: YAKCLOUD_URL, YAKCLOUD_TOKEN(배포토큰 yakd_…), (선택) YAKCLOUD_CLUSTER
|
|
set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때 CLI 를 중단시키는 함정 회피. 중요 경로는 명시적 || die.
|
|
|
|
REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}"
|
|
BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}"
|
|
VERSION="0.4.0"
|
|
CONFIG_DIR="${YAKCLOUD_CONFIG_DIR:-$HOME/.config/yakcloud}"
|
|
CONFIG_FILE="$CONFIG_DIR/config"
|
|
|
|
die() { echo "✗ $*" >&2; exit 1; }
|
|
info() { echo "▸ $*"; }
|
|
|
|
ensure_pyyaml() {
|
|
python3 -c "import yaml" 2>/dev/null && return 0
|
|
pip3 install --quiet pyyaml 2>/dev/null \
|
|
|| python3 -m pip install --quiet --break-system-packages pyyaml 2>/dev/null \
|
|
|| die "pyyaml 설치 실패 — 'pip install pyyaml'"
|
|
}
|
|
|
|
# 설정/조회 커맨드는 프로젝트의 ctl 엔진에 위임(현재 폴더 = 프로젝트).
|
|
ctl() {
|
|
[ -f scripts/yakcloud_ctl.py ] || die "프로젝트 폴더가 아닙니다 — 'yakcloud project init' 로 초기화하세요."
|
|
ensure_pyyaml
|
|
python3 scripts/yakcloud_ctl.py "$@"
|
|
}
|
|
|
|
# ── 자격 저장 ── 비밀(배포토큰·Gitea토큰)=OS 키체인(암호화), 그 외(URL·사용자)=config 파일(600) ──
|
|
_KC="none"
|
|
if command -v security >/dev/null 2>&1; then _KC="macos"
|
|
elif command -v secret-tool >/dev/null 2>&1; then _KC="linux"; fi
|
|
|
|
_enc_key() {
|
|
[ -f "$CONFIG_DIR/.key" ] || { mkdir -p "$CONFIG_DIR"; (openssl rand -hex 32 2>/dev/null || date +%s%N) > "$CONFIG_DIR/.key"; chmod 600 "$CONFIG_DIR/.key"; }
|
|
cat "$CONFIG_DIR/.key"
|
|
}
|
|
secret_set() { # secret_set NAME VALUE — 항상 exit 0(set -e 안전).
|
|
case "$_KC" in
|
|
macos) security add-generic-password -U -a "${USER:-yakcloud}" -s "yakcloud-$1" -w "$2" >/dev/null 2>&1 || echo " ⚠ 키체인 저장 실패($1)" >&2 ;;
|
|
linux) printf '%s' "$2" | secret-tool store --label="yakcloud $1" service yakcloud name "$1" >/dev/null 2>&1 || echo " ⚠ 키체인 저장 실패($1)" >&2 ;;
|
|
*) if command -v openssl >/dev/null 2>&1; then
|
|
mkdir -p "$CONFIG_DIR"; printf '%s' "$2" | openssl enc -aes-256-cbc -pbkdf2 -salt -pass "pass:$(_enc_key)" -out "$CONFIG_DIR/sec-$1.enc" 2>/dev/null && chmod 600 "$CONFIG_DIR/sec-$1.enc"
|
|
else echo " ⚠ 키체인/openssl 없음 — '$1' 저장 못 함" >&2; fi ;;
|
|
esac
|
|
return 0
|
|
}
|
|
secret_get() { # secret_get NAME — 없으면 빈 문자열, 항상 exit 0(set -e 안전).
|
|
local out=""
|
|
case "$_KC" in
|
|
macos) out="$(security find-generic-password -a "${USER:-yakcloud}" -s "yakcloud-$1" -w 2>/dev/null || true)" ;;
|
|
linux) out="$(secret-tool lookup service yakcloud name "$1" 2>/dev/null || true)" ;;
|
|
*) [ -f "$CONFIG_DIR/sec-$1.enc" ] && out="$(openssl enc -d -aes-256-cbc -pbkdf2 -pass "pass:$(_enc_key)" -in "$CONFIG_DIR/sec-$1.enc" 2>/dev/null || true)" ;;
|
|
esac
|
|
printf '%s' "$out"
|
|
return 0
|
|
}
|
|
secret_del() {
|
|
case "$_KC" in
|
|
macos) security delete-generic-password -a "${USER:-yakcloud}" -s "yakcloud-$1" >/dev/null 2>&1 || true ;;
|
|
linux) secret-tool clear service yakcloud name "$1" >/dev/null 2>&1 || true ;;
|
|
*) rm -f "$CONFIG_DIR/sec-$1.enc" 2>/dev/null || true ;;
|
|
esac
|
|
}
|
|
|
|
# 저장된 자격 로드 — 이미 export 된 env 가 우선(env > 저장값). set -e 안전(모두 if/then).
|
|
load_config() {
|
|
if [ -f "$CONFIG_FILE" ]; then
|
|
while IFS='=' read -r k v; do
|
|
case "$k" in ''|\#*) continue ;; esac
|
|
v="${v%\"}"; v="${v#\"}"
|
|
if [ -z "$(eval printf '%s' "\${$k:-}")" ]; then export "$k=$v"; fi
|
|
done < "$CONFIG_FILE"
|
|
fi
|
|
if [ -z "${YAKCLOUD_TOKEN:-}" ]; then
|
|
local t; t="$(secret_get YAKCLOUD_TOKEN)"
|
|
if [ -n "$t" ]; then export YAKCLOUD_TOKEN="$t"; fi
|
|
fi
|
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
|
local g; g="$(secret_get GITEA_TOKEN)"
|
|
if [ -n "$g" ]; then export GITEA_TOKEN="$g"; fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
_save_nonsecret() { # url cluster gitea_url gitea_user
|
|
mkdir -p "$CONFIG_DIR"; chmod 700 "$CONFIG_DIR" 2>/dev/null || true
|
|
( umask 177; cat > "$CONFIG_FILE" <<EOF
|
|
YAKCLOUD_URL="$1"
|
|
YAKCLOUD_CLUSTER="$2"
|
|
GITEA_URL="$3"
|
|
GITEA_USER="$4"
|
|
EOF
|
|
); chmod 600 "$CONFIG_FILE"
|
|
}
|
|
|
|
cmd_login() {
|
|
echo "yakcloud 자격 설정 (비밀=OS 키체인[$_KC] 암호화, 그 외=$CONFIG_FILE)"
|
|
local url tok clus gurl guser gtok
|
|
printf "YakCloud URL [%s]: " "${YAKCLOUD_URL:-https://console.yakenator.io}"; read -r url; url="${url:-${YAKCLOUD_URL:-https://console.yakenator.io}}"
|
|
printf "YakCloud 배포 토큰(yakd_…): "; read -rs tok; echo
|
|
printf "기본 클러스터(선택) [%s]: " "${YAKCLOUD_CLUSTER:-}"; read -r clus; clus="${clus:-${YAKCLOUD_CLUSTER:-}}"
|
|
printf "Gitea URL [%s]: " "${GITEA_URL:-https://gitea.yakenator.io}"; read -r gurl; gurl="${gurl:-${GITEA_URL:-https://gitea.yakenator.io}}"
|
|
printf "Gitea 사용자 [%s]: " "${GITEA_USER:-}"; read -r guser; guser="${guser:-${GITEA_USER:-}}"
|
|
printf "Gitea 토큰: "; read -rs gtok; echo
|
|
_save_nonsecret "$url" "$clus" "$gurl" "$guser"
|
|
[ -n "$tok" ] && secret_set YAKCLOUD_TOKEN "$tok"
|
|
[ -n "$gtok" ] && secret_set GITEA_TOKEN "$gtok"
|
|
echo "✓ 저장 완료 — 비밀은 키체인($_KC), 설정은 $CONFIG_FILE (600)"
|
|
}
|
|
|
|
cmd_config() {
|
|
[ -f "$CONFIG_FILE" ] || { echo "설정 없음 — 'yakcloud login'"; return 0; }
|
|
echo "설정: $CONFIG_FILE (비밀=키체인 $_KC)"
|
|
grep -vE '^\s*#' "$CONFIG_FILE" | sed 's/^/ /'
|
|
local t; t="$(secret_get YAKCLOUD_TOKEN)"; [ -n "$t" ] && echo " YAKCLOUD_TOKEN=$(printf '%s' "$t" | cut -c1-9)…(키체인)"
|
|
local g; g="$(secret_get GITEA_TOKEN)"; [ -n "$g" ] && echo " GITEA_TOKEN=••••••(키체인)"
|
|
}
|
|
|
|
cmd_logout() { rm -f "$CONFIG_FILE"; secret_del YAKCLOUD_TOKEN; secret_del GITEA_TOKEN; echo "✓ 자격 삭제(설정 파일 + 키체인)"; }
|
|
|
|
cmd_project_init() {
|
|
local name="" noninteractive=0
|
|
for a in "$@"; do case "$a" in -y|--yes|--no-wizard) noninteractive=1 ;; *) [ -z "$name" ] && name="$a" ;; esac; done
|
|
[ -n "$name" ] && { mkdir -p "$name"; cd "$name"; }
|
|
if [ -n "$(ls -A . 2>/dev/null | grep -v '^\.git$' || true)" ]; then
|
|
die "폴더가 비어있지 않습니다. 빈 폴더에서 실행하거나 'yakcloud project init <name>'."
|
|
fi
|
|
info "yakcloud-starter 내려받는 중… ($REPO @ $BRANCH)"
|
|
command -v git >/dev/null 2>&1 || die "git 이 필요합니다."
|
|
local tmp; tmp="$(mktemp -d)"
|
|
git clone -q --depth 1 -b "$BRANCH" "$REPO.git" "$tmp/s" \
|
|
|| { rm -rf "$tmp"; die "스타터 클론 실패 — 네트워크/레포 공개 여부 확인 ($REPO)"; }
|
|
rm -rf "$tmp/s/.git"
|
|
cp -R "$tmp/s/." .
|
|
rm -rf "$tmp"
|
|
[ -n "$name" ] && [ -f yakcloud.yaml ] && { sed -i.bak "s/^project: .*/project: $name/" yakcloud.yaml && rm -f yakcloud.yaml.bak; }
|
|
git rev-parse --git-dir >/dev/null 2>&1 || git init -q
|
|
echo "✓ 스캐폴딩 완료 ($(pwd))"
|
|
# 대화형 마법사(TTY + not --yes): 클러스터·앱·포트·헬스·CPU·메모리·replicas·env 를 물어 매니페스트에 반영.
|
|
if [ "$noninteractive" = 0 ] && [ -t 0 ]; then
|
|
_init_wizard "${name:-$(basename "$(pwd)")}"
|
|
_maybe_login_prompt
|
|
fi
|
|
echo " 다음: yakcloud project info · yakcloud domain <fqdn> · yakcloud project deploy"
|
|
}
|
|
|
|
# 매니페스트 채우기 마법사 — ctl 은 API 미접속(YAKCLOUD_TOKEN 비움)으로 매니페스트만 편집.
|
|
_init_wizard() {
|
|
local dflt_app="$1" app cluster port health cpu mem reps kv
|
|
echo "── 프로젝트 설정 (엔터=기본값) ──"
|
|
printf "앱 이름 [%s]: " "$dflt_app"; read -r app; app="${app:-$dflt_app}"
|
|
printf "대상 클러스터(이름 또는 콘솔 id): "; read -r cluster
|
|
printf "컨테이너 포트 [8080]: "; read -r port; port="${port:-8080}"
|
|
printf "헬스체크 경로 [/healthz]: "; read -r health; health="${health:-/healthz}"
|
|
printf "CPU 요청 [25m]: "; read -r cpu; cpu="${cpu:-25m}"
|
|
printf "메모리 요청 [64Mi]: "; read -r mem; mem="${mem:-64Mi}"
|
|
printf "replicas [2]: "; read -r reps; reps="${reps:-2}"
|
|
printf "노출 경로(ingress path) [/]: "; read -r path; path="${path:-/}"
|
|
sed -i.bak "s/^project: .*/project: $app/" yakcloud.yaml && rm -f yakcloud.yaml.bak
|
|
sed -i.bak "s/yakcloud-starter-web/${app}-web/" yakcloud.yaml 2>/dev/null; rm -f yakcloud.yaml.bak
|
|
[ -n "${GITEA_USER:-}" ] && { sed -i.bak "s#/yakenator/#/${GITEA_USER}/#" yakcloud.yaml 2>/dev/null; rm -f yakcloud.yaml.bak; }
|
|
[ -n "$cluster" ] && { sed -i.bak "s#^cluster: .*#cluster: $cluster#" yakcloud.yaml && rm -f yakcloud.yaml.bak; }
|
|
ensure_pyyaml
|
|
YAKCLOUD_TOKEN= python3 scripts/yakcloud_ctl.py set web --port "$port" --health "$health" --cpu "$cpu" --mem "$mem" --path "$path" >/dev/null 2>&1 || true
|
|
YAKCLOUD_TOKEN= python3 scripts/yakcloud_ctl.py scale web "$reps" >/dev/null 2>&1 || true
|
|
echo "환경변수(KEY=VALUE, 빈 줄=종료):"
|
|
while :; do
|
|
printf " env> "; read -r kv || break; [ -z "$kv" ] && break
|
|
YAKCLOUD_TOKEN= python3 scripts/yakcloud_ctl.py env web "$kv" >/dev/null 2>&1 || echo " (형식: KEY=VALUE)"
|
|
done
|
|
echo "✓ 매니페스트 반영: app=$app cluster=${cluster:-<미지정>} port=$port path=$path cpu=$cpu mem=$mem replicas=$reps"
|
|
}
|
|
|
|
_maybe_login_prompt() {
|
|
[ -n "${YAKCLOUD_TOKEN:-}" ] && return 0 # 이미 저장/설정됨
|
|
printf "배포 자격(YakCloud 배포토큰·Gitea)을 지금 저장할까요? [y/N]: "; read -r a
|
|
case "$a" in y|Y|yes|YES) cmd_login ;; *) echo " 나중에 'yakcloud login' 으로 설정하세요." ;; esac
|
|
}
|
|
|
|
cmd_project_deploy() {
|
|
[ -f yakcloud.yaml ] || die "yakcloud.yaml 없음 — 'yakcloud project init' 먼저"
|
|
git rev-parse --git-dir >/dev/null 2>&1 || git init -q
|
|
git remote get-url origin >/dev/null 2>&1 \
|
|
|| die "git 원격(origin) 없음 — Gitea 레포 만들어 'git remote add origin <url>' + Secrets 설정 후 다시"
|
|
git fetch -q --tags origin 2>/dev/null || true
|
|
# 버전: 인자 있으면 그것, 없으면 최신 v* 태그에서 패치 +1(없으면 v0.1.0) — 배포마다 자동 버전업.
|
|
local ver="${1:-}"
|
|
if [ -z "$ver" ]; then
|
|
local last; last="$(git tag -l 'v*' --sort=-v:refname 2>/dev/null | head -1)"
|
|
if [ -n "$last" ]; then
|
|
ver="$(printf '%s' "$last" | awk -F. 'BEGIN{OFS="."} {$NF=$NF+1; print}')"
|
|
else
|
|
ver="v0.1.0"
|
|
fi
|
|
fi
|
|
case "$ver" in v*) : ;; *) ver="v$ver" ;; esac
|
|
info "배포 버전: $ver (커밋 + 태그 push)"
|
|
git add -A; git commit -q -m "deploy $ver" 2>/dev/null || true
|
|
git push -q origin HEAD 2>/dev/null || true
|
|
git tag "$ver" 2>/dev/null || die "태그 $ver 이미 존재 — 'yakcloud project deploy <다음버전>'"
|
|
git push -q origin "$ver"
|
|
echo "✓ $ver push — Gitea Actions 에서 빌드·배포. 콘솔 앱 탭/도메인 확인."
|
|
}
|
|
|
|
cmd_project_check() {
|
|
[ -f scripts/yakcloud_deploy.py ] || die "'yakcloud project init' 먼저"
|
|
: "${YAKCLOUD_URL:?YAKCLOUD_URL 필요}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요}"
|
|
ensure_pyyaml
|
|
python3 scripts/yakcloud_deploy.py yakcloud.yaml --dry-run
|
|
}
|
|
|
|
# 전역 CLI 자기 갱신 — 버전이 같아도 항상 최신 파일을 강제로 다시 받아 덮어쓴다(캐시 우회).
|
|
cmd_upgrade() {
|
|
local self; self="$(command -v yakcloud 2>/dev/null || true)"; [ -n "$self" ] || self="$0"
|
|
# 심볼릭 링크면 실체 파일을 갱신.
|
|
if [ -L "$self" ]; then self="$(readlink -f "$self" 2>/dev/null || readlink "$self" 2>/dev/null || echo "$self")"; fi
|
|
local before; before="$("$self" version 2>/dev/null || echo '?')"
|
|
info "yakcloud CLI 강제 갱신… ($self, 현재 $before)"
|
|
local tmp; tmp="$(mktemp)"
|
|
# 캐시 우회: no-cache 헤더 + 캐시버스터 쿼리. Gitea/프록시가 raw 를 캐시해도 항상 최신 획득.
|
|
local bust; bust="$(date +%s 2>/dev/null || echo 0)"
|
|
curl -fsSL -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' \
|
|
"$REPO/raw/branch/$BRANCH/bin/yakcloud?nocache=$bust" -o "$tmp" \
|
|
|| { rm -f "$tmp"; die "다운로드 실패"; }
|
|
head -1 "$tmp" | grep -q 'bash' || { rm -f "$tmp"; die "받은 파일이 올바르지 않습니다"; }
|
|
chmod +x "$tmp"
|
|
mv "$tmp" "$self" 2>/dev/null || { rm -f "$tmp"; die "설치 실패(권한) — 'DEST=<dir> 재설치' 또는 sudo"; }
|
|
hash -r 2>/dev/null || true # 셸 명령 해시 캐시 초기화(새 바이너리 즉시 반영)
|
|
echo "✓ 업그레이드 완료: $before → $("$self" version 2>/dev/null || echo yakcloud)"
|
|
}
|
|
|
|
# 기존 프로젝트의 플러밍(엔진·CI·스킬)만 최신화. 앱/매니페스트/Dockerfile 등 사용자 파일은 보존.
|
|
cmd_project_update() {
|
|
[ -f yakcloud.yaml ] || die "프로젝트 폴더가 아닙니다 — 'yakcloud project init' 먼저"
|
|
command -v git >/dev/null 2>&1 || die "git 이 필요합니다."
|
|
info "프로젝트 플러밍 최신화(앱·매니페스트 보존)… ($REPO @ $BRANCH)"
|
|
local tmp; tmp="$(mktemp -d)"
|
|
git clone -q --depth 1 -b "$BRANCH" "$REPO.git" "$tmp/s" || { rm -rf "$tmp"; die "클론 실패"; }
|
|
mkdir -p scripts .gitea/workflows .claude/skills
|
|
cp "$tmp/s/scripts/yakcloud_deploy.py" scripts/ 2>/dev/null || true
|
|
cp "$tmp/s/scripts/yakcloud_ctl.py" scripts/ 2>/dev/null || true
|
|
cp "$tmp/s/.gitea/workflows/deploy.yml" .gitea/workflows/ 2>/dev/null || true
|
|
cp "$tmp/s/DATA-SOURCES.md" DATA-SOURCES.md 2>/dev/null || true
|
|
rm -rf .claude/skills/yakcloud-deploy
|
|
cp -R "$tmp/s/.claude/skills/yakcloud-deploy" .claude/skills/ 2>/dev/null || true
|
|
rm -rf "$tmp"
|
|
echo "✓ 최신화 완료 — scripts/·.gitea/·.claude 스킬 갱신(app.py·yakcloud.yaml·Dockerfile 그대로)"
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
yakcloud $VERSION — YakCloud 프로젝트 CLI
|
|
project init [name] 빈 폴더에 스타터 스캐폴딩(앱+CI+매니페스트+Claude 스킬)
|
|
project deploy [vX.Y.Z] 커밋 + 태그 push → Gitea Actions 자동 배포(버전 생략=자동 버전업)
|
|
project info 프로젝트 개괄(매니페스트 + 라이브 상태)
|
|
project check dry-run(직접 API 계획만)
|
|
project update 이 프로젝트의 엔진·CI·스킬만 최신화(앱·매니페스트 보존)
|
|
upgrade 전역 yakcloud CLI 자기 갱신(최신 버전으로)
|
|
── 자격(한 곳에 안전 보관; 비밀=OS 키체인 암호화) ──
|
|
login Gitea 계정 + YakCloud 배포토큰 입력·저장
|
|
config 저장된 설정 표시(비밀 마스킹)
|
|
logout 저장 자격 삭제
|
|
── 배포환경 설정(앱별) ──
|
|
domain <fqdn> [wl] 도메인 등록 + 워크로드 할당(관리형=즉시/배포중이면 라이브)
|
|
scale <wl> <n> replicas 변경
|
|
set <wl> --image/--port/--health/--cpu/--mem/--path/--rewrite
|
|
env <wl> KEY=VAL … [--secret KEY] [--unset KEY]
|
|
source add <name> <type> [plan] | source rm <name>
|
|
bind <wl> <source> <alias> | unbind <wl> <alias>
|
|
env: YAKCLOUD_URL, YAKCLOUD_TOKEN(배포토큰 yakd_…), (선택) YAKCLOUD_CLUSTER
|
|
EOF
|
|
}
|
|
|
|
load_config # 저장된 자격 로드(env 우선)
|
|
|
|
case "${1:-help}" in
|
|
project)
|
|
shift
|
|
case "${1:-}" in
|
|
init) shift; cmd_project_init "$@" ;;
|
|
deploy) shift; cmd_project_deploy "$@" ;;
|
|
info) shift; ctl info "$@" ;;
|
|
check|dry-run) shift; cmd_project_check "$@" ;;
|
|
update) shift; cmd_project_update "$@" ;;
|
|
*) echo "yakcloud project <init|deploy|info|check|update>"; exit 1 ;;
|
|
esac ;;
|
|
login) cmd_login ;;
|
|
config) cmd_config ;;
|
|
logout) cmd_logout ;;
|
|
domain|scale|set|env|source|bind|unbind) ctl "$@" ;;
|
|
upgrade|self-update) cmd_upgrade ;;
|
|
version|-v|--version) echo "yakcloud $VERSION" ;;
|
|
help|-h|--help|"") usage ;;
|
|
*) echo "알 수 없는 명령: $1"; echo; usage; exit 1 ;;
|
|
esac
|