diff --git a/bin/yakcloud b/bin/yakcloud index 1bc3882..ff68ea1 100755 --- a/bin/yakcloud +++ b/bin/yakcloud @@ -14,11 +14,13 @@ # yakcloud bind | unbind # # env: YAKCLOUD_URL, YAKCLOUD_TOKEN(배포토큰 yakd_…), (선택) YAKCLOUD_CLUSTER -set -euo pipefail +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.3.1" +VERSION="0.4.0" +CONFIG_DIR="${YAKCLOUD_CONFIG_DIR:-$HOME/.config/yakcloud}" +CONFIG_FILE="$CONFIG_DIR/config" die() { echo "✗ $*" >&2; exit 1; } info() { echo "▸ $*"; } @@ -37,8 +39,102 @@ ctl() { 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" </dev/null | grep -v '^\.git$' || true)" ]; then die "폴더가 비어있지 않습니다. 빈 폴더에서 실행하거나 'yakcloud project init '." @@ -53,8 +149,46 @@ cmd_project_init() { 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))" - echo " 다음: yakcloud project info · yakcloud domain … · yakcloud project deploy v0.1.0" + 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 · 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() { @@ -136,6 +270,10 @@ yakcloud $VERSION — YakCloud 프로젝트 CLI project check dry-run(직접 API 계획만) project update 이 프로젝트의 엔진·CI·스킬만 최신화(앱·매니페스트 보존) upgrade 전역 yakcloud CLI 자기 갱신(최신 버전으로) + ── 자격(한 곳에 안전 보관; 비밀=OS 키체인 암호화) ── + login Gitea 계정 + YakCloud 배포토큰 입력·저장 + config 저장된 설정 표시(비밀 마스킹) + logout 저장 자격 삭제 ── 배포환경 설정(앱별) ── domain [wl] 도메인 등록 + 워크로드 할당(관리형=즉시/배포중이면 라이브) scale replicas 변경 @@ -147,6 +285,8 @@ env: YAKCLOUD_URL, YAKCLOUD_TOKEN(배포토큰 yakd_…), (선택) YAKCLOUD_CLUS EOF } +load_config # 저장된 자격 로드(env 우선) + case "${1:-help}" in project) shift @@ -158,6 +298,9 @@ case "${1:-help}" in update) shift; cmd_project_update "$@" ;; *) echo "yakcloud project "; 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" ;;