Files
opsxsql/kubernetes/install.sh
T
opsx 396df57c9c build+deploy: 编译加 -trimpath,镜像改 lghost/opsxsql,K8s 部署到 sql 命名空间
1. -trimpath(抹除源码绝对路径)
   - Dockerfile: go build 加 -trimpath
   - run.sh: 两处 go build 加 -trimpath
   效果:二进制内 /app/backend/ 绝对路径命中 66 → 0(module 相对路径 sql-platform/ 仍保留,
        那是 Go 的正常行为,-trimpath 只去绝对前缀)

2. 镜像
   - kubernetes/deployment.yaml: opsxsql:latest → lghost/opsxsql:latest
   - kubernetes/install.sh: 默认 IMAGE 改 lghost/opsxsql:latest
   - 已构建并推送:lghost/opsxsql:latest (digest 70562a408cae, 25.2MB)

3. K8s 部署验证(namespace: sql)
   - Deployment opsxsql 1/1 Running,0 重启
   - Pod: opsxsql-695444d6b9-chrzp (node 172.19.0.161)
   - Service opsxsql.sql.svc.cluster.local:80
   - PVC opsxsql-data Bound 10Gi (cbs)
   - Secret opsxsql-secrets 自动生成(SECRET_KEY / JWT_SECRET)
   - /health → {"backend":"go","status":"ok","version":"0.4.0"}
   - 首页正常,seed 完成(26 条审核规则 + LDAP 默认配置)

部署命令:bash kubernetes/install.sh
2026-10-26 17:22:29 +00:00

