#!/usr/bin/env bash
# kube-up — sema-up 的 k8s 单机两态装配(helm chart 驱动;2026-07-12 一键补完批次)。
#   KUBE_MODE=existing  已有 k8s(kubeconfig 指向;kind/k3d/k3s/云商托管都行)
#   KUBE_MODE=k3s       本机现装 k3s 单机集群(Linux;需要 root/passwordless sudo)
# 多机 ssh 装配(machines.yaml)走 cluster-up.sh,不在本脚本。
# 纪律与 sema-up.sh 相同:幂等 check-then-apply;凭据只生成一次;密码不进日志/argv;
# 凭据经 stdin 给 helm -f -(不落远端、不进 /proc cmdline)。
#
# 输入(sema-up.sh 注入;也可单独跑):
#   WORK(状态目录)PROJECT REGION KUBE_MODE [KUBECONFIG_PATH] [DRY_RUN]
#   USERS REGISTRY_ENABLED GIT_MODE [GIT_URL GIT_TOKEN]
#   SEMA_SERVER_IMAGE(必需)[SEMA_REGISTRY_IMAGE][GITEA_IMAGE]
#   MODEL_GATEWAY_BASEURL MODEL_ID MODEL_API_KEY
#   [NAMESPACE=sema][STORAGE_CLASS=集群默认][CNPG_VERSION]
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
log() { printf '[kube-up] %s\n' "$*" >&2; }
die() { log "⛔ $*"; exit 1; }

: "${WORK:?WORK 未设置(状态目录,一般 ~/.sema-up/<project>)}"
PROJECT="${PROJECT:-sema}"
REGION="${REGION:-global}"
KUBE_MODE="${KUBE_MODE:?KUBE_MODE 未设置(existing|k3s)}"
DRY_RUN="${DRY_RUN:-false}"
USERS="${USERS:-single}"
REGISTRY_ENABLED="${REGISTRY_ENABLED:-true}"
GIT_MODE="${GIT_MODE:-none}"
NAMESPACE="${NAMESPACE:-sema}"
CNPG_VERSION="${CNPG_VERSION:-1.30.0}"   # ⚠ in-tree barman 1.31 移除;升版前先迁 barman-cloud plugin
K3S_CHANNEL="${K3S_CHANNEL:-stable}"

# ── 0. kubeconfig / 工具解析 ─────────────────────────────────────────────────────
if [ "$KUBE_MODE" = "k3s" ]; then
  [ "$(uname -s)" = "Linux" ] || die "--kube=k3s 只支持 Linux(本机装 k3s)。macOS 下一步:用 kind/k3d 起集群后 --kube=existing"
  SUDO=""; [ "$(id -u)" != "0" ] && { sudo -n true 2>/dev/null || die "--kube=k3s 需要 root 或 passwordless sudo(装 k3s 是系统级动作)"; SUDO="sudo"; }
  if ! $SUDO test -f /usr/local/bin/k3s || ! systemctl is-active --quiet k3s 2>/dev/null; then
    if [ "$DRY_RUN" = "true" ]; then log "dry-run:将安装 k3s 单机($K3S_CHANNEL$([ "$REGION" = cn ] && echo ',国内镜像'))"
    else
      log "0/6 安装 k3s 单机($K3S_CHANNEL)"
      MIRROR=""; [ "$REGION" = "cn" ] && MIRROR="INSTALL_K3S_MIRROR=cn "
      curl -sfL https://get.k3s.io | env ${MIRROR}INSTALL_K3S_CHANNEL="$K3S_CHANNEL" $SUDO sh - >/dev/null \
        || die "k3s 安装失败。下一步:看上方安装器输出;网络不通时 --region=cn 走国内镜像"
    fi
  else log "0/6 k3s 已在跑,跳过安装"; fi
  export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
  KUBECTL="$SUDO k3s kubectl"
else
  KC="${KUBECONFIG_PATH:-${KUBECONFIG:-$HOME/.kube/config}}"
  [ -r "$KC" ] || die "kubeconfig 不可读:$KC。下一步:--kubeconfig=<路径> 指向你的集群;没有集群可 --kube=k3s(Linux)或本机 kind create cluster"
  export KUBECONFIG="$KC"
  command -v kubectl >/dev/null || die "未装 kubectl。下一步:https://kubernetes.io/docs/tasks/tools/(macOS:brew install kubectl)"
  KUBECTL="kubectl"
