- deployment/quick-deploy: PVC 挂载 sqlite3.db/redka.db 到 /app/,含旧 data/ 迁移 - configmap 与 run.sh 状态输出对齐新路径 - README 补充 local 模式说明
1295 lines
44 KiB
Bash
Executable File
1295 lines
44 KiB
Bash
Executable File
#!/bin/bash
|
||
#===========================================================================
|
||
# AIOps 智能运维平台 - 统一启动脚本
|
||
# 支持三种部署方式 + 本地开发模式:
|
||
#
|
||
# 方式 1 — Docker Compose:
|
||
# ./run.sh docker up 构建+启动 (MySQL+Redis+Backend+Nginx)
|
||
#
|
||
# 方式 2 — Kubernetes (embed 单容器):
|
||
# ./run.sh kubernetes deploy 构建镜像+全量部署 (embed 前端嵌入后端)
|
||
# ./run.sh kubernetes upgrade 重新构建+滚动更新
|
||
# ./run.sh kubernetes status 查看资源状态
|
||
# ./run.sh kubernetes logs/exec 日志/进入容器
|
||
#
|
||
# 方式 3 — 独立二进制 (默认 --mode local, SQLite + redka):
|
||
# ./run.sh build 编译独立二进制 (embed 前端)
|
||
# ./run.sh binary upgrade 停服 → 清旧产物 → 重编译 → 后台启动
|
||
# ./run.sh binary start|stop|status
|
||
#
|
||
# 本地开发:
|
||
# ./run.sh dev 前后端热重载
|
||
# ./run.sh help 查看完整帮助
|
||
#===========================================================================
|
||
set -e
|
||
|
||
# ============================================================
|
||
# 配置区
|
||
# ============================================================
|
||
PROJECT_NAME="AIOps 智能运维平台"
|
||
BACKEND_PORT=8080
|
||
FRONTEND_PORT=5173
|
||
BACKEND_DIR="backend"
|
||
FRONTEND_DIR="frontend"
|
||
BACKEND_BIN="aiops-server"
|
||
STANDALONE_BIN="aiops"
|
||
|
||
# 日志配置
|
||
MAX_LOG_FILES=3
|
||
MAX_LOG_SIZE=$((50 * 1024 * 1024)) # 50MB
|
||
|
||
# ============================================================
|
||
# 通用函数
|
||
# ============================================================
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
MAGENTA='\033[0;35m'
|
||
NC='\033[0m'
|
||
|
||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||
warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||
error() { echo -e "${RED}[ERR]${NC} $1"; }
|
||
banner() { echo -e "${CYAN}$1${NC}"; }
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
cd "$SCRIPT_DIR"
|
||
|
||
# PID 文件
|
||
BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid"
|
||
FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.pid"
|
||
STANDALONE_BIN_PATH="$SCRIPT_DIR/$STANDALONE_BIN"
|
||
BINARY_PID_FILE="$SCRIPT_DIR/.aiops.pid"
|
||
BINARY_ARGS_FILE="$SCRIPT_DIR/.aiops.args"
|
||
LOGS_DIR="$SCRIPT_DIR/logs"
|
||
|
||
# 检测操作系统
|
||
detect_os() {
|
||
case "$(uname -s)" in
|
||
Darwin) echo "macos" ;;
|
||
Linux) echo "linux" ;;
|
||
*) echo "unknown" ;;
|
||
esac
|
||
}
|
||
OS=$(detect_os)
|
||
|
||
# 检查端口是否被占用
|
||
check_port() {
|
||
if [ "$OS" = "macos" ]; then
|
||
lsof -ti:$1 2>/dev/null | head -1
|
||
else
|
||
ss -tlnp 2>/dev/null | grep -q ":$1 " && echo "in-use" || echo ""
|
||
fi
|
||
}
|
||
|
||
# 释放端口
|
||
kill_port() {
|
||
local port=$1
|
||
if [ "$OS" = "macos" ]; then
|
||
local pids=$(lsof -ti:$port 2>/dev/null || echo "")
|
||
[ -n "$pids" ] && echo "$pids" | xargs kill -9 2>/dev/null || true
|
||
else
|
||
fuser -k ${port}/tcp 2>/dev/null || true
|
||
fi
|
||
sleep 0.5
|
||
}
|
||
|
||
# 日志轮转
|
||
rotate_log() {
|
||
local log_file=$1
|
||
[ ! -f "$log_file" ] && return 0
|
||
|
||
local file_size
|
||
if [ "$OS" = "macos" ]; then
|
||
file_size=$(stat -f%z "$log_file" 2>/dev/null || echo "0")
|
||
else
|
||
file_size=$(stat -c%s "$log_file" 2>/dev/null || echo "0")
|
||
fi
|
||
|
||
if [ "$file_size" -gt "$MAX_LOG_SIZE" ]; then
|
||
[ -f "${log_file}.${MAX_LOG_FILES}" ] && rm -f "${log_file}.${MAX_LOG_FILES}"
|
||
for i in $(seq $((MAX_LOG_FILES - 1)) -1 1); do
|
||
[ -f "${log_file}.${i}" ] && mv "${log_file}.${i}" "${log_file}.$((i + 1))"
|
||
done
|
||
mv "$log_file" "${log_file}.1"
|
||
info "日志已轮转: $log_file"
|
||
fi
|
||
}
|
||
|
||
# ============================================================
|
||
# 后端服务(本地开发模式)
|
||
# ============================================================
|
||
start_backend() {
|
||
ensure_go_path || return 1
|
||
info "启动后端服务..."
|
||
cd "$SCRIPT_DIR/$BACKEND_DIR"
|
||
mkdir -p logs
|
||
rotate_log "logs/server.log"
|
||
|
||
kill_port $BACKEND_PORT
|
||
|
||
export GIN_MODE=debug
|
||
|
||
info "编译 Go 程序..."
|
||
CGO_ENABLED=1 go build -o "$BACKEND_BIN" . 2>&1 || {
|
||
error "编译失败"
|
||
return 1
|
||
}
|
||
|
||
nohup ./"$BACKEND_BIN" >> logs/server.log 2>&1 &
|
||
echo $! > "$BACKEND_PID_FILE"
|
||
|
||
# 等待服务启动
|
||
local retries=0
|
||
while ! curl -s "http://localhost:$BACKEND_PORT/health" > /dev/null 2>&1; do
|
||
retries=$((retries + 1))
|
||
if [ $retries -ge 30 ]; then
|
||
error "后端启动超时,查看日志: $BACKEND_DIR/logs/server.log"
|
||
tail -20 logs/server.log 2>/dev/null
|
||
return 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
success "后端已启动 (PID: $(cat "$BACKEND_PID_FILE"), 端口: $BACKEND_PORT)"
|
||
return 0
|
||
}
|
||
|
||
stop_backend() {
|
||
if [ -f "$BACKEND_PID_FILE" ]; then
|
||
local pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || echo "")
|
||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||
kill "$pid" 2>/dev/null || true
|
||
success "后端已停止"
|
||
fi
|
||
rm -f "$BACKEND_PID_FILE"
|
||
fi
|
||
kill_port $BACKEND_PORT
|
||
}
|
||
|
||
# ============================================================
|
||
# 前端服务(本地开发模式)
|
||
# ============================================================
|
||
start_frontend() {
|
||
info "启动前端服务..."
|
||
cd "$SCRIPT_DIR/$FRONTEND_DIR"
|
||
mkdir -p logs
|
||
rotate_log "logs/dev.log"
|
||
|
||
kill_port $FRONTEND_PORT
|
||
|
||
if [ ! -d "node_modules" ]; then
|
||
warning "安装前端依赖..."
|
||
npm install
|
||
fi
|
||
|
||
nohup npm run dev >> logs/dev.log 2>&1 &
|
||
echo $! > "$FRONTEND_PID_FILE"
|
||
|
||
local retries=0
|
||
while ! curl -s "http://localhost:$FRONTEND_PORT" > /dev/null 2>&1; do
|
||
retries=$((retries + 1))
|
||
if [ $retries -ge 30 ]; then
|
||
error "前端启动超时,查看日志: $FRONTEND_DIR/logs/dev.log"
|
||
tail -20 logs/dev.log 2>/dev/null
|
||
return 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
success "前端已启动 (PID: $(cat "$FRONTEND_PID_FILE"), 端口: $FRONTEND_PORT)"
|
||
return 0
|
||
}
|
||
|
||
stop_frontend() {
|
||
if [ -f "$FRONTEND_PID_FILE" ]; then
|
||
local pid=$(cat "$FRONTEND_PID_FILE" 2>/dev/null || echo "")
|
||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||
kill "$pid" 2>/dev/null || true
|
||
success "前端已停止"
|
||
fi
|
||
rm -f "$FRONTEND_PID_FILE"
|
||
fi
|
||
kill_port $FRONTEND_PORT
|
||
}
|
||
|
||
# ============================================================
|
||
# 本地开发命令
|
||
# ============================================================
|
||
cmd_dev() {
|
||
banner "========== $PROJECT_NAME (本地开发) =========="
|
||
|
||
# 检查依赖
|
||
if ! command -v go &> /dev/null; then
|
||
error "Go 未安装,请先安装 Go 1.21+"
|
||
exit 1
|
||
fi
|
||
if ! command -v npm &> /dev/null; then
|
||
error "npm 未安装,请先安装 Node.js 20+"
|
||
exit 1
|
||
fi
|
||
|
||
if start_backend && start_frontend; then
|
||
echo ""
|
||
banner "========== 所有服务已启动 =========="
|
||
info "前端地址: http://localhost:$FRONTEND_PORT"
|
||
info "后端地址: http://localhost:$BACKEND_PORT"
|
||
info "健康检查: http://localhost:$BACKEND_PORT/health"
|
||
info "数据库: SQLite (./sqlite3.db)"
|
||
echo ""
|
||
info "使用 './run.sh stop' 停止服务"
|
||
else
|
||
error "服务启动失败"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
cmd_stop() {
|
||
info "停止本地服务..."
|
||
stop_backend
|
||
stop_frontend
|
||
success "所有本地服务已停止"
|
||
}
|
||
|
||
cmd_restart() {
|
||
cmd_stop
|
||
sleep 1
|
||
cmd_dev
|
||
}
|
||
|
||
cmd_status() {
|
||
info "本地服务状态:"
|
||
if [ -n "$(check_port $BACKEND_PORT)" ]; then
|
||
success "后端: 运行中 (端口 $BACKEND_PORT)"
|
||
else
|
||
warning "后端: 未运行"
|
||
fi
|
||
if [ -n "$(check_port $FRONTEND_PORT)" ]; then
|
||
success "前端: 运行中 (端口 $FRONTEND_PORT)"
|
||
else
|
||
warning "前端: 未运行"
|
||
fi
|
||
if [ -f "$SCRIPT_DIR/sqlite3.db" ]; then
|
||
local db_size=$(du -h "$SCRIPT_DIR/sqlite3.db" | cut -f1)
|
||
info "数据库: SQLite (./sqlite3.db, $db_size)"
|
||
elif [ -f "$SCRIPT_DIR/$BACKEND_DIR/sqlite3.db" ]; then
|
||
local db_size=$(du -h "$SCRIPT_DIR/$BACKEND_DIR/sqlite3.db" | cut -f1)
|
||
info "数据库: SQLite (backend/sqlite3.db 旧路径, $db_size)"
|
||
fi
|
||
}
|
||
|
||
cmd_logs() {
|
||
info "实时日志 (Ctrl+C 退出):"
|
||
echo ""
|
||
local pids=()
|
||
if [ -f "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" ]; then
|
||
echo "=== 后端日志 ==="
|
||
tail -f "$SCRIPT_DIR/$BACKEND_DIR/logs/server.log" &
|
||
pids+=($!)
|
||
fi
|
||
if [ -f "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" ]; then
|
||
echo "=== 前端日志 ==="
|
||
tail -f "$SCRIPT_DIR/$FRONTEND_DIR/logs/dev.log" &
|
||
pids+=($!)
|
||
fi
|
||
trap "kill ${pids[*]} 2>/dev/null; exit 0" INT TERM
|
||
wait
|
||
}
|
||
|
||
# ============================================================
|
||
# 构建清理(三种部署方式共用,避免旧代码/旧静态资源残留)// Logan.Li
|
||
# ============================================================
|
||
clean_frontend_artifacts() {
|
||
info "清理前端旧构建产物..."
|
||
rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/dist"
|
||
rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/node_modules/.vite"
|
||
}
|
||
|
||
clean_embed_static() {
|
||
info "清理 embed static 旧文件..."
|
||
mkdir -p "$SCRIPT_DIR/$BACKEND_DIR/static"
|
||
find "$SCRIPT_DIR/$BACKEND_DIR/static" -mindepth 1 -delete 2>/dev/null || rm -rf "$SCRIPT_DIR/$BACKEND_DIR/static"/*
|
||
}
|
||
|
||
clean_go_artifacts() {
|
||
info "清理 Go 旧编译产物..."
|
||
rm -f "$STANDALONE_BIN_PATH"
|
||
rm -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN"
|
||
rm -f "$SCRIPT_DIR/$BACKEND_DIR/$BACKEND_BIN"
|
||
}
|
||
|
||
# 构建前端并同步到 backend/static(二进制 / embed 镜像共用)// Logan.Li
|
||
build_frontend_embed() {
|
||
clean_frontend_artifacts
|
||
cd "$SCRIPT_DIR/$FRONTEND_DIR"
|
||
if [ ! -d "node_modules" ]; then
|
||
npm install --registry=https://registry.npmmirror.com
|
||
fi
|
||
npm run build 2>&1 | tail -8
|
||
cd "$SCRIPT_DIR"
|
||
clean_embed_static
|
||
cp -r "$FRONTEND_DIR/dist/"* "$BACKEND_DIR/static/"
|
||
local fe_files
|
||
fe_files=$(find "$BACKEND_DIR/static" -type f | wc -l | tr -d ' ')
|
||
success "前端已嵌入 static/ ($fe_files 个文件)"
|
||
}
|
||
|
||
# 停止独立二进制进程(PID 文件 + 端口双保险)// Logan.Li
|
||
stop_binary_process() {
|
||
local port=${1:-8080}
|
||
if [ -f "$BINARY_PID_FILE" ]; then
|
||
local pid
|
||
pid=$(cat "$BINARY_PID_FILE" 2>/dev/null || echo "")
|
||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||
kill "$pid" 2>/dev/null || true
|
||
sleep 1
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
success "已停止二进制进程 (PID: $pid)"
|
||
fi
|
||
rm -f "$BINARY_PID_FILE"
|
||
fi
|
||
# 兜底:按端口和进程名清理残留
|
||
if [ "$OS" = "macos" ]; then
|
||
local pids
|
||
pids=$(lsof -ti:"$port" 2>/dev/null || echo "")
|
||
[ -n "$pids" ] && echo "$pids" | xargs kill -9 2>/dev/null || true
|
||
else
|
||
fuser -k "${port}/tcp" 2>/dev/null || true
|
||
pkill -f "${STANDALONE_BIN_PATH}" 2>/dev/null || true
|
||
pkill -f "${SCRIPT_DIR}/${BACKEND_DIR}/${STANDALONE_BIN}" 2>/dev/null || true
|
||
pkill -f "./${STANDALONE_BIN} --port" 2>/dev/null || true
|
||
fi
|
||
sleep 0.5
|
||
}
|
||
|
||
wait_binary_ready() {
|
||
local port=${1:-8080}
|
||
local retries=0
|
||
while ! curl -sf "http://localhost:${port}/version" > /dev/null 2>&1; do
|
||
retries=$((retries + 1))
|
||
if [ $retries -ge 30 ]; then
|
||
error "二进制启动超时,查看日志: $LOGS_DIR/aiops.log"
|
||
tail -20 "$LOGS_DIR/aiops.log" 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# 确保二进制默认 config.yaml 存在 // Logan.Li
|
||
ensure_binary_config() {
|
||
local cfg="$SCRIPT_DIR/config.yaml"
|
||
if [ -f "$cfg" ]; then
|
||
return 0
|
||
fi
|
||
info "生成默认 config.yaml (local 模式: SQLite + redka 本地缓存)..."
|
||
cat > "$cfg" << 'YAML'
|
||
mode: "local"
|
||
port: "8080"
|
||
log_level: "info"
|
||
|
||
database:
|
||
driver: "sqlite"
|
||
host: "localhost"
|
||
port: 3306
|
||
user: ""
|
||
password: ""
|
||
dbname: "sqlite3.db"
|
||
charset: "utf8mb4"
|
||
max_conn: 100
|
||
max_idle: 10
|
||
|
||
redis:
|
||
host: "localhost"
|
||
port: 6379
|
||
driver: "redka"
|
||
password: ""
|
||
db: 0
|
||
|
||
jwt:
|
||
secret: "aiops-local-dev-secret-change-in-production"
|
||
expire_hour: 24
|
||
|
||
websocket:
|
||
read_buffer_size: 1024
|
||
write_buffer_size: 1024
|
||
YAML
|
||
}
|
||
|
||
# 非交互 shell 常缺 Go PATH,补充常见安装目录 // Logan.Li
|
||
ensure_go_path() {
|
||
if command -v go &>/dev/null; then
|
||
return 0
|
||
fi
|
||
export PATH="/usr/local/go/bin:$HOME/go/bin:/usr/lib/go/bin:$PATH"
|
||
if ! command -v go &>/dev/null; then
|
||
error "Go 未安装或不在 PATH,请先安装 Go 1.24+"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 编译独立二进制(含 embed 前端)// Logan.Li
|
||
build_standalone_binary() {
|
||
ensure_go_path || return 1
|
||
build_frontend_embed
|
||
ensure_binary_config
|
||
clean_go_artifacts
|
||
cd "$SCRIPT_DIR/$BACKEND_DIR"
|
||
info "编译中..."
|
||
CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GONOSUMCHECK='*' GOTOOLCHAIN=go1.24.0 \
|
||
go build -ldflags="-w -s" -o "$STANDALONE_BIN_PATH" .
|
||
if [ ! -f "$STANDALONE_BIN_PATH" ]; then
|
||
error "编译失败"
|
||
return 1
|
||
fi
|
||
local bin_size
|
||
bin_size=$(du -h "$STANDALONE_BIN_PATH" | cut -f1)
|
||
success "编译完成: ./$STANDALONE_BIN ($bin_size)"
|
||
cd "$SCRIPT_DIR"
|
||
return 0
|
||
}
|
||
|
||
# 解析/持久化二进制启动参数 // Logan.Li
|
||
save_binary_args() {
|
||
printf '%s\n' "$@" > "$BINARY_ARGS_FILE"
|
||
}
|
||
|
||
# 将旧版 --db-driver sqlite 参数迁移为 --mode local // Logan.Li
|
||
migrate_binary_args_file() {
|
||
[ -f "$BINARY_ARGS_FILE" ] || return 0
|
||
if grep -qE '^--mode ' "$BINARY_ARGS_FILE" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
if ! grep -qE '^--db-driver' "$BINARY_ARGS_FILE" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
local port="8080"
|
||
local lines=()
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
[ -n "$line" ] && lines+=("$line")
|
||
done < "$BINARY_ARGS_FILE"
|
||
local i=0
|
||
while [ $i -lt ${#lines[@]} ]; do
|
||
if [ "${lines[$i]}" = "--port" ] && [ $((i + 1)) -lt ${#lines[@]} ]; then
|
||
port="${lines[$((i + 1))]}"
|
||
break
|
||
fi
|
||
if [[ "${lines[$i]}" == --port=* ]]; then
|
||
port="${lines[$i]#--port=}"
|
||
break
|
||
fi
|
||
i=$((i + 1))
|
||
done
|
||
info "迁移启动参数: --db-driver → --mode local --port $port"
|
||
save_binary_args --mode local --port "$port"
|
||
}
|
||
|
||
load_binary_args() {
|
||
migrate_binary_args_file
|
||
local -n _out=$1
|
||
_out=()
|
||
if [ -f "$BINARY_ARGS_FILE" ]; then
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
[ -n "$line" ] && _out+=("$line")
|
||
done < "$BINARY_ARGS_FILE"
|
||
fi
|
||
if [ ${#_out[@]} -eq 0 ]; then
|
||
_out=(--mode local --port 8080)
|
||
fi
|
||
}
|
||
|
||
# 升级后校验 /version 驱动信息 // Logan.Li
|
||
verify_binary_version() {
|
||
local port=${1:-8080}
|
||
local body
|
||
body=$(curl -sf "http://localhost:${port}/version" 2>/dev/null) || {
|
||
warning "/version 未响应"
|
||
return 1
|
||
}
|
||
info "版本探测: $body"
|
||
echo "$body" | grep -q '"mode":"local"' && success "mode=local 已生效" || warning "mode 非 local"
|
||
echo "$body" | grep -q '"db_driver":"sqlite"' && success "db_driver=sqlite 已生效" || true
|
||
echo "$body" | grep -q '"redis_driver":"redka"' && success "redis_driver=redka 已生效" || true
|
||
return 0
|
||
}
|
||
|
||
binary_listen_port() {
|
||
local args=("$@")
|
||
local i=0
|
||
while [ $i -lt ${#args[@]} ]; do
|
||
if [ "${args[$i]}" = "--port" ] && [ $((i + 1)) -lt ${#args[@]} ]; then
|
||
echo "${args[$((i + 1))]}"
|
||
return 0
|
||
fi
|
||
i=$((i + 1))
|
||
done
|
||
echo "8080"
|
||
}
|
||
|
||
# 后台启动独立二进制 // Logan.Li
|
||
start_binary_daemon() {
|
||
local args=("$@")
|
||
local port
|
||
port=$(binary_listen_port "${args[@]}")
|
||
cd "$SCRIPT_DIR"
|
||
mkdir -p "$LOGS_DIR" uploads
|
||
rotate_log "$LOGS_DIR/aiops.log"
|
||
stop_binary_process "$port"
|
||
nohup ./"$STANDALONE_BIN" "${args[@]}" >> "$LOGS_DIR/aiops.log" 2>&1 &
|
||
echo $! > "$BINARY_PID_FILE"
|
||
save_binary_args "${args[@]}"
|
||
if wait_binary_ready "$port"; then
|
||
success "二进制已启动 (PID: $(cat "$BINARY_PID_FILE"), 端口: $port)"
|
||
info "日志: $LOGS_DIR/aiops.log"
|
||
info "数据: $SCRIPT_DIR/sqlite3.db, $SCRIPT_DIR/redka.db"
|
||
verify_binary_version "$port" || true
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ============================================================
|
||
# Docker Compose 部署
|
||
# ============================================================
|
||
# 检测 Docker Compose 命令 (兼容 docker compose 插件和 docker-compose 独立二进制)
|
||
detect_compose_cmd() {
|
||
if docker compose version &>/dev/null 2>&1; then
|
||
echo "docker compose"
|
||
elif command -v docker-compose &>/dev/null; then
|
||
echo "docker-compose"
|
||
else
|
||
echo ""
|
||
fi
|
||
}
|
||
|
||
cmd_docker() {
|
||
local action=${1:-help}
|
||
|
||
if ! command -v docker &> /dev/null; then
|
||
error "Docker 未安装,请先安装 Docker"
|
||
exit 1
|
||
fi
|
||
|
||
DOCKER_COMPOSE=$(detect_compose_cmd)
|
||
if [ -z "$DOCKER_COMPOSE" ]; then
|
||
error "Docker Compose 未安装 (需要 docker compose 插件或 docker-compose)"
|
||
exit 1
|
||
fi
|
||
# 自动从 git tag 获取版本号
|
||
GIT_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||
export VERSION="${GIT_TAG:-dev}"
|
||
export BUILD_TIME
|
||
info "使用: $DOCKER_COMPOSE"
|
||
info "版本: $VERSION (构建时间: $BUILD_TIME)"
|
||
|
||
# 启用 BuildKit 加速构建(cache mount 跨构建复用 go mod/apk 缓存)// Logan.Li
|
||
export DOCKER_BUILDKIT=1
|
||
export COMPOSE_DOCKER_CLI_BUILD=1
|
||
info "BuildKit: 已启用 (增量编译 + 缓存复用)"
|
||
|
||
case "$action" in
|
||
up|upgrade|update)
|
||
local fresh_build=0
|
||
if [ "${2:-}" = "fresh" ] || [ "$action" = "fresh" ]; then
|
||
fresh_build=1
|
||
fi
|
||
banner "========== Docker Compose 启动/升级 =========="
|
||
clean_frontend_artifacts
|
||
if [ "$fresh_build" -eq 1 ]; then
|
||
info "全量重建镜像 (--no-cache)..."
|
||
$DOCKER_COMPOSE build --no-cache
|
||
$DOCKER_COMPOSE up -d --force-recreate --remove-orphans
|
||
else
|
||
info "增量构建 + 强制重建容器..."
|
||
$DOCKER_COMPOSE up -d --build --force-recreate --remove-orphans
|
||
fi
|
||
echo ""
|
||
# 清理历史 build cache 和悬空镜像,防止占满磁盘
|
||
info "清理历史构建缓存..."
|
||
local before_prune=$(df -h / | awk 'NR==2{print $5}')
|
||
docker builder prune -f 2>/dev/null || true
|
||
docker image prune -f 2>/dev/null || true
|
||
local after_prune=$(df -h / | awk 'NR==2{print $5}')
|
||
info "磁盘使用: ${before_prune} → ${after_prune}"
|
||
echo ""
|
||
info "等待服务就绪..."
|
||
sleep 5
|
||
$DOCKER_COMPOSE ps
|
||
echo ""
|
||
info "前端地址: http://localhost:80"
|
||
info "后端地址: http://localhost:8080"
|
||
info "健康检查: http://localhost:8080/health"
|
||
;;
|
||
down)
|
||
info "停止 Docker Compose..."
|
||
$DOCKER_COMPOSE down
|
||
success "容器已停止"
|
||
;;
|
||
build)
|
||
info "构建 Docker 镜像..."
|
||
$DOCKER_COMPOSE build
|
||
success "镜像构建完成"
|
||
;;
|
||
restart)
|
||
info "重启 Docker Compose..."
|
||
$DOCKER_COMPOSE restart
|
||
success "容器已重启"
|
||
;;
|
||
logs)
|
||
info "Docker 实时日志 (Ctrl+C 退出):"
|
||
$DOCKER_COMPOSE logs -f
|
||
;;
|
||
ps|status)
|
||
$DOCKER_COMPOSE ps
|
||
;;
|
||
clean|prune)
|
||
banner "========== Docker 缓存清理 =========="
|
||
info "清理前磁盘使用:"
|
||
echo " $(df -h / | awk 'NR==2{print $3, "used /", $2, "total ("$5")"}')"
|
||
echo ""
|
||
info "清理 dangling 镜像..."
|
||
docker image prune -f 2>/dev/null || true
|
||
info "清理构建缓存..."
|
||
docker builder prune -f 2>/dev/null || true
|
||
info "清理未使用的网络..."
|
||
docker network prune -f 2>/dev/null || true
|
||
echo ""
|
||
info "清理后磁盘使用:"
|
||
echo " $(df -h / | awk 'NR==2{print $3, "used /", $2, "total ("$5")"}')"
|
||
success "清理完成"
|
||
;;
|
||
fresh)
|
||
cmd_docker up fresh
|
||
;;
|
||
help|--help|-h)
|
||
echo "Docker Compose 部署命令:"
|
||
echo " ./run.sh docker up 增量构建 + 强制重建容器"
|
||
echo " ./run.sh docker up fresh 无缓存全量重建镜像并启动"
|
||
echo " ./run.sh docker upgrade 同 up(升级别名)"
|
||
echo " ./run.sh docker down 停止并移除容器"
|
||
echo " ./run.sh docker build 仅构建镜像"
|
||
echo " ./run.sh docker restart 重启容器"
|
||
echo " ./run.sh docker logs 查看实时日志"
|
||
echo " ./run.sh docker ps 查看容器状态"
|
||
echo " ./run.sh docker clean 清理构建缓存/无用镜像"
|
||
;;
|
||
*)
|
||
error "未知 docker 子命令: $action"
|
||
cmd_docker help
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ============================================================
|
||
# 编译独立二进制
|
||
# ============================================================
|
||
cmd_build() {
|
||
local target=${1:-binary}
|
||
|
||
case "$target" in
|
||
binary|aiops)
|
||
banner "========== 编译独立二进制 =========="
|
||
build_standalone_binary || exit 1
|
||
echo ""
|
||
info "使用方法:"
|
||
echo " ./run.sh binary upgrade # 停服重编译并后台启动"
|
||
echo " ./$STANDALONE_BIN --port 8080 # 前台启动"
|
||
echo " ./$STANDALONE_BIN --mode local # 本地模式 (SQLite + redka)"
|
||
echo " ./$STANDALONE_BIN --config config.yaml # 使用配置文件"
|
||
;;
|
||
agent)
|
||
banner "========== 编译 Agent =========="
|
||
cd "$SCRIPT_DIR/agent"
|
||
|
||
info "编译 Linux AMD64..."
|
||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent-linux-amd64 .
|
||
success "aiops-agent-linux-amd64 ($(du -h aiops-agent-linux-amd64 | cut -f1))"
|
||
|
||
info "编译 Linux ARM64..."
|
||
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent-linux-arm64 .
|
||
success "aiops-agent-linux-arm64 ($(du -h aiops-agent-linux-arm64 | cut -f1))"
|
||
|
||
if [ "$OS" = "macos" ]; then
|
||
info "编译 macOS (当前平台)..."
|
||
CGO_ENABLED=0 go build -ldflags="-w -s" -o aiops-agent .
|
||
success "aiops-agent ($(du -h aiops-agent | cut -f1))"
|
||
fi
|
||
;;
|
||
frontend)
|
||
info "构建前端生产包..."
|
||
cd "$SCRIPT_DIR/$FRONTEND_DIR"
|
||
npm run build
|
||
success "前端构建完成: $FRONTEND_DIR/dist/"
|
||
;;
|
||
help|--help|-h)
|
||
echo "编译命令:"
|
||
echo " ./run.sh build 编译独立二进制 aiops (默认 SQLite)"
|
||
echo " ./run.sh build agent 交叉编译 Agent (amd64+arm64)"
|
||
echo " ./run.sh build frontend 构建前端生产包"
|
||
;;
|
||
*)
|
||
error "未知构建目标: $target"
|
||
cmd_build help
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ============================================================
|
||
# 独立二进制模式
|
||
# ============================================================
|
||
cmd_binary() {
|
||
local action="run"
|
||
if [ $# -gt 0 ] && [[ "$1" =~ ^(upgrade|start|stop|status|help|--help|-h)$ ]]; then
|
||
action=$1
|
||
shift
|
||
fi
|
||
|
||
case "$action" in
|
||
upgrade|update)
|
||
banner "========== 二进制升级部署 =========="
|
||
local args=()
|
||
if [ $# -gt 0 ]; then
|
||
args=("$@")
|
||
else
|
||
load_binary_args args
|
||
fi
|
||
local port
|
||
port=$(binary_listen_port "${args[@]}")
|
||
info "启动参数: ${args[*]}"
|
||
stop_binary_process "$port"
|
||
if [ -f "$STANDALONE_BIN_PATH" ]; then
|
||
local backup="$SCRIPT_DIR/${STANDALONE_BIN}.bak.$(date +%Y%m%d%H%M%S)"
|
||
cp "$STANDALONE_BIN_PATH" "$backup"
|
||
info "已备份旧二进制: $backup"
|
||
fi
|
||
build_standalone_binary || exit 1
|
||
start_binary_daemon "${args[@]}" || exit 1
|
||
;;
|
||
start)
|
||
local args=()
|
||
if [ $# -gt 0 ]; then
|
||
args=("$@")
|
||
else
|
||
load_binary_args args
|
||
fi
|
||
if [ ! -f "$STANDALONE_BIN_PATH" ]; then
|
||
info "二进制不存在,先编译..."
|
||
build_standalone_binary || exit 1
|
||
fi
|
||
start_binary_daemon "${args[@]}" || exit 1
|
||
;;
|
||
stop)
|
||
local port=${1:-8080}
|
||
stop_binary_process "$port"
|
||
success "二进制已停止"
|
||
;;
|
||
status)
|
||
local args=()
|
||
load_binary_args args
|
||
local port
|
||
port=$(binary_listen_port "${args[@]}")
|
||
info "监听端口: $port"
|
||
if [ -f "$BINARY_PID_FILE" ]; then
|
||
local pid
|
||
pid=$(cat "$BINARY_PID_FILE" 2>/dev/null || echo "")
|
||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||
success "进程运行中 (PID: $pid)"
|
||
else
|
||
warning "PID 文件存在但进程未运行"
|
||
fi
|
||
else
|
||
warning "未找到 PID 文件"
|
||
fi
|
||
if curl -sf "http://localhost:${port}/version" > /dev/null 2>&1; then
|
||
success "HTTP 就绪: http://localhost:${port}/version"
|
||
else
|
||
warning "HTTP 未响应"
|
||
fi
|
||
;;
|
||
help|--help|-h)
|
||
echo "独立二进制命令:"
|
||
echo " ./run.sh binary upgrade [--port 8080] 停服→清旧→重编译→启动 (默认 --mode local)"
|
||
echo " ./run.sh binary start [args...] 后台启动(参数会持久化)"
|
||
echo " ./run.sh binary stop [port] 停止进程"
|
||
echo " ./run.sh binary status 查看状态"
|
||
echo " ./run.sh binary [--mode local --port 8080 ...] 前台运行 (SQLite + redka)"
|
||
;;
|
||
run|*)
|
||
if [ ! -f "$STANDALONE_BIN_PATH" ]; then
|
||
info "二进制不存在,先编译..."
|
||
build_standalone_binary || exit 1
|
||
fi
|
||
banner "========== 前台启动独立二进制 =========="
|
||
mkdir -p "$LOGS_DIR" uploads
|
||
if [ $# -gt 0 ]; then
|
||
save_binary_args "$@"
|
||
fi
|
||
cd "$SCRIPT_DIR"
|
||
./"$STANDALONE_BIN" "$@"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ============================================================
|
||
# Kubernetes 部署 (embed 单容器模式)
|
||
# ============================================================
|
||
KUBERNETES_NAMESPACE="devops"
|
||
KUBERNETES_MANIFEST_DIR="$SCRIPT_DIR/deploy/kubernetes"
|
||
KUBERNETES_IMAGE="aiops:embed"
|
||
KUBERNETES_NODE_PORT=30080
|
||
KUBERNETES_BUILT_IMAGE=""
|
||
|
||
# 检测 kubectl 和 Docker
|
||
check_kubernetes_deps() {
|
||
if ! command -v kubectl &>/dev/null; then
|
||
error "kubectl 未安装,请先安装 kubectl"
|
||
exit 1
|
||
fi
|
||
if ! kubectl cluster-info &>/dev/null 2>&1; then
|
||
error "无法连接 Kubernetes 集群,请检查 kubeconfig"
|
||
kubectl cluster-info 2>&1 | tail -3
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 检测容器运行时并加载镜像到集群
|
||
load_image_to_cluster() {
|
||
local image=$1
|
||
info "加载镜像到集群..."
|
||
|
||
# minikube
|
||
if command -v minikube &>/dev/null && minikube status &>/dev/null 2>&1; then
|
||
minikube image load "$image" 2>/dev/null && success "镜像已加载到 minikube" && return 0
|
||
fi
|
||
|
||
# kind
|
||
if command -v kind &>/dev/null && kind get clusters &>/dev/null 2>&1 | grep -q .; then
|
||
kind load docker-image "$image" 2>/dev/null && success "镜像已加载到 kind" && return 0
|
||
fi
|
||
|
||
# k3d
|
||
if command -v k3d &>/dev/null; then
|
||
k3d image import "$image" 2>/dev/null && success "镜像已加载到 k3d" && return 0
|
||
fi
|
||
|
||
# Docker Desktop K8s / 远程仓库 — 镜像通过 registry 共享
|
||
warning "未检测到 minikube/kind/k3d,假设镜像通过 registry 共享"
|
||
warning "如集群无法拉取镜像,请手动推送: docker push <registry>/$image"
|
||
}
|
||
|
||
# Kubernetes 构建镜像(upgrade 时无缓存 + 时间戳 tag 避免旧层残留)// Logan.Li
|
||
cmd_kubernetes_build() {
|
||
local no_cache=${1:-0}
|
||
if ! command -v docker &>/dev/null; then
|
||
error "Docker 未安装"
|
||
exit 1
|
||
fi
|
||
|
||
banner "========== 构建 embed 镜像 =========="
|
||
clean_frontend_artifacts
|
||
clean_embed_static
|
||
|
||
local version
|
||
version=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")
|
||
local build_time
|
||
build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||
local image_tag
|
||
image_tag="${KUBERNETES_IMAGE}-$(date +%Y%m%d%H%M%S)"
|
||
info "镜像: $KUBERNETES_IMAGE (+ $image_tag)"
|
||
|
||
local build_args=(
|
||
-f Dockerfile.embed
|
||
--build-arg VERSION="$version"
|
||
--build-arg BUILD_TIME="$build_time"
|
||
-t "$KUBERNETES_IMAGE"
|
||
-t "$image_tag"
|
||
)
|
||
if [ "$no_cache" -eq 1 ]; then
|
||
build_args+=(--no-cache --pull)
|
||
info "无缓存全量构建 (--no-cache --pull)"
|
||
fi
|
||
build_args+=(.)
|
||
|
||
info "构建中(前端 + 后端 embed)..."
|
||
docker build "${build_args[@]}" || {
|
||
error "镜像构建失败"
|
||
exit 1
|
||
}
|
||
|
||
local img_size
|
||
img_size=$(docker images "$KUBERNETES_IMAGE" --format "{{.Size}}" | head -1)
|
||
success "镜像构建完成: $KUBERNETES_IMAGE ($img_size)"
|
||
|
||
load_image_to_cluster "$KUBERNETES_IMAGE"
|
||
load_image_to_cluster "$image_tag"
|
||
KUBERNETES_BUILT_IMAGE="$image_tag"
|
||
}
|
||
|
||
# 读取 deployment.yaml 中的 replicas(默认 1)// Logan.Li
|
||
kubernetes_manifest_replicas() {
|
||
local replicas
|
||
replicas=$(awk '/^ replicas:/{print $2; exit}' "$KUBERNETES_MANIFEST_DIR/deployment.yaml" 2>/dev/null)
|
||
echo "${replicas:-1}"
|
||
}
|
||
|
||
# 读取集群中 aiops Deployment 当前副本数 // Logan.Li
|
||
kubernetes_live_replicas() {
|
||
kubectl -n "$KUBERNETES_NAMESPACE" get deployment aiops -o jsonpath='{.spec.replicas}' 2>/dev/null
|
||
}
|
||
|
||
# 多副本时 SQLite 不共享,强制切换 MODE=release + 集群内 MySQL/Redis // Logan.Li
|
||
ensure_cluster_mode_for_replicas() {
|
||
local replicas=${1:-1}
|
||
if [ "$replicas" -le 1 ]; then
|
||
return 0
|
||
fi
|
||
if ! kubectl -n "$KUBERNETES_NAMESPACE" get deployment aiops &>/dev/null; then
|
||
warning "Deployment 尚未创建,跳过多副本模式切换(apply 后会自动处理)"
|
||
return 0
|
||
fi
|
||
local current_mode
|
||
current_mode=$(kubectl -n "$KUBERNETES_NAMESPACE" get deployment aiops \
|
||
-o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="MODE")].value}' 2>/dev/null)
|
||
if [ "$current_mode" = "release" ]; then
|
||
info "副本 $replicas,已为 release 模式"
|
||
return 0
|
||
fi
|
||
warning "副本 $replicas > 1,SQLite 数据不共享,自动切换 MODE=release (mysql + redis)"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" set env deployment/aiops \
|
||
MODE=release \
|
||
DATABASE_DRIVER=mysql \
|
||
DATABASE_HOST=mysql \
|
||
DATABASE_PORT=3306 \
|
||
DATABASE_USER=aiops \
|
||
DATABASE_PASSWORD=aiops_pass_2026 \
|
||
DATABASE_DBNAME=aiops \
|
||
REDIS_DRIVER=redis \
|
||
REDIS_HOST=redis \
|
||
REDIS_PORT=6379 \
|
||
--overwrite
|
||
success "已切换 release 模式,使用集群 MySQL/Redis"
|
||
}
|
||
|
||
# Kubernetes 部署
|
||
cmd_kubernetes() {
|
||
local action=${1:-help}
|
||
|
||
case "$action" in
|
||
deploy|up)
|
||
check_kubernetes_deps
|
||
banner "========== Kubernetes 部署 ($KUBERNETES_NAMESPACE) =========="
|
||
|
||
# 1. 构建镜像
|
||
cmd_kubernetes_build
|
||
|
||
# 2. 部署基础设施
|
||
info "部署 Namespace + Secret + ConfigMap..."
|
||
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/namespace.yaml"
|
||
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/secret.yaml"
|
||
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/configmap.yaml"
|
||
|
||
info "部署 local 模式数据卷 (PVC)..."
|
||
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/pvc-aiops-data.yaml"
|
||
|
||
info "部署 MySQL + Redis..."
|
||
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/mysql.yaml"
|
||
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/redis.yaml"
|
||
|
||
# 3. 等待数据库就绪
|
||
info "等待 MySQL 就绪..."
|
||
kubectl -n "$KUBERNETES_NAMESPACE" rollout status statefulset/mysql --timeout=120s 2>/dev/null || {
|
||
warning "MySQL 启动较慢,继续部署应用(可在后台自行完成初始化)"
|
||
}
|
||
info "等待 Redis 就绪..."
|
||
kubectl -n "$KUBERNETES_NAMESPACE" rollout status statefulset/redis --timeout=60s 2>/dev/null || true
|
||
|
||
# 4. 部署应用
|
||
info "部署 AIOps 应用..."
|
||
local target_replicas
|
||
target_replicas=$(kubernetes_manifest_replicas)
|
||
if [ "$target_replicas" -gt 1 ]; then
|
||
warning "manifest replicas=$target_replicas,将自动切换 MODE=release"
|
||
fi
|
||
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/deployment.yaml"
|
||
ensure_cluster_mode_for_replicas "$target_replicas"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" rollout status deployment/aiops --timeout=180s 2>/dev/null || {
|
||
warning "应用正在启动,查看日志: ./run.sh kubernetes logs"
|
||
}
|
||
|
||
# 5. Ingress(可选)
|
||
if [ -f "$KUBERNETES_MANIFEST_DIR/ingress.yaml" ]; then
|
||
info "部署 Ingress(如集群无 Ingress Controller 会跳过)..."
|
||
kubectl apply -f "$KUBERNETES_MANIFEST_DIR/ingress.yaml" 2>/dev/null || true
|
||
fi
|
||
|
||
echo ""
|
||
success "部署完成!"
|
||
echo ""
|
||
kubectl -n "$KUBERNETES_NAMESPACE" get pods,svc
|
||
echo ""
|
||
local node_ip=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null)
|
||
info "访问地址: http://${node_ip:-<NODE_IP>}:$KUBERNETES_NODE_PORT"
|
||
info "健康检查: http://${node_ip:-<NODE_IP>}:$KUBERNETES_NODE_PORT/health"
|
||
;;
|
||
|
||
upgrade|update)
|
||
check_kubernetes_deps
|
||
banner "========== Kubernetes 滚动更新 =========="
|
||
local live_replicas
|
||
live_replicas=$(kubernetes_live_replicas)
|
||
live_replicas=${live_replicas:-1}
|
||
ensure_cluster_mode_for_replicas "$live_replicas"
|
||
cmd_kubernetes_build 1
|
||
info "更新 Deployment 镜像: $KUBERNETES_BUILT_IMAGE"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" set image deployment/aiops aiops="$KUBERNETES_BUILT_IMAGE" --record
|
||
kubectl -n "$KUBERNETES_NAMESPACE" rollout status deployment/aiops --timeout=180s
|
||
info "清理悬空镜像..."
|
||
docker image prune -f 2>/dev/null || true
|
||
success "滚动更新完成"
|
||
;;
|
||
|
||
delete|down|remove)
|
||
check_kubernetes_deps
|
||
banner "========== Kubernetes 清理 ($KUBERNETES_NAMESPACE) =========="
|
||
warning "这将删除 $KUBERNETES_NAMESPACE 命名空间中的所有资源"
|
||
warning "MySQL/Redis 持久卷(PVC)需手动删除"
|
||
echo ""
|
||
read -p "确认删除?[y/N] " confirm
|
||
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
|
||
kubectl delete -f "$KUBERNETES_MANIFEST_DIR/" --ignore-not-found 2>/dev/null
|
||
echo ""
|
||
info "PVC 状态(需手动清理):"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" get pvc 2>/dev/null || echo " 无 PVC"
|
||
success "清理完成"
|
||
else
|
||
info "已取消"
|
||
fi
|
||
;;
|
||
|
||
build|image)
|
||
cmd_kubernetes_build
|
||
;;
|
||
|
||
status|ps)
|
||
check_kubernetes_deps
|
||
banner "========== Kubernetes 状态 ($KUBERNETES_NAMESPACE) =========="
|
||
echo ""
|
||
echo "--- Pods ---"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" get pods -o wide 2>/dev/null
|
||
echo ""
|
||
echo "--- Services ---"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" get svc 2>/dev/null
|
||
echo ""
|
||
echo "--- PVC ---"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" get pvc 2>/dev/null
|
||
echo ""
|
||
echo "--- Ingress ---"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" get ingress 2>/dev/null
|
||
;;
|
||
|
||
logs|log)
|
||
check_kubernetes_deps
|
||
local follow=${2:--f}
|
||
info "应用日志 (Ctrl+C 退出):"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" logs $follow deployment/aiops --tail=100
|
||
;;
|
||
|
||
exec|shell|sh)
|
||
check_kubernetes_deps
|
||
info "进入容器 Shell..."
|
||
kubectl -n "$KUBERNETES_NAMESPACE" exec -it deployment/aiops -- bash || \
|
||
kubectl -n "$KUBERNETES_NAMESPACE" exec -it deployment/aiops -- sh
|
||
;;
|
||
|
||
mysql|db)
|
||
check_kubernetes_deps
|
||
info "进入 MySQL..."
|
||
kubectl -n "$KUBERNETES_NAMESPACE" exec -it mysql-0 -- mysql -uaiops -paiops_pass_2026 aiops
|
||
;;
|
||
|
||
redis)
|
||
check_kubernetes_deps
|
||
info "进入 Redis CLI..."
|
||
kubectl -n "$KUBERNETES_NAMESPACE" exec -it redis-0 -- redis-cli
|
||
;;
|
||
|
||
scale)
|
||
check_kubernetes_deps
|
||
local replicas=${2:-1}
|
||
if [ "$replicas" -gt 1 ]; then
|
||
ensure_cluster_mode_for_replicas "$replicas"
|
||
fi
|
||
info "扩缩容到 $replicas 副本..."
|
||
kubectl -n "$KUBERNETES_NAMESPACE" scale deployment/aiops --replicas="$replicas"
|
||
;;
|
||
|
||
port-forward|pf)
|
||
check_kubernetes_deps
|
||
local local_port=${2:-8080}
|
||
info "端口转发 localhost:$local_port → aiops:8080 (Ctrl+C 退出):"
|
||
kubectl -n "$KUBERNETES_NAMESPACE" port-forward svc/aiops "$local_port":8080
|
||
;;
|
||
|
||
help|--help|-h)
|
||
echo "Kubernetes 部署命令 (embed 单容器模式):"
|
||
echo ""
|
||
echo " ${GREEN}部署${NC}"
|
||
echo " ./run.sh kubernetes deploy 构建镜像 + 全量部署"
|
||
echo " ./run.sh kubernetes upgrade 重新构建镜像 + 滚动更新"
|
||
echo " ./run.sh kubernetes delete 删除所有 Kubernetes 资源(保留 PVC)"
|
||
echo ""
|
||
echo " ${CYAN}运维${NC}"
|
||
echo " ./run.sh kubernetes status 查看资源状态"
|
||
echo " ./run.sh kubernetes logs 查看应用日志(默认 follow)"
|
||
echo " ./run.sh kubernetes exec 进入容器 Shell"
|
||
echo " ./run.sh kubernetes mysql 进入 MySQL CLI"
|
||
echo " ./run.sh kubernetes redis 进入 Redis CLI"
|
||
echo " ./run.sh kubernetes scale 3 扩缩容到 3 副本"
|
||
echo " ./run.sh kubernetes port-forward 端口转发到本机 8080"
|
||
echo ""
|
||
echo " ${YELLOW}镜像${NC}"
|
||
echo " ./run.sh kubernetes build 仅构建 embed 镜像"
|
||
echo ""
|
||
echo " 镜像: $KUBERNETES_IMAGE"
|
||
echo " 命名空间: $KUBERNETES_NAMESPACE"
|
||
echo " NodePort: $KUBERNETES_NODE_PORT"
|
||
;;
|
||
|
||
*)
|
||
error "未知 kubernetes 子命令: $action"
|
||
cmd_kubernetes help
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ============================================================
|
||
# 帮助
|
||
# ============================================================
|
||
cmd_help() {
|
||
echo ""
|
||
banner " $PROJECT_NAME 启动脚本"
|
||
echo ""
|
||
echo -e " ${GREEN}本地开发${NC}"
|
||
echo " ./run.sh dev 启动前后端开发服务 (SQLite, 端口 8080+5173)"
|
||
echo " ./run.sh stop 停止本地服务"
|
||
echo " ./run.sh restart 重启本地服务"
|
||
echo " ./run.sh status 查看本地服务状态"
|
||
echo " ./run.sh logs 查看实时日志"
|
||
echo ""
|
||
echo -e " ${CYAN}Docker 容器部署${NC}"
|
||
echo " ./run.sh docker up 增量构建 + 强制重建容器"
|
||
echo " ./run.sh docker up fresh 无缓存全量重建并启动"
|
||
echo " ./run.sh docker upgrade 同 up(升级别名)"
|
||
echo " ./run.sh docker down 停止并移除容器"
|
||
echo " ./run.sh docker build 仅构建镜像"
|
||
echo " ./run.sh docker restart 重启容器"
|
||
echo " ./run.sh docker logs 查看容器日志"
|
||
echo " ./run.sh docker ps 查看容器状态"
|
||
echo ""
|
||
echo -e " ${MAGENTA}Kubernetes 部署 (embed 单容器)${NC}"
|
||
echo " ./run.sh kubernetes deploy 构建镜像 + 全量部署到 Kubernetes"
|
||
echo " ./run.sh kubernetes upgrade 重新构建 + 滚动更新"
|
||
echo " ./run.sh kubernetes delete 删除所有 Kubernetes 资源"
|
||
echo " ./run.sh kubernetes status 查看资源状态"
|
||
echo " ./run.sh kubernetes logs 查看应用日志"
|
||
echo " ./run.sh kubernetes exec 进入容器 Shell"
|
||
echo " ./run.sh kubernetes mysql 进入 MySQL CLI"
|
||
echo " ./run.sh kubernetes redis 进入 Redis CLI"
|
||
echo " ./run.sh kubernetes scale 3 扩缩容"
|
||
echo " ./run.sh kubernetes port-forward 端口转发到本机"
|
||
echo " ./run.sh kubernetes build 仅构建 embed 镜像"
|
||
echo ""
|
||
echo -e " ${YELLOW}编译 & 独立二进制${NC}"
|
||
echo " ./run.sh build 编译独立二进制 ./$STANDALONE_BIN"
|
||
echo " ./run.sh binary upgrade 停服→清旧→重编译→后台启动(生产升级)"
|
||
echo " ./run.sh binary start|stop|status 进程管理"
|
||
echo " ./run.sh binary [args] 前台启动(调试)"
|
||
echo " ./run.sh build agent 交叉编译 Agent (amd64+arm64)"
|
||
echo " ./run.sh build frontend 构建前端生产包"
|
||
echo ""
|
||
echo -e " ${BLUE}独立二进制用法${NC}"
|
||
echo " ./$STANDALONE_BIN 默认启动 (SQLite, 端口 8080)"
|
||
echo " ./$STANDALONE_BIN --port 9090 自定义端口"
|
||
echo " ./$STANDALONE_BIN --mode local 本地模式 (SQLite + redka)"
|
||
echo " ./$STANDALONE_BIN --config config.yaml 使用配置文件"
|
||
echo " ./$STANDALONE_BIN --db-driver mysql \\"
|
||
echo " --db-host 127.0.0.1 --db-user root \\"
|
||
echo " --db-password pass --db-name aiops MySQL 模式"
|
||
echo ""
|
||
echo -e " ${GREEN}快捷操作${NC}"
|
||
echo " ./run.sh 等同于 ./run.sh dev"
|
||
echo " ./run.sh help 显示此帮助"
|
||
echo ""
|
||
}
|
||
|
||
# ============================================================
|
||
# 主入口
|
||
# ============================================================
|
||
main() {
|
||
local cmd=${1:-dev}
|
||
|
||
case "$cmd" in
|
||
# 本地开发
|
||
dev|start)
|
||
cmd_dev
|
||
;;
|
||
stop)
|
||
cmd_stop
|
||
;;
|
||
restart)
|
||
cmd_restart
|
||
;;
|
||
status)
|
||
cmd_status
|
||
;;
|
||
logs)
|
||
cmd_logs
|
||
;;
|
||
|
||
# Docker Compose
|
||
docker|compose)
|
||
shift
|
||
cmd_docker "$@"
|
||
;;
|
||
|
||
# 编译
|
||
build)
|
||
shift
|
||
cmd_build "$@"
|
||
;;
|
||
|
||
# 独立二进制
|
||
binary|bin)
|
||
shift
|
||
cmd_binary "$@"
|
||
;;
|
||
|
||
# Kubernetes 部署 (k8s 为别名)
|
||
kubernetes|k8s)
|
||
shift
|
||
cmd_kubernetes "$@"
|
||
;;
|
||
|
||
# 帮助
|
||
help|--help|-h)
|
||
cmd_help
|
||
;;
|
||
|
||
*)
|
||
error "未知命令: $cmd"
|
||
cmd_help
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
main "$@"
|