345 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# opsxsql 一键部署(参照 opsx-loki 最新 install.sh)
#
# 命名空间固定:sql
# 自动适配:阿里云 ACK / 腾讯云 TKE / AWS EKS / 华为云 CCE / GCP GKE / Azure AKS / K3s / 自建
# 幂等:重复执行不覆盖密钥、不重建数据卷
#
# 用法:
# bash kubernetes/install.sh # 自动探测,默认镜像 opsxsql:latest
# IMAGE=registry.example.com/opsxsql:v1 bash kubernetes/install.sh
# WITH_NODEPORT=1 bash kubernetes/install.sh # 同时开 NodePort(:30900)
# bash kubernetes/install.sh --nodeport # 等价于上一行
# bash kubernetes/install.sh --ingress opsxsql.example.com # 通用 nginx Ingress
# STORAGE_CLASS=local-path PVC_SIZE=20Gi bash kubernetes/install.sh # 命名空间固定 sql
# ============================================================================
set -euo pipefail
NS="sql"
# 命名空间固定 sql(如需修改请改此变量与 kubernetes/*.yaml)
IMAGE="${IMAGE:-lghost/opsxsql:latest}"
SECRET_NAME="opsxsql-secrets"
WITH_NODEPORT="${WITH_NODEPORT:-0}"
INGRESS_HOST=""
STORAGE_CLASS="${STORAGE_CLASS:-}"
PVC_SIZE="${PVC_SIZE:-10Gi}"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
info() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m✅\33[0m %s\n' "$*"; }
warn() { printf '\033[1;33m⚠️ \33[0m %s\n' "$*"; }
die() { printf '\033[1;31m❌\33[0m %s\n' "$*" >&2; exit 1; }
# ---- 参数解析(兼容 flags 与环境变量) ----
while [[ $# -gt 0 ]]; do
case "$1" in
--nodeport) WITH_NODEPORT=1; shift ;;
--ingress) INGRESS_HOST="${2:?--ingress 需要域名参数}"; shift 2 ;;
-h|--help) sed -n '2,17p' "$0"; exit 0 ;;
*) die "未知参数: $1(看 --help)" ;;
esac
done
# ============================================================================
# 0) 前置检查
# ============================================================================
command -v kubectl >/dev/null 2>&1 || die "未找到 kubectl,请先安装"
kubectl version --request-timeout=5s >/dev/null 2>&1 || die "kubectl 无法连接集群,请检查 kubeconfig"
# ============================================================================
# 0.1) 云平台识别(信息展示 + 失败诊断用)
# ============================================================================
detect_cloud() {
local pid=""
pid=$(kubectl get nodes -o jsonpath='{.items[0].spec.providerID}' 2>/dev/null || true)
case "${pid}" in
qcloud://*) echo "腾讯云 TKE" ;;
aws://*) echo "AWS EKS" ;;
aliyun://*) echo "阿里云 ACK" ;;
huaweicloud*|huawei*) echo "华为云 CCE" ;;
gce://*) echo "GCP GKE" ;;
azure://*) echo "Azure AKS" ;;
k3s://*) echo "K3s(自建)" ;;
*) echo "自建 / 其他 K8s" ;;
esac
}
# ============================================================================
# 0.2) StorageClass 探测
# 优先级:显式 STORAGE_CLASS > default 注解 > 唯一 SC > 失败
# 结果写入全局 RESOLVED_SC / SC_ERROR(不要用 $() 调用,子 shell 赋值不回传)
# ============================================================================
RESOLVED_SC=""
SC_ERROR=""
resolve_storage_class() {
local sc="" all="" count=0
RESOLVED_SC=""; SC_ERROR=""
if [ -n "${STORAGE_CLASS}" ]; then
if ! kubectl get sc "${STORAGE_CLASS}" >/dev/null 2>&1; then
SC_ERROR="指定的 StorageClass '${STORAGE_CLASS}' 不存在。现有:$(kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}' 2>/dev/null)"
return 1
fi
RESOLVED_SC="${STORAGE_CLASS}"; return 0
fi
sc=$(kubectl get sc -o jsonpath='{range .items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")]}{.metadata.name}{"\n"}{end}' 2>/dev/null | head -1 || true)
if [ -n "${sc}" ]; then RESOLVED_SC="${sc}"; return 0; fi
all=$(kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
count=$(printf '%s' "${all}" | grep -c . || true)
if [ "${count}" = "1" ]; then RESOLVED_SC="${all}"; return 0; fi
return 1
}
# ============================================================================
# 0.3) 自建集群兜底:无任何 StorageClass 时创建「静态 local PV」(零外部依赖)
# hostPath + DirectoryOrCreate + nodeAffinity 锁节点 + WaitForFirstConsumer + Retain
# ============================================================================
LOCAL_SC_NAME="opsxsql-local"
LOCAL_PV_NAME="opsxsql-data-pv"
LOCAL_PV_SIZE="${PVC_SIZE}"
LOCAL_PV_PATH="${PV_PATH:-/var/lib/opsxsql-data}"
pick_local_node() {
if [ -n "${NODE_NAME:-}" ]; then echo "${NODE_NAME}"; return 0; fi
kubectl get nodes --no-headers \
-o custom-columns=NAME:.metadata.name,READY:.status.conditions[-1].status 2>/dev/null \
| awk '$2=="True"{print $1; exit}'
}
create_local_storage() {
local node
node="$(pick_local_node)"
[ -n "${node}" ] || { warn "找不到 Ready 节点,无法创建本地存储"; return 1; }
info "创建节点本地存储:节点=${node} 路径=${LOCAL_PV_PATH} 容量=${LOCAL_PV_SIZE}"
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${LOCAL_SC_NAME}
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
EOF
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: v1
kind: PersistentVolume
metadata:
name: ${LOCAL_PV_NAME}
labels:
app: opsxsql
spec:
capacity:
storage: ${LOCAL_PV_SIZE}
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: ${LOCAL_SC_NAME}
hostPath:
path: ${LOCAL_PV_PATH}
type: DirectoryOrCreate
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${node}
EOF
ok "已创建 StorageClass ${LOCAL_SC_NAME} + PV ${LOCAL_PV_NAME}(节点 ${node})"
warn "注意:数据固定在该节点 ${LOCAL_PV_PATH},Pod 不会漂移到其他节点"
RESOLVED_SC="${LOCAL_SC_NAME}"
return 0
}
CLOUD="$(detect_cloud)"
info "opsxsql 部署开始(namespace: ${NS},镜像 ${IMAGE})"
info "集群: $(kubectl config current-context 2>/dev/null || echo unknown)"
info "平台: ${CLOUD}"
# LOCAL_STORAGE 三态:auto(默认,无 SC 时自动建) / off(只提示) / force(强制建)
LOCAL_STORAGE="${LOCAL_STORAGE:-auto}"
if [ "${LOCAL_STORAGE}" = "force" ]; then
warn "LOCAL_STORAGE=force:强制使用节点本地存储"
create_local_storage || die "创建节点本地存储失败"
elif ! resolve_storage_class; then
if [ -n "${SC_ERROR}" ]; then
die "${SC_ERROR}"
fi
echo ""
if [ "${LOCAL_STORAGE}" = "auto" ]; then
warn "集群没有可用 StorageClass(自建集群常见)→ 自动启用节点本地存储"
create_local_storage || die "创建节点本地存储失败"
else
warn "未找到可用的 StorageClass,且 LOCAL_STORAGE=off"
echo ""
echo " 现有 StorageClass:"
kubectl get sc 2>/dev/null | sed 's/^/ /' || echo " (无)"
echo ""
echo " 解决办法(选一个):"
echo " 1) 手动指定(推荐,若已有可用 SC):"
echo " STORAGE_CLASS=<sc名称> bash kubernetes/install.sh"
echo " 2) 标记某个 SC 为默认:"
echo " kubectl patch sc <sc名称> -p '{\"metadata\":{\"annotations\":{\"storageclass.kubernetes.io/is-default-class\":\"true\"}}}'"
echo " 3) 自建集群安装 local-path-provisioner:"
echo " kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml"
echo " STORAGE_CLASS=local-path bash kubernetes/install.sh"
echo ""
die "StorageClass 未就绪,部署中止(避免 PVC 永久 Pending)"
fi
else
ok "StorageClass: ${RESOLVED_SC}"
fi
# ============================================================================
# 1) 命名空间
# ============================================================================
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: v1
kind: Namespace
metadata:
name: ${NS}
EOF
# ============================================================================
# 2) Secret(幂等:已存在则跳过,绝不覆盖 —— SECRET_KEY 换了已加密数据不可解)
# ============================================================================
rand_hex() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
elif [ -c /dev/urandom ]; then
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
else
die "无法生成随机密钥(缺 openssl 且无 /dev/urandom)"
fi
}
if kubectl -n "${NS}" get secret "${SECRET_NAME}" >/dev/null 2>&1; then
info "Secret ${SECRET_NAME} 已存在,跳过(不覆盖现有密钥)"
else
info "生成随机密钥 SECRET_KEY / JWT_SECRET(丢失则已加密的实例/LDAP 密码无法解密,请自行备份)"
kubectl -n "${NS}" create secret generic "${SECRET_NAME}" \
--from-literal=SECRET_KEY="$(rand_hex)" \
--from-literal=JWT_SECRET="$(rand_hex)" >/dev/null
ok "Secret 已创建"
fi
# ============================================================================
# 3) 数据卷(渲染注入探测到的 StorageClass 与容量)
# ============================================================================
RENDER_DIR="$(mktemp -d)"
trap 'rm -rf "${RENDER_DIR}"' EXIT
awk -v sc="${RESOLVED_SC}" -v size="${PVC_SIZE}" '
/^spec:/ && !done { print; print " storageClassName: " sc; done=1; next }
/storage:/ && !sized { $0=" storage: " size; sized=1 }
{ print }
' "${DIR}/pvc.yaml" > "${RENDER_DIR}/pvc.yaml"
kubectl apply -f "${RENDER_DIR}/pvc.yaml"
# ============================================================================
# 4) 工作负载 + Service(镜像渲染进 deployment)
# ============================================================================
sed "s|image: opsxsql:latest.*|image: ${IMAGE}|" "${DIR}/deployment.yaml" > "${RENDER_DIR}/deployment.yaml"
kubectl apply -f "${RENDER_DIR}/deployment.yaml"
kubectl apply -f "${DIR}/service.yaml"
if [ "${WITH_NODEPORT}" = "1" ]; then
kubectl apply -f "${DIR}/service-nodeport.yaml"
ok "NodePort Service 已创建(:30900)"
fi
if [ -n "${INGRESS_HOST}" ]; then
sed "s|host: opsxsql.example.com.*|host: ${INGRESS_HOST}|" "${DIR}/ingress.yaml" > "${RENDER_DIR}/ingress.yaml"
kubectl apply -f "${RENDER_DIR}/ingress.yaml"
ok "Ingress 已创建(${INGRESS_HOST},nginx class;其他云参照 ${DIR}/examples/)"
fi
# ============================================================================
# 5) 等待就绪(失败时按云诊断)
# ============================================================================
info "等待 Deployment 就绪(最多 180 秒)..."
if ! kubectl -n "${NS}" rollout status deployment/opsxsql --timeout=180s; then
echo ""
warn "部署未在 180 秒内就绪,开始诊断:"
echo ""
kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o wide 2>/dev/null | sed 's/^/ /'
REASON=$(kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o jsonpath='{.items[0].status.containerStatuses[0].state.waiting.reason}' 2>/dev/null || true)
echo ""
case "${REASON}" in
ImagePullBackOff|ErrImagePull)
warn "镜像拉取失败(${REASON})"
echo " → 当前镜像: ${IMAGE}"
echo " → 该节点拉不到该仓库。改为目标云可拉的仓库后重跑:"
echo " 腾讯云 TCR : ccr.ccs.tencentyun.com/<ns>/opsxsql:latest"
echo " 阿里云 ACR : registry.cn-hangzhou.aliyuncs.com/<ns>/opsxsql:latest"
echo " 华为云 SWR : swr.cn-north-4.myhuaweicloud.com/<ns>/opsxsql:latest"
echo " 自建 Harbor: harbor.your.domain/library/opsxsql:latest"
echo " 示例: IMAGE=registry.cn-hangzhou.aliyuncs.com/<ns>/opsxsql:latest bash kubernetes/install.sh"
;;
CreateContainerConfigError)
warn "容器配置缺失(通常是 Secret 不存在)"
echo " → 检查: kubectl -n ${NS} get secret ${SECRET_NAME}"
;;
"")
PVC_PHASE=$(kubectl -n "${NS}" get pvc opsxsql-data -o jsonpath='{.status.phase}' 2>/dev/null || echo unknown)
if [ "${PVC_PHASE}" != "Bound" ]; then
warn "PVC 未绑定(phase=${PVC_PHASE})"
echo " → 检查 StorageClass: kubectl -n ${NS} describe pvc opsxsql-data"
echo " → ${CLOUD} 建议的 SC:"
case "${CLOUD}" in
腾讯云*) echo " cbs" ;;
AWS*) echo " gp3 / gp2" ;;
阿里云*) echo " alicloud-disk-essd" ;;
华为云*) echo " csi-disk" ;;
*) echo " local-path(需装 local-path-provisioner)" ;;
esac
fi
;;
esac
echo ""
kubectl -n "${NS}" describe po -l app.kubernetes.io/name=opsxsql 2>/dev/null | tail -15 | sed 's/^/ /'
die "部署未完成,请按上面提示处理"
fi
ok "Deployment 已就绪"
# ============================================================================
# 6) 输出访问信息
# ============================================================================
POD=$(kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "-")
POD_IP=$(kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o jsonpath='{.items[0].status.podIP}' 2>/dev/null || echo "-")
NODE=$(kubectl -n "${NS}" get po -l app.kubernetes.io/name=opsxsql -o jsonpath='{.items[0].spec.nodeName}' 2>/dev/null || echo "-")
echo ""
echo "════════════════════════════════════════════════════════"
ok "opsxsql 部署完成"
echo "════════════════════════════════════════════════════════"
echo " 平台 : ${CLOUD}"
echo " Namespace : ${NS}"
echo " 镜像 : ${IMAGE}"
echo " 存储类 : ${RESOLVED_SC}"
echo " Pod : ${POD} (node=${NODE} ip=${POD_IP})"
echo " Service : opsxsql.${NS}.svc.cluster.local:80"
[ "${WITH_NODEPORT}" = "1" ] && echo " NodePort : <任意节点IP>:30900"
[ -n "${INGRESS_HOST}" ] && echo " Ingress : http://${INGRESS_HOST}"
echo ""
echo " 初始账号 : admin / admin123 ← 首次登录请立即改密"
echo ""
echo " 本地访问 : kubectl -n ${NS} port-forward svc/opsxsql 9000:80"
echo " 浏览器打开 http://localhost:9000"
echo ""
echo " 数据备份 : kubectl -n ${NS} exec deploy/opsxsql -- sqlite3 /app/data/sqlite3.db '.backup /app/data/backup.db'"
echo " 配 Ingress(可选):参照 ${DIR}/examples/ 按云选模板"
echo "════════════════════════════════════════════════════════"