fi
command -v helm >/dev/null || {
  if [ "$KUBE_MODE" = "k3s" ] && [ "$DRY_RUN" != "true" ]; then
    log "装 helm(get-helm-3)"
    curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash >/dev/null || die "helm 安装失败"
  else die "未装 helm。下一步:brew install helm(macOS)/ https://helm.sh/docs/intro/install/"; fi
}

# ── 1. 集群探测(可达/节点数/默认 SC/kata)────────────────────────────────────────
$KUBECTL get nodes >/dev/null 2>&1 || die "集群不可达(kubeconfig=$KUBECONFIG)。下一步:kubectl get nodes 手查;云商集群注意网络/凭据过期"
NODES=$($KUBECTL get nodes --no-headers | grep -cw Ready || true)
[ "$NODES" -ge 1 ] || die "没有 Ready 节点。下一步:kubectl get nodes -o wide 看节点状态"

# ── 节点容量测量+建议(建议不强求;某台确实过小才拦)────────────────────────────────
# allocatable 口径(比 capacity 更接近可用);盘容量 kubectl 不可见,PVC Pending 时按报错查 SC
NODE_CAPS=$($KUBECTL get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.allocatable.cpu}{" "}{.status.allocatable.memory}{"\n"}{end}' 2>/dev/null)
while read -r n_name n_cpu n_mem; do
  [ -z "$n_name" ] && continue
  # cpu:"4"或"3900m"→ 毫核;memory:"8049856Ki"/"7Gi" → MB
  case "$n_cpu" in *m) n_mcpu="${n_cpu%m}" ;; *) n_mcpu=$(( ${n_cpu:-0} * 1000 )) ;; esac
  case "$n_mem" in
    *Ki) n_mb=$(( ${n_mem%Ki} / 1024 )) ;;
    *Mi) n_mb="${n_mem%Mi}" ;;
    *Gi) n_mb=$(( ${n_mem%Gi} * 1024 )) ;;
    *) n_mb=$(( ${n_mem:-0} / 1048576 )) ;;
  esac
  if [ "$n_mcpu" -lt 1000 ] || [ "$n_mb" -lt 2048 ]; then
    die "节点 $n_name 过小(${n_mcpu}m CPU / ${n_mb}MB)——低于 1c/2G 硬底线,确实无法部署。下一步:换/扩这台节点,或用 nodeSelector 把它排除后重跑"
  elif [ "$n_mcpu" -lt 2000 ] || [ "$n_mb" -lt 4096 ]; then
    log "⚠ 节点 $n_name 偏小(${n_mcpu}m CPU / ${n_mb}MB,推荐 ≥2c/4G)——能跑,数据面/沙箱余量紧;建议升配或外接 S3/DB 减负"
  fi
done <<EOF_NODES
$NODE_CAPS
EOF_NODES
[ "$NODES" -lt 3 ] && log "建议:准生产配置=3 节点(PG 反亲和 HA+副本打散);当前 $NODES 节点能跑,按单机形态缩放(不强求)"
DEFAULT_SC=$($KUBECTL get storageclass -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}{"\n"}{end}' 2>/dev/null | awk -F'\t' '$2=="true"{print $1; exit}')
STORAGE_CLASS="${STORAGE_CLASS:-}"   # 空=用集群默认 SC;显式给了就用给的
if [ -z "$STORAGE_CLASS" ] && [ -z "$DEFAULT_SC" ]; then
  die "集群没有默认 StorageClass,PVC 会永久 Pending。下一步:kubectl get sc 看有哪些,然后 STORAGE_CLASS=<名字> 重跑;或给集群设默认 SC"
fi
KATA_OK=false
$KUBECTL get runtimeclass kata-qemu >/dev/null 2>&1 && KATA_OK=true
RUNTIME_CLASS=$($KATA_OK && echo kata-qemu || echo "")
$KATA_OK || log "⚠ 集群无 kata-qemu runtimeclass——沙箱降级为 runc(容器隔离,弱于 VM)。要 VM 隔离:节点装 kata-deploy(见 cluster-up.sh 4/8)后重跑"

