diff --git a/run.sh b/run.sh index 30d21b9..aab3883 100755 --- a/run.sh +++ b/run.sh @@ -14,7 +14,8 @@ # # 方式 3 — 独立二进制: # ./run.sh build 编译独立二进制 (embed 前端) -# ./run.sh binary 编译并启动 +# ./run.sh binary upgrade 停服 → 清旧产物 → 重编译 → 后台启动 +# ./run.sh binary start|stop|status # # 本地开发: # ./run.sh dev 前后端热重载 @@ -60,6 +61,8 @@ cd "$SCRIPT_DIR" # PID 文件 BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid" FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.pid" +BINARY_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.aiops.pid" +BINARY_ARGS_FILE="$SCRIPT_DIR/$BACKEND_DIR/.aiops.args" # 检测操作系统 detect_os() { @@ -290,6 +293,196 @@ cmd_logs() { 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 "$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 "${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 "二进制启动超时,查看日志: $BACKEND_DIR/logs/aiops.log" + tail -20 "$SCRIPT_DIR/$BACKEND_DIR/logs/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/$BACKEND_DIR/config.yaml" + if [ -f "$cfg" ]; then + return 0 + fi + info "生成默认 config.yaml (SQLite + 无Redis)..." + cat > "$cfg" << 'YAML' +mode: "release" +port: "8080" +log_level: "info" + +database: + driver: "sqlite" + host: "localhost" + port: 3306 + user: "" + password: "" + dbname: "data/aiops.db" + charset: "utf8mb4" + max_conn: 100 + max_idle: 10 + +redis: + host: "localhost" + port: 6379 + password: "" + db: 0 + +jwt: + secret: "CHANGE_ME_JWT_SECRET" # ⚠️ 生产环境必须替换 + expire_hour: 24 + +websocket: + read_buffer_size: 1024 + write_buffer_size: 1024 +YAML +} + +# 编译独立二进制(含 embed 前端)// Logan.Li +build_standalone_binary() { + 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" . + if [ ! -f "$STANDALONE_BIN" ]; then + error "编译失败" + return 1 + fi + local bin_size + bin_size=$(du -h "$STANDALONE_BIN" | cut -f1) + success "编译完成: $BACKEND_DIR/$STANDALONE_BIN ($bin_size)" + cd "$SCRIPT_DIR" + return 0 +} + +# 解析/持久化二进制启动参数 // Logan.Li +save_binary_args() { + printf '%s\n' "$@" > "$BINARY_ARGS_FILE" +} + +load_binary_args() { + 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=(--port 8080 --db-driver sqlite) + fi +} + +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/$BACKEND_DIR" + mkdir -p data logs uploads + rotate_log "logs/aiops.log" + stop_binary_process "$port" + nohup ./"$STANDALONE_BIN" "${args[@]}" >> logs/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 "日志: $BACKEND_DIR/logs/aiops.log" + info "健康检查: http://localhost:${port}/version" + return 0 + fi + return 1 +} + # ============================================================ # Docker Compose 部署 # ============================================================ @@ -331,9 +524,21 @@ cmd_docker() { info "BuildKit: 已启用 (增量编译 + 缓存复用)" case "$action" in - up) - banner "========== Docker Compose 启动 ==========" - $DOCKER_COMPOSE up -d --build + 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 "清理历史构建缓存..." @@ -389,15 +594,20 @@ cmd_docker() { 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 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 清理构建缓存/无用镜像" + 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" @@ -416,78 +626,12 @@ cmd_build() { case "$target" in binary|aiops) banner "========== 编译独立二进制 ==========" - - # 构建前端 → 拷贝到 embed 目录(go:embed 需要 static/ 下有文件) - info "构建前端 → embed..." - cd "$SCRIPT_DIR/$FRONTEND_DIR" - if [ ! -d "node_modules" ]; then - npm install --registry=https://registry.npmmirror.com - fi - npm run build 2>&1 | tail -5 - - # 拷贝前端构建产物到后端 embed 目录 - cd "$SCRIPT_DIR" - rm -rf "$BACKEND_DIR/static"/* - cp -r "$FRONTEND_DIR/dist/"* "$BACKEND_DIR/static/" - local fe_files=$(find "$BACKEND_DIR/static" -type f | wc -l) - success "前端已嵌入 ($fe_files 文件)" - - cd "$SCRIPT_DIR/$BACKEND_DIR" - - # 确保 config.yaml 存在(二进制模式默认 SQLite) - if [ ! -f "config.yaml" ]; then - info "生成默认 config.yaml (SQLite + 无Redis)..." - cat > config.yaml << 'YAML' -mode: "release" -port: "8080" -log_level: "info" - -database: - driver: "sqlite" - host: "localhost" - port: 3306 - user: "" - password: "" - dbname: "data/aiops.db" - charset: "utf8mb4" - max_conn: 100 - max_idle: 10 - -redis: - host: "localhost" - port: 6379 - password: "" - db: 0 - -jwt: - secret: "CHANGE_ME_JWT_SECRET" # ⚠️ 生产环境必须替换 - expire_hour: 24 - -websocket: - read_buffer_size: 1024 - write_buffer_size: 1024 -YAML - fi - - # 静态编译(CGO_ENABLED=1 因为 SQLite 需要 CGO) - info "编译中..." - CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GONOSUMCHECK='*' GOTOOLCHAIN=go1.24.0 go build -ldflags="-w -s" -o "$STANDALONE_BIN" . - - if [ -f "$STANDALONE_BIN" ]; then - local bin_size=$(du -h "$STANDALONE_BIN" | cut -f1) - success "编译完成: $BACKEND_DIR/$STANDALONE_BIN ($bin_size)" - echo "" - info "使用方法:" - echo " ./$BACKEND_DIR/$STANDALONE_BIN # 默认启动 (SQLite, 端口 8080)" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 9090 # 自定义端口" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --db-driver mysql \\" - echo " --db-host localhost --db-user root \\" - echo " --db-password pass --db-name aiops # MySQL 模式" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml # 使用配置文件" - else - error "编译失败" - exit 1 - fi + build_standalone_binary || exit 1 + echo "" + info "使用方法:" + echo " ./run.sh binary upgrade # 停服重编译并后台启动" + echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 8080 # 前台启动" + echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml # 使用配置文件" ;; agent) banner "========== 编译 Agent ==========" @@ -531,19 +675,96 @@ YAML # 独立二进制模式 # ============================================================ cmd_binary() { - cd "$SCRIPT_DIR/$BACKEND_DIR" - - # 先编译(如果不存在) - if [ ! -f "$STANDALONE_BIN" ]; then - info "二进制不存在,先编译..." - cmd_build binary + local action="run" + if [ $# -gt 0 ] && [[ "$1" =~ ^(upgrade|start|stop|status|help|--help|-h)$ ]]; then + action=$1 + shift fi - banner "========== 启动独立二进制 ==========" - mkdir -p data logs uploads - - # 传递额外参数 - ./"$STANDALONE_BIN" "$@" + 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 "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" ]; then + local backup="$SCRIPT_DIR/$BACKEND_DIR/${STANDALONE_BIN}.bak.$(date +%Y%m%d%H%M%S)" + cp "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" "$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 "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" ]; 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 --db-driver sqlite] 停服→清旧→重编译→启动" + echo " ./run.sh binary start [args...] 后台启动(参数会持久化)" + echo " ./run.sh binary stop [port] 停止进程" + echo " ./run.sh binary status 查看状态" + echo " ./run.sh binary [--port 8080 ...] 前台运行(开发调试)" + ;; + run|*) + cd "$SCRIPT_DIR/$BACKEND_DIR" + if [ ! -f "$STANDALONE_BIN" ]; then + info "二进制不存在,先编译..." + build_standalone_binary || exit 1 + fi + banner "========== 前台启动独立二进制 ==========" + mkdir -p data logs uploads + if [ $# -gt 0 ]; then + save_binary_args "$@" + fi + ./"$STANDALONE_BIN" "$@" + ;; + esac } # ============================================================ @@ -553,6 +774,7 @@ 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() { @@ -592,34 +814,52 @@ load_image_to_cluster() { warning "如集群无法拉取镜像,请手动推送: docker push /$image" } -# Kubernetes 构建镜像 +# 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 镜像 ==========" - info "镜像: $KUBERNETES_IMAGE" + clean_frontend_artifacts + clean_embed_static - # 版本信息 - local version=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev") - local build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ) + 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 -f Dockerfile.embed \ - --build-arg VERSION="$version" \ - --build-arg BUILD_TIME="$build_time" \ - -t "$KUBERNETES_IMAGE" . || { + docker build "${build_args[@]}" || { error "镜像构建失败" exit 1 } - local img_size=$(docker images "$KUBERNETES_IMAGE" --format "{{.Size}}" | head -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" } # Kubernetes 部署 @@ -678,10 +918,12 @@ cmd_kubernetes() { upgrade|update) check_kubernetes_deps banner "========== Kubernetes 滚动更新 ==========" - cmd_kubernetes_build - info "触发滚动更新..." - kubectl -n "$KUBERNETES_NAMESPACE" rollout restart deployment/aiops + 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 "滚动更新完成" ;; @@ -812,7 +1054,9 @@ cmd_help() { echo " ./run.sh logs 查看实时日志" echo "" echo -e " ${CYAN}Docker 容器部署${NC}" - echo " ./run.sh docker up 构建并启动所有容器 (MySQL+Redis+Nginx)" + 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 重启容器" @@ -834,7 +1078,9 @@ cmd_help() { echo "" echo -e " ${YELLOW}编译 & 独立二进制${NC}" echo " ./run.sh build 编译独立二进制 ./$BACKEND_DIR/$STANDALONE_BIN" - echo " ./run.sh binary 编译+启动独立二进制" + 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 ""