# 形态按节点数缩放(cluster-up 同款:<3 preferred,否则 required 永久 Pending——真踩)
PG_INSTANCES=$(( NODES >= 3 ? 3 : NODES ))
PG_AA=$([ "$NODES" -ge 3 ] && echo required || echo preferred)
SERVER_REPLICAS=$(( NODES >= 3 ? 3 : NODES ))

# ── 2. 输入校验(在 dry-run 打印之前,报错带下一步)───────────────────────────────
: "${SEMA_SERVER_IMAGE:?需要 SEMA_SERVER_IMAGE(server 镜像 registry 引用)}"
: "${MODEL_GATEWAY_BASEURL:?模型面必需(--gateway)}"; : "${MODEL_ID:?模型面必需(--model)}"
if [ "$REGISTRY_ENABLED" = "true" ] && [ -z "${SEMA_REGISTRY_IMAGE:-}" ]; then
  die "registry=true(默认)需要 SEMA_REGISTRY_IMAGE。下一步:--registry-image=<仓>/sema-registry:<tag>,或 --registry=false"
fi

ENVF="$WORK/.env"
if [ "$DRY_RUN" = "true" ]; then
  {
    echo "══════ kube-up dry-run(不写文件、不动集群)══════"
    echo " 集群: $NODES 节点(mode=$KUBE_MODE,kubeconfig=$KUBECONFIG)"
    echo " SC: ${STORAGE_CLASS:-默认($DEFAULT_SC)} kata: $KATA_OK(否=沙箱 runc 降级)"
    echo " 形态: pg×$PG_INSTANCES($PG_AA) server×$SERVER_REPLICAS registry=$REGISTRY_ENABLED git=$GIT_MODE"
    echo " 将执行: CNPG operator $CNPG_VERSION → helm upgrade --install sema-stack -n $NAMESPACE"
    echo "        → runner-token 复制 → MinIO 建桶 → $([ "$GIT_MODE" = provision ] && printf 'Gitea 引导(管理员/token/OAuth)→ ')smoke → summary"
    echo " 凭据: $ENVF $([ -f "$ENVF" ] && echo '(已存在:只补缺不轮换)' || echo '(将新建)')"
    echo " 卸载: helm -n $NAMESPACE uninstall sema-stack(数据 PVC 保留;sema-sandbox ns 有 keep 注记)"
    echo "═══════════════════════════════════════════════"
  } >&2
  exit 0
fi

# ── 3. CNPG operator(钉版;server-side apply+重试——聚合 openapi 未就绪的瞬时错)──
log "1/6 CNPG operator $CNPG_VERSION"
if ! $KUBECTL get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
  ok=0
  for t in 1 2 3 4 5; do
    $KUBECTL apply --server-side -f \
      "https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-${CNPG_VERSION%.*}/releases/cnpg-$CNPG_VERSION.yaml" >/dev/null 2>&1 && { ok=1; break; }
    sleep 10
  done
  [ "$ok" = 1 ] || die "CNPG manifest apply 重试耗尽。下一步:raw.githubusercontent.com 可达性(cn 网络可先手动下载后 kubectl apply -f 本地文件)"
fi
$KUBECTL rollout status deployment/cnpg-controller-manager -n cnpg-system --timeout=300s >/dev/null || die "CNPG operator 未就绪。下一步:kubectl -n cnpg-system get pods 看事件"

# ── 4. 凭据(.env 幂等,同 sema-up.sh 纪律)───────────────────────────────────────
mkdir -p "$WORK"; chmod 700 "$WORK" 2>/dev/null || true
touch "$ENVF"; chmod 600 "$ENVF"
setk() { grep -q "^$1=" "$ENVF" || printf '%s=%s\n' "$1" "$2" >> "$ENVF"; }
setk_new() { if grep -q "^$1=" "$ENVF"; then return 1; else printf '%s=%s\n' "$1" "$2" >> "$ENVF"; return 0; fi; }
has_env() { grep -q "^$1=..*" "$ENVF"; }
setk MINIO_PASSWORD "$(openssl rand -hex 24)"
setk SERVICE_AUTH_TOKEN "$(openssl rand -hex 24)"
NEW_REG_PW=false NEW_GIT_PW=false
if [ "$REGISTRY_ENABLED" = "true" ]; then
  setk_new REGISTRY_ADMIN_PASSWORD "$(openssl rand -hex 24)" && NEW_REG_PW=true
  setk REGISTRY_SESSION_SECRET "$(openssl rand -hex 24)"
  setk REGISTRY_PULL_TOKEN "$(openssl rand -hex 24)"
fi
if [ "$GIT_MODE" = "provision" ]; then
  setk GIT_ADMIN_USER "${GIT_ADMIN_USER:-sema}"
  setk_new GIT_ADMIN_PASSWORD "$(openssl rand -hex 24)" && NEW_GIT_PW=true
fi
# openssl 失败会被 $() 吞成空串写进 .env(set -e 不管参数位替换)——断言长度,fail-loud
for k in MINIO_PASSWORD SERVICE_AUTH_TOKEN; do
  grep -q "^$k=.\{32,\}$" "$ENVF" || die "$k 生成失败(openssl rand?)——检查 $ENVF"
done
. "$ENVF"
[ -n "${MODEL_API_KEY:-}" ] || die "需要 MODEL_API_KEY(--model-key 或 -f 配置)"

# 节点 IP(NodePort 入口;kind 场景宿主上不可达,summary 会给 port-forward 替代)
NODE_IP=$($KUBECTL get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' | awk '{print $1}')

# ── 5. helm 装配(凭据经 stdin;两遍:git=provision 时先无 OAuth 起栈,引导完 Gitea
#    再带 OAuth 升级——registry 第一遍已可表单登录,不是半成品)────────────────────
helm_values() { # helm_values <giteaBaseUrl> <oauthId> <oauthSecret>
  V_GITEA_URL="$1" V_OA_ID="$2" V_OA_SECRET="$3" \
  V_MINIO_PW="$MINIO_PASSWORD" V_SVC_TOKEN="$SERVICE_AUTH_TOKEN" V_MODEL_KEY="$MODEL_API_KEY" \
  V_MODEL_GATEWAY="$MODEL_GATEWAY_BASEURL" V_MODEL_ID="$MODEL_ID" \
  V_SERVER_IMAGE="$SEMA_SERVER_IMAGE" V_REPLICAS="$SERVER_REPLICAS" \
  V_PG_INSTANCES="$PG_INSTANCES" V_PG_AA="$PG_AA" V_SC="$STORAGE_CLASS" \
  V_RT_CLASS="$RUNTIME_CLASS" V_REQ_PRINCIPAL="$([ "$USERS" = multi ] && echo true || echo false)" \
  V_PKG_SOURCE="$([ "$REGION" = "global" ] && echo global || echo '')" \
  V_REG_ENABLED="$REGISTRY_ENABLED" V_REG_IMAGE="${SEMA_REGISTRY_IMAGE:-}" \
  V_REG_PW="${REGISTRY_ADMIN_PASSWORD:-}" V_REG_SS="${REGISTRY_SESSION_SECRET:-}" V_REG_PT="${REGISTRY_PULL_TOKEN:-}" \
  V_GITEA_ENABLED="$([ "$GIT_MODE" = provision ] && echo true || echo false)" \
  V_GITEA_IMAGE="${GITEA_IMAGE:-gitea/gitea:1.24}" V_NODE_IP="$NODE_IP" \
  python3 - <<'PY'
import json, os
e = os.environ
# json 是合法 yaml;json.dumps 对任意凭据字符安全(引号/反斜杠/换行都正确转义)
v = {
  # helm 的 values 合并是深合并——只给部分 parameters 时 values.yaml 里 3 机 as-built 的
  # shared_buffers=8GB/effective_cache_size=86GB 会留下来,小集群 PG 直接起不来(内存不够)。
  # 必须整组显式覆盖成保守值;真按机器规格调优的入口是 STORAGE/PG 显式 values(后续档)。
  "pg": {"instances": int(e["V_PG_INSTANCES"]), "antiAffinity": e["V_PG_AA"],
         "storageClass": e["V_SC"], "storageSize": "20Gi",
         "parameters": {"shared_buffers": "256MB", "effective_cache_size": "1GB",
                        "maintenance_work_mem": "128MB", "random_page_cost": "1.1",
                        "effective_io_concurrency": "200", "checkpoint_completion_target": "0.9",
                        "max_parallel_workers": "2"}},
  "minio": {"rootPassword": e["V_MINIO_PW"], "storageClass": e["V_SC"], "storageSize": "20Gi"},
  "server": {
    "image": e["V_SERVER_IMAGE"], "replicas": int(e["V_REPLICAS"]),
    "serviceAuthToken": e["V_SVC_TOKEN"], "modelGatewayBaseurl": e["V_MODEL_GATEWAY"],
    "modelId": e["V_MODEL_ID"], "modelApiKey": e["V_MODEL_KEY"],
    "sandboxPkgSource": e["V_PKG_SOURCE"], "requirePrincipal": e["V_REQ_PRINCIPAL"],
  },
  "sandbox": {"runtimeClass": e["V_RT_CLASS"]},
  "registry": {"enabled": e["V_REG_ENABLED"] == "true", "image": e["V_REG_IMAGE"],
               "storageClass": e["V_SC"], "adminPassword": e["V_REG_PW"],
               "sessionSecret": e["V_REG_SS"], "pullToken": e["V_REG_PT"],
               "giteaBaseUrl": e["V_GITEA_URL"], "giteaOauthClientId": e["V_OA_ID"],
               "giteaOauthClientSecret": e["V_OA_SECRET"]},
  "gitea": {"enabled": e["V_GITEA_ENABLED"] == "true", "image": e["V_GITEA_IMAGE"],
            "storageClass": e["V_SC"], "domain": e["V_NODE_IP"]},
}
print(json.dumps(v))
PY
}
helm_apply() { # helm_apply <giteaBaseUrl> <oauthId> <oauthSecret>
  helm_values "$1" "$2" "$3" | helm upgrade --install sema-stack "$HERE/chart" \
    -n "$NAMESPACE" --create-namespace -f - >/dev/null \
    || die "helm upgrade --install sema-stack 失败。下一步:helm -n $NAMESPACE status sema-stack;kubectl -n $NAMESPACE get events --sort-by=.lastTimestamp | tail"
}
log "2/6 helm 装配 sema-stack(-n $NAMESPACE,pg×$PG_INSTANCES server×$SERVER_REPLICAS registry=$REGISTRY_ENABLED gitea=$([ "$GIT_MODE" = provision ] && echo true || echo false))"
OA_URL="${REGISTRY_GITEA_BASE_URL:-}"; OA_ID="${REGISTRY_GITEA_OAUTH_ID:-}"; OA_SECRET="${REGISTRY_GITEA_OAUTH_SECRET:-}"
# git=connect:先验连通(fail-loud),Gitea 顺手自动建 OAuth app
GIT_MANUAL=""
if [ "$GIT_MODE" = "connect" ]; then
  : "${GIT_URL:?git=connect 需要 GIT_URL}"; : "${GIT_TOKEN:?git=connect 需要 GIT_TOKEN}"
  if printf '%s' "$GIT_URL" | grep -q 'github.com'; then
    C=$(curl -m10 -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $GIT_TOKEN" https://api.github.com/user)
    [ "$C" = "200" ] || die "GitHub token 验证失败(HTTP $C)。下一步:检查 token 有效性/scope"
    GIT_MANUAL="配置中心 OAuth 登录只支持 Gitea——GitHub 仓可用但登录集成无法自动配(表单登录即可)"
  else
    VER=$(curl -m10 -s -H "Authorization: token $GIT_TOKEN" "$GIT_URL/api/v1/version" | python3 -c 'import json,sys
try: print(json.load(sys.stdin).get("version",""))
except Exception: print("")')
    [ -n "$VER" ] || die "$GIT_URL 不像可达的 Gitea(/api/v1/version 无响应)。下一步:核对根地址与 token"
    if [ "$REGISTRY_ENABLED" = "true" ] && [ -z "$OA_ID" ]; then
      REDIRECT="http://$NODE_IP:30300/api/auth/oauth/callback"
      RESP=$(curl -m10 -s -H "Authorization: token $GIT_TOKEN" -X POST "$GIT_URL/api/v1/user/applications/oauth2" \
        -H 'Content-Type: application/json' -d "{\"name\":\"sema-registry\",\"redirect_uris\":[\"$REDIRECT\"],\"confidential_client\":true}")
      OA_ID=$(printf '%s' "$RESP" | python3 -c 'import json,sys
try: print(json.load(sys.stdin).get("client_id",""))
except Exception: print("")')
      OA_SECRET=$(printf '%s' "$RESP" | python3 -c 'import json,sys
try: print(json.load(sys.stdin).get("client_secret",""))
except Exception: print("")')
      if [ -n "$OA_ID" ] && [ -n "$OA_SECRET" ]; then
        setk REGISTRY_GITEA_BASE_URL "$GIT_URL"; setk REGISTRY_GITEA_OAUTH_ID "$OA_ID"; setk REGISTRY_GITEA_OAUTH_SECRET "$OA_SECRET"
        OA_URL="$GIT_URL"
      else GIT_MANUAL="OAuth app 自动创建失败(token 可能缺 write:user)——手动建后把 id/secret 写进 $ENVF 重跑"; fi
    fi
  fi
  setk GIT_URL "$GIT_URL"; setk GIT_TOKEN "$GIT_TOKEN"
fi
helm_apply "$OA_URL" "$OA_ID" "$OA_SECRET"

# runner token 跨 ns 复制要赶在 server 起 pod 前(晚了=CreateContainerConfigError 干等——真踩)
log "3/6 runner-token 复制 + 等数据面"
TOK=""
for t in $(seq 1 24); do
  TOK=$($KUBECTL get secret sema-runner-token -n sema-sandbox -o jsonpath='{.data.token}' 2>/dev/null || true)
  [ -n "$TOK" ] && break; sleep 5
done
[ -n "$TOK" ] || die "sema-runner-token 未被控制器填充。下一步:kubectl -n sema-sandbox get secret,sa"
printf '%s' "$TOK" | base64 -d | $KUBECTL create secret generic sema-runner-token-copy -n "$NAMESPACE" \
  --from-file=token=/dev/stdin --dry-run=client -o yaml | $KUBECTL apply -f - >/dev/null
# PG healthy(冷启拉镜像+init 可要几分钟)
s=""
for t in $(seq 1 120); do
  s=$($KUBECTL get cluster sema-pg -n "$NAMESPACE" -o jsonpath='{.status.phase}' 2>/dev/null || true)
  [ "$s" = "Cluster in healthy state" ] && break; sleep 5
done
[ "$s" = "Cluster in healthy state" ] || { $KUBECTL get cluster -n "$NAMESPACE" >&2 || true; die "PG 未达 healthy。下一步:kubectl -n $NAMESPACE describe cluster sema-pg;PVC Pending 时查 StorageClass"; }
$KUBECTL rollout status deployment/sema-minio -n "$NAMESPACE" --timeout=300s >/dev/null
# MinIO 建桶(幂等;备份桶+对象桶)
$KUBECTL exec deploy/sema-minio -n "$NAMESPACE" -- sh -c \
  "mc alias set local http://localhost:9000 sema \"\$MINIO_ROOT_PASSWORD\" >/dev/null && mc mb --ignore-existing local/sema-pg-backup local/workspaces local/blobs local/backups" >/dev/null \
  || die "MinIO 建桶失败。下一步:kubectl -n $NAMESPACE logs deploy/sema-minio"
$KUBECTL rollout status deployment/sema-server -n "$NAMESPACE" --timeout=600s >/dev/null || die "sema-server 未就绪。下一步:kubectl -n $NAMESPACE logs deploy/sema-server(常见:镜像拉取失败→imagePullSecrets/私仓凭据)"

# ── Gitea 引导(provision:管理员/token/OAuth app;全部 in-pod 动作,不依赖 NodePort 可达)──
if [ "$GIT_MODE" = "provision" ]; then
  log "4/6 Gitea 引导"
  $KUBECTL rollout status deployment/sema-gitea -n "$NAMESPACE" --timeout=300s >/dev/null || die "Gitea 未就绪。下一步:kubectl -n $NAMESPACE logs deploy/sema-gitea"
  . "$ENVF"
  gexec() { $KUBECTL exec deploy/sema-gitea -n "$NAMESPACE" -- su git -c "$*"; }
  if ! gexec "gitea admin user list" 2>/dev/null | awk '{print $2}' | grep -qx "$GIT_ADMIN_USER"; then
    gexec "gitea admin user create --admin --username '$GIT_ADMIN_USER' --password '$GIT_ADMIN_PASSWORD' --email '${GIT_ADMIN_USER}@sema.local' --must-change-password=false" >/dev/null \
      || die "Gitea 管理员创建失败。下一步:kubectl -n $NAMESPACE logs deploy/sema-gitea"
  fi
  if ! has_env GIT_ADMIN_TOKEN; then
    TOK_NAME="sema-up-$(openssl rand -hex 3)"
    GTOK=$($KUBECTL exec deploy/sema-gitea -n "$NAMESPACE" -- \
      curl -m10 -s -u "$GIT_ADMIN_USER:$GIT_ADMIN_PASSWORD" -X POST "http://127.0.0.1:3000/api/v1/users/$GIT_ADMIN_USER/tokens" \
      -H 'Content-Type: application/json' -d "{\"name\":\"$TOK_NAME\",\"scopes\":[\"all\"]}" \
      | python3 -c 'import json,sys
try: print(json.load(sys.stdin).get("sha1",""))
except Exception: print("")')
    [ -n "$GTOK" ] || die "Gitea access token 生成失败。下一步:kubectl -n $NAMESPACE logs deploy/sema-gitea"
    setk GIT_ADMIN_TOKEN "$GTOK"
  fi
  . "$ENVF"
  if [ "$REGISTRY_ENABLED" = "true" ] && ! has_env REGISTRY_GITEA_OAUTH_ID; then
    REDIRECT="http://$NODE_IP:30300/api/auth/oauth/callback"
    RESP=$($KUBECTL exec deploy/sema-gitea -n "$NAMESPACE" -- \
      curl -m10 -s -H "Authorization: token $GIT_ADMIN_TOKEN" -X POST "http://127.0.0.1:3000/api/v1/user/applications/oauth2" \
      -H 'Content-Type: application/json' -d "{\"name\":\"sema-registry\",\"redirect_uris\":[\"$REDIRECT\"],\"confidential_client\":true}")
    OA_ID=$(printf '%s' "$RESP" | python3 -c 'import json,sys
try: print(json.load(sys.stdin).get("client_id",""))
except Exception: print("")')
    OA_SECRET=$(printf '%s' "$RESP" | python3 -c 'import json,sys
try: print(json.load(sys.stdin).get("client_secret",""))
except Exception: print("")')
    if [ -n "$OA_ID" ] && [ -n "$OA_SECRET" ]; then
      setk REGISTRY_GITEA_BASE_URL "http://$NODE_IP:30330"; setk REGISTRY_GITEA_OAUTH_ID "$OA_ID"; setk REGISTRY_GITEA_OAUTH_SECRET "$OA_SECRET"
      log "Gitea→registry OAuth 已接,helm 二遍升级注入"
      helm_apply "http://$NODE_IP:30330" "$OA_ID" "$OA_SECRET"
    else
      GIT_MANUAL="Gitea OAuth app 自动创建失败——registry 仍可表单登录;手动建(回调 $REDIRECT)后把 id/secret 写进 $ENVF 重跑"
    fi
  fi
fi
if [ "$REGISTRY_ENABLED" = "true" ]; then
  $KUBECTL rollout status deployment/sema-registry -n "$NAMESPACE" --timeout=300s >/dev/null || die "registry 未就绪。下一步:kubectl -n $NAMESPACE logs deploy/sema-registry(常见:镜像拉取失败)"
fi

# ── smoke(port-forward 直验——kind/远程 kubeconfig 下 NodePort 对本机不可达,别靠它)──
log "5/6 smoke"
smoke_pf() { # smoke_pf <svc> <port> <path> → http code
  local pf_pid code
  $KUBECTL port-forward "svc/$1" -n "$NAMESPACE" "1$2:$2" >/dev/null 2>&1 &
  pf_pid=$!
  sleep 2
  code=$(curl -m5 -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:1$2$3" || echo 000)
  kill $pf_pid 2>/dev/null; wait $pf_pid 2>/dev/null || true
  printf '%s' "$code"
}
H=$(smoke_pf sema-server 8090 /health)
[ "$H" = "200" ] || die "smoke 失败:server /health=$H(期望 200)。下一步:kubectl -n $NAMESPACE logs deploy/sema-server"
A=$(smoke_pf sema-server 8090 /v1/runs)
[ "$A" = "401" ] || die "smoke 失败:无 token /v1/runs=$A(期望 401=auth 门在)。SERVICE_AUTH_TOKEN 未生效=裸奔,立刻查"
R="n/a"
if [ "$REGISTRY_ENABLED" = "true" ]; then
  R=$(smoke_pf sema-registry 3000 /api/healthz)
  [ "$R" = "200" ] || die "smoke 失败:registry healthz=$R。下一步:kubectl -n $NAMESPACE logs deploy/sema-registry"
fi
G="n/a"
if [ "$GIT_MODE" = "provision" ]; then
  G=$(smoke_pf sema-gitea 3000 /api/healthz)
  [ "$G" = "200" ] || die "smoke 失败:gitea healthz=$G"
fi
log "smoke ✓ health=$H auth=$A registry=$R gitea=$G"

# ── summary(一屏;凭据一次性显示)────────────────────────────────────────────────
log "6/6 完成"
. "$ENVF"
{
  echo
  echo "══════════════ kube-up 完成($NODES 节点,mode=$KUBE_MODE)══════════════"
  echo " 引擎      NodePort 30890 → http://$NODE_IP:30890(API 凭据=SERVICE_AUTH_TOKEN)"
  echo "           本机不可达 NodePort 时(kind/远程):kubectl -n $NAMESPACE port-forward svc/sema-server 8090:8090"
  echo " 数据      CNPG sema-pg×$PG_INSTANCES($PG_AA)+ MinIO(桶:workspaces/blobs/backups/sema-pg-backup)"
  if [ "$REGISTRY_ENABLED" = "true" ]; then
    echo " 配置中心  NodePort 30300 → http://$NODE_IP:30300"
    if $NEW_REG_PW; then echo "           首登:${REGISTRY_ADMIN_USER:-zhaoheran} / ${REGISTRY_ADMIN_PASSWORD}(⚠ 只显示这一次,之后看 .env)"
    else echo "           首登:${REGISTRY_ADMIN_USER:-zhaoheran}(密码沿用上次,在 .env REGISTRY_ADMIN_PASSWORD)"; fi
  fi
  if [ "$GIT_MODE" = "provision" ]; then
    echo " Git/Gitea NodePort 30330(http)/30322(ssh)→ http://$NODE_IP:30330"
    if $NEW_GIT_PW; then echo "           管理员:${GIT_ADMIN_USER} / ${GIT_ADMIN_PASSWORD}(⚠ 只显示这一次,之后看 .env)"
    else echo "           管理员:${GIT_ADMIN_USER}(密码在 .env GIT_ADMIN_PASSWORD)"; fi
  elif [ "$GIT_MODE" = "connect" ]; then
    echo " Git       已连接 ${GIT_URL}(token 在 .env GIT_TOKEN)"
  fi
  [ -n "$GIT_MANUAL" ] && echo " ⚠ 待人工  $GIT_MANUAL"
  [ "$RUNTIME_CLASS" = "" ] && echo " ⚠ 沙箱    无 kata——runc 容器隔离(生产建议装 kata-deploy 后重跑)"
  echo " 凭据      $ENVF(0600)"
  echo " 卸载      helm -n $NAMESPACE uninstall sema-stack(PVC/数据留);CNPG operator/k3s 另卸"
  echo "═══════════════════════════════════════════════════════════════════════"
} >&2
