487 lines
21 KiB
Bash
487 lines
21 KiB
Bash
# ===========================================
|
||
# common.sh — deploy.sh 共享基础设施
|
||
# ===========================================
|
||
|
||
# ---------- 彩色输出 ----------
|
||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
|
||
log_dry() { echo -e "${YELLOW}[DRY-RUN]${NC} $1"; }
|
||
|
||
# 默认超时(秒)
|
||
: "${DEPLOY_TIMEOUT_SEC:=400}"
|
||
|
||
# ---------- Git 信息 ----------
|
||
get_git_info() {
|
||
local b=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||
local c=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||
echo "${b}@${c}"
|
||
}
|
||
|
||
# ---------- SSH 连接复用 ----------
|
||
get_ssh_socket() { echo "/tmp/ssh-master-${REMOTE_HOST}-$$"; }
|
||
|
||
ssh_connect() {
|
||
[ "$DRY_RUN" = true ] && { log_dry "ssh -o ControlMaster=auto -o ControlPath=$(get_ssh_socket) ${REMOTE_USER}@${REMOTE_HOST}"; return 0; }
|
||
local socket; socket=$(get_ssh_socket)
|
||
# 测试连接
|
||
if ! timeout "${DEPLOY_TIMEOUT_SEC}" ssh -o "ControlPath=${socket}" -o "ConnectionAttempts=3" -o "ConnectTimeout=10" \
|
||
-o "ControlMaster=no" "${REMOTE_USER}@${REMOTE_HOST}" "echo ok" &>/dev/null; then
|
||
log_info "建立 SSH 连接..."
|
||
timeout "${DEPLOY_TIMEOUT_SEC}" ssh -o ControlMaster=auto -o ControlPath="${socket}" -o ControlPersist=600 \
|
||
-o ConnectionAttempts=3 -o ConnectTimeout=10 \
|
||
-N "${REMOTE_USER}@${REMOTE_HOST}" &
|
||
sleep 2
|
||
fi
|
||
}
|
||
|
||
ssh_disconnect() {
|
||
[ "$DRY_RUN" = true ] && return 0
|
||
local socket; socket=$(get_ssh_socket)
|
||
[ -S "${socket}" ] 2>/dev/null || return 0
|
||
ssh -o "ControlPath=${socket}" -O exit "${REMOTE_USER}@${REMOTE_HOST}" 2>/dev/null || true
|
||
}
|
||
|
||
# SSH 快捷执行(复用 ControlMaster)
|
||
remote() { timeout "${DEPLOY_TIMEOUT_SEC}" ssh -o "ConnectTimeout=10" -o "ServerAliveInterval=30" -o "ControlPath=$(get_ssh_socket)" "${REMOTE_USER}@${REMOTE_HOST}" "$@"; }
|
||
|
||
# ---------- 部署日志 ----------
|
||
get_deploy_log() { echo "${REMOTE_DIR}/deploy.log"; }
|
||
|
||
log_to_file() {
|
||
[ "$DRY_RUN" = true ] && return 0
|
||
remote "echo '[$(date '+%Y-%m-%d %H:%M:%S')] ${APP_ENV} ${GIT_VERSION} $1' >> $(get_deploy_log)" 2>/dev/null || true
|
||
}
|
||
|
||
# ---------- Webhook ----------
|
||
send_webhook() {
|
||
[ -z "${WEBHOOK_URL:-}" ] && return 0
|
||
local payload; payload=$(cat <<EOF
|
||
{"app":"rust-backend","env":"${APP_ENV}","status":"$1","version":"${GIT_VERSION}","message":"$2","timestamp":"$(date -Iseconds)"}
|
||
EOF
|
||
)
|
||
[ "$DRY_RUN" = true ] && { log_dry "curl -X POST '${WEBHOOK_URL}' -d '${payload}'"; return 0; }
|
||
curl -s -X POST "${WEBHOOK_URL}" -H "Content-Type: application/json" -d "${payload}" >/dev/null 2>&1 || true
|
||
}
|
||
|
||
# ---------- 环境配置(systemd service)----------
|
||
get_service_file_local() {
|
||
local target="${BG_TARGET:-}"
|
||
if [ -n "$target" ]; then
|
||
echo "${SCRIPT_DIR}/deploy/rust-backend-${target}.service"
|
||
else
|
||
echo "${SCRIPT_DIR}/deploy/rust-backend-${APP_ENV}.service"
|
||
fi
|
||
}
|
||
|
||
check_service_file() {
|
||
log_step "检查远程 systemd service 文件..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "检查 ${SERVICE_NAME}"; return 0; }
|
||
|
||
if ! remote "systemctl cat '${SERVICE_NAME}'" &>/dev/null; then
|
||
log_error "systemd service ${SERVICE_NAME} 不存在,请先用 --deploy-service 安装"
|
||
exit 1
|
||
fi
|
||
|
||
local content; content=$(remote "systemctl cat '${SERVICE_NAME}'" 2>/dev/null || echo "")
|
||
[ -z "$content" ] && { log_error "service 内容为空"; exit 1; }
|
||
|
||
local missing=0 ph=0
|
||
local required_vars=("DATABASE_URL" "JWT_SECRET" "WECHAT_APPID" "WECHAT_SECRET")
|
||
[ "${APP_ENV}" = "production" ] && required_vars+=("SSL_KEY_PATH" "SSL_CERT_PATH" "ALIPAY_APP_ID")
|
||
|
||
for var in "${required_vars[@]}"; do
|
||
local val; val=$(echo "$content" | grep -E "^Environment=${var}=" | head -1 | sed "s/^Environment=${var}=//")
|
||
if [ -z "$val" ]; then
|
||
log_error " 缺少: Environment=${var}="; missing=$((missing+1))
|
||
elif echo "$val" | grep -qiE "^(your_|placeholder|change_me|xxx|test_)"; then
|
||
log_warn " 占位符: ${var}=${val}"; ph=$((ph+1))
|
||
fi
|
||
done
|
||
|
||
[ $missing -gt 0 ] && { log_error "service 配置检查失败:${missing} 个缺失"; exit 1; }
|
||
[ $ph -gt 0 ] && log_warn "service 存在 ${ph} 个占位符,部署后可能无法正常运行"
|
||
log_info "service 配置检查通过"
|
||
}
|
||
|
||
# ---------- 蓝绿部署:检查目标是否为当前活动环境 ----------
|
||
NGINX_CONTAINER="1Panel-openresty-ABu5"
|
||
|
||
get_proxy_conf() {
|
||
if [ "${APP_ENV}" = "development" ]; then
|
||
echo "/www/sites/dev.xmclassmate.top/proxy/root.conf"
|
||
else
|
||
echo "/www/sites/xmclassmate.top/proxy/root.conf"
|
||
fi
|
||
}
|
||
|
||
check_not_active() {
|
||
[ -z "$BG_TARGET" ] && return 0 # 非蓝绿部署,跳过
|
||
|
||
local proxy_conf; proxy_conf=$(get_proxy_conf)
|
||
local active_env
|
||
active_env=$(remote "docker exec ${NGINX_CONTAINER} cat ${proxy_conf} 2>/dev/null" 2>/dev/null | grep -oP '127\.0\.0\.1:\d+' | head -1 || echo "")
|
||
|
||
local current_target
|
||
case "$active_env" in
|
||
*4433) current_target="blue" ;;
|
||
*4434) current_target="green" ;;
|
||
*) current_target="unknown" ;;
|
||
esac
|
||
|
||
if [ "$current_target" = "$BG_TARGET" ]; then
|
||
log_error "禁止部署: ${BG_TARGET} (:${BG_PORT}) 是当前活动环境"
|
||
log_error "请部署到待命环境: $( [ "$BG_TARGET" = "blue" ] && echo 'green' || echo 'blue' )"
|
||
exit 1
|
||
fi
|
||
log_info "目标 ${BG_TARGET} 非活动环境(当前: ${current_target}),可以部署 ✅"
|
||
}
|
||
|
||
deploy_service_file() {
|
||
log_step "部署 systemd service 文件..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "上传 $(get_service_file_local) → /etc/systemd/system/${SERVICE_NAME}"; return 0; }
|
||
|
||
if [ ! -f "$(get_service_file_local)" ]; then
|
||
log_error "本地 service 文件不存在: $(get_service_file_local)"
|
||
log_info "可通过 --init-env 生成模板"
|
||
exit 1
|
||
fi
|
||
|
||
rsync -avzP "$(get_service_file_local)" "${REMOTE_USER}@${REMOTE_HOST}:/etc/systemd/system/${SERVICE_NAME}"
|
||
|
||
# 开发环境自动配置 mock login(带随机密钥)
|
||
if [ "${APP_ENV}" = "development" ]; then
|
||
local has_mock; has_mock=$(remote "grep -c 'MOCK_LOGIN_ENABLED' /etc/systemd/system/${SERVICE_NAME} 2>/dev/null || echo 0")
|
||
if [ "$has_mock" = "0" ]; then
|
||
local mock_key; mock_key=$(openssl rand -base64 12 2>/dev/null | tr -d '\n' || echo "dev-mock-$(date +%s)")
|
||
remote "sed -i '/^\[Service\]/a Environment=MOCK_LOGIN_ENABLED=true\nEnvironment=MOCK_LOGIN_KEY=${mock_key}' /etc/systemd/system/${SERVICE_NAME}"
|
||
log_info "自动配置 MOCK_LOGIN_ENABLED=true, MOCK_LOGIN_KEY=${mock_key}"
|
||
fi
|
||
fi
|
||
|
||
remote "systemctl daemon-reload"
|
||
log_info "service 文件部署完成,使用 systemctl restart ${SERVICE_NAME} 重启"
|
||
}
|
||
|
||
init_env() {
|
||
log_step "初始化 systemd service 文件(从模板生成)..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "生成 $(get_service_file_local)"; return 0; }
|
||
|
||
if [ -f "$(get_service_file_local)" ]; then
|
||
log_warn "$(get_service_file_local) 已存在,继续将覆盖!"
|
||
read -p "是否覆盖? [y/N] " -n 1 -r; echo
|
||
[[ ! $REPLY =~ ^[Yy]$ ]] && { log_warn "已取消"; return 0; }
|
||
cp "$(get_service_file_local)" "$(get_service_file_local).bak.$(date +%Y%m%d_%H%M%S)"
|
||
fi
|
||
|
||
local jwt_secret
|
||
jwt_secret=$(openssl rand -base64 32 2>/dev/null | tr -d '\n' || echo "change-me-$(date +%s)-$$")
|
||
|
||
mkdir -p "${SCRIPT_DIR}/deploy"
|
||
cat > "$(get_service_file_local)" << SERVEOF
|
||
[Unit]
|
||
Description=Rust Backend ${APP_ENV^} Service
|
||
After=network.target postgresql.service
|
||
Wants=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
Group=root
|
||
WorkingDirectory=${REMOTE_DIR}
|
||
ExecStart=${REMOTE_DIR}/rust-backend
|
||
Restart=always
|
||
RestartSec=5
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
|
||
Environment=DATABASE_URL=请替换为实际数据库连接
|
||
Environment=WECHAT_APPID=请替换为实际微信 AppID
|
||
Environment=WECHAT_SECRET=请替换为实际微信 Secret
|
||
Environment=JWT_SECRET=${jwt_secret}
|
||
Environment=SSL_KEY_PATH=
|
||
Environment=SSL_CERT_PATH=
|
||
Environment=RUST_LOG=info
|
||
Environment=APP_VERSION=0.3.0
|
||
Environment=FREE_USER_DATA_LIMIT=20
|
||
SERVEOF
|
||
|
||
chmod 600 "$(get_service_file_local)"
|
||
log_info "service 模板已生成: $(get_service_file_local)"
|
||
log_warn "请手动填写 DATABASE_URL / WECHAT_APPID / WECHAT_SECRET"
|
||
log_info "然后使用 ./deploy.sh ${APP_ENV} --deploy-service 上传"
|
||
}
|
||
|
||
# ---------- 编译 ----------
|
||
build_project() {
|
||
log_step "编译项目 (release 模式)..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "cargo build --release"; return 0; }
|
||
APP_ENV="${APP_ENV}" cargo build --release || { log_error "编译失败!"; exit 1; }
|
||
log_info "编译成功"
|
||
}
|
||
|
||
# ---------- 备份 ----------
|
||
backup_old_version() {
|
||
log_step "备份旧版本..."
|
||
local ts; ts=$(date +%Y%m%d_%H%M%S)
|
||
local gh; gh=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||
local md5=""
|
||
[ "$DRY_RUN" = false ] && md5=$(remote "md5sum '${REMOTE_DIR}/${PROJECT_NAME}' 2>/dev/null | cut -d' ' -f1" || echo "")
|
||
BACKUP_PATH="${REMOTE_DIR}/${PROJECT_NAME}.backup.${ts}.${gh}.${md5:0:8}"
|
||
|
||
[ "$DRY_RUN" = true ] && { log_dry "备份到 ${BACKUP_PATH}"; return 0; }
|
||
remote "if [ -f '${REMOTE_DIR}/${PROJECT_NAME}' ]; then cp '${REMOTE_DIR}/${PROJECT_NAME}' '${BACKUP_PATH}'; echo '${BACKUP_PATH}' > '${REMOTE_DIR}/.last_deployed'; fi"
|
||
log_info "备份完成"
|
||
}
|
||
|
||
cleanup_old_backups() {
|
||
local keep="${1:-5}"
|
||
[ "$DRY_RUN" = true ] && { log_dry "保留最近 ${keep} 个备份"; return 0; }
|
||
|
||
local backups; backups=$(remote "ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null" || echo "")
|
||
[ -z "$backups" ] && return 0
|
||
|
||
local seen="" to_del=""
|
||
while IFS= read -r b; do
|
||
[ -z "$b" ] && continue
|
||
local md5; md5=$(basename "$b" | rev | cut -d'.' -f2 | rev || echo "")
|
||
[ -z "$md5" ] && { to_del="${to_del}${b}"$'\n'; continue; }
|
||
echo "$seen" | grep -q "^${md5}$" && to_del="${to_del}${b}"$'\n' || seen="${seen}${md5}"$'\n'
|
||
done <<< "$backups"
|
||
|
||
# 如果唯一版本数超过 keep,按时间删除最旧的溢出部分
|
||
local count=0
|
||
while IFS= read -r b; do
|
||
[ -z "$b" ] && continue; count=$((count+1))
|
||
[ "$count" -gt "$keep" ] && to_del="${to_del}${b}"$'\n'
|
||
done <<< "$backups"
|
||
|
||
[ -z "$to_del" ] && return 0
|
||
log_step "清理旧备份(保留 ${keep} 个版本)..."
|
||
echo "$to_del" | while IFS= read -r f; do
|
||
[ -n "$f" ] && remote "rm -f '${f}'" 2>/dev/null && echo " 删除: $(basename "$f")" || true
|
||
done
|
||
}
|
||
|
||
backup_database() {
|
||
log_step "备份数据库..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "pg_dump → ${REMOTE_DIR}/backups/"; return 0; }
|
||
local ts; ts=$(date +%Y%m%d_%H%M%S)
|
||
local bf="${REMOTE_DIR}/backups/${DB_NAME}_${ts}.dump"
|
||
remote "mkdir -p ${REMOTE_DIR}/backups"
|
||
if remote "docker exec ${DB_CONTAINER} pg_dump -Fc -U ${DB_USER} ${DB_NAME} > /tmp/backup_${ts}.dump && mv /tmp/backup_${ts}.dump '${bf}'" 2>&1; then
|
||
log_info "数据库备份成功: ${DB_NAME}_${ts}.dump"
|
||
cleanup_old_db_backups
|
||
else
|
||
log_warn "数据库备份失败,继续部署"
|
||
fi
|
||
}
|
||
|
||
cleanup_old_db_backups() {
|
||
local keep="${1:-1}"
|
||
local to_del; to_del=$(remote "ls -t ${REMOTE_DIR}/backups/${DB_NAME}_*.dump 2>/dev/null | tail -n +$((keep+1))" || echo "")
|
||
[ -z "$to_del" ] && return 0
|
||
log_step "清理旧数据库备份(保留 ${keep} 个)..."
|
||
echo "$to_del" | while read -r f; do [ -n "$f" ] && remote "rm -f '${f}'" 2>/dev/null && echo " 删除: $(basename "$f")" || true; done
|
||
}
|
||
|
||
list_backups() {
|
||
log_step "可用备份列表..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "ls ${REMOTE_DIR}/${PROJECT_NAME}.backup.*"; return 0; }
|
||
remote "ls -la ${REMOTE_DIR}/${PROJECT_NAME}.backup.*" 2>/dev/null || echo "无备份文件"
|
||
}
|
||
|
||
# ---------- 上传 ----------
|
||
upload_binary() {
|
||
log_step "上传二进制文件..."
|
||
local src="./target/release/${PROJECT_NAME}"
|
||
[ ! -f "$src" ] && { log_error "编译产物不存在: $src"; exit 1; }
|
||
[ "$DRY_RUN" = true ] && { log_dry "rsync ${src} → ${REMOTE_DIR}/"; return 0; }
|
||
rsync -avzP --progress "$src" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
|
||
log_info "上传完成"
|
||
}
|
||
|
||
upload_dir() {
|
||
local label="$1" src="$2" dst="${3:-$2}"
|
||
log_step "上传 ${label}..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "rsync ${src}/ → ${REMOTE_DIR}/${dst}/"; return 0; }
|
||
[ ! -d "$src" ] && { log_warn "${src} 目录不存在,跳过"; return 0; }
|
||
rsync -avzP --progress "${src}/" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/${dst}/"
|
||
log_info "${label} 上传完成"
|
||
}
|
||
|
||
upload_script() {
|
||
local label="$1" src="$2" dst="$3"
|
||
log_step "上传 ${label}..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "rsync ${src} → ${REMOTE_DIR}/${dst}"; return 0; }
|
||
[ ! -f "$src" ] && { log_warn "${src} 不存在,跳过"; return 0; }
|
||
rsync -avzP --progress "$src" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/${dst}"
|
||
remote "chmod +x ${REMOTE_DIR}/${dst}" 2>/dev/null || true
|
||
log_info "${label} 上传完成"
|
||
}
|
||
|
||
# ---------- 迁移 ----------
|
||
run_migrations() {
|
||
log_step "执行数据库迁移..."
|
||
[ "$DRY_RUN" = true ] && { local n; n=$(ls ./migrations/*.sql 2>/dev/null | wc -l || echo 0); log_dry "发现 ${n} 个迁移文件"; return 0; }
|
||
|
||
# 先备份再执行
|
||
local sql_files; sql_files=$(remote "ls ${REMOTE_DIR}/migrations/*.sql 2>/dev/null | sort" || echo "")
|
||
[ -n "$sql_files" ] && backup_database
|
||
|
||
# 创建迁移追踪表
|
||
remote "docker exec -i ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}" <<<'CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMPTZ DEFAULT NOW());' 2>/dev/null || true
|
||
|
||
for f in $sql_files; do
|
||
local name; name=$(basename "$f")
|
||
log_info "检查迁移: ${name}"
|
||
local done; done=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \"SELECT COUNT(*) FROM _migrations WHERE name='${name}'\"" 2>/dev/null || echo "0")
|
||
[ "$done" != "0" ] && { log_info " 跳过(已执行): ${name}"; continue; }
|
||
log_info " 执行: ${name}"
|
||
remote "cat '${f}' | docker exec -i ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}" && \
|
||
remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"INSERT INTO _migrations (name) VALUES ('${name}')\"" && \
|
||
log_info " 完成: ${name}" || log_warn " 迁移 ${name} 可能已执行或失败"
|
||
done
|
||
log_info "数据库迁移完成"
|
||
}
|
||
|
||
# ---------- 服务 ----------
|
||
restart_service() {
|
||
log_step "重启服务 '${SERVICE_NAME}'..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "systemctl restart ${SERVICE_NAME}"; return 0; }
|
||
# 部署前先确保 service 文件存在
|
||
if ! remote "systemctl cat '${SERVICE_NAME}'" &>/dev/null; then
|
||
log_warn "${SERVICE_NAME} 未安装,尝试部署 service 文件..."
|
||
deploy_service_file
|
||
fi
|
||
remote "systemctl restart ${SERVICE_NAME}" && log_info "服务重启成功" || log_error "服务重启失败!"
|
||
sleep 3
|
||
}
|
||
|
||
# ---------- 回滚 ----------
|
||
rollback() {
|
||
log_step "查找可用备份..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "回滚预览"; return 0; }
|
||
|
||
local backups
|
||
backups=$(remote "ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null" || echo "")
|
||
[ -z "$backups" ] && { log_error "没有可用备份"; return 1; }
|
||
|
||
echo ""; echo -e "${BLUE}可用备份:${NC}"; echo "$backups" | head -10 | nl -v 0
|
||
local count; count=$(echo "$backups" | wc -l)
|
||
read -p "选择备份 [0-$(($count>10?9:$count-1)),默认 0]: " -n 1 -r; echo
|
||
[[ ! $REPLY =~ ^[0-9]$ ]] && REPLY=0
|
||
local chosen; chosen=$(echo "$backups" | sed -n "$((REPLY+1))p")
|
||
|
||
echo -e "${YELLOW}回滚到: ${chosen}${NC}"
|
||
[ "$SKIP_CONFIRM" = false ] && { read -p "确认? [y/N] " -n 1 -r; echo; [[ ! $REPLY =~ ^[Yy]$ ]] && { log_warn "已取消"; return 0; } }
|
||
|
||
remote "systemctl stop ${SERVICE_NAME}" 2>/dev/null || true
|
||
remote "cp '${chosen}' '${REMOTE_DIR}/${PROJECT_NAME}'"
|
||
remote "systemctl start ${SERVICE_NAME}"
|
||
log_info "回滚完成"
|
||
log_to_file "MANUAL_ROLLBACK to $(basename ${chosen})"
|
||
send_webhook "rollback" "手动回滚: $(basename ${chosen})"
|
||
}
|
||
|
||
rollback_on_failure() {
|
||
local bp="$1"; [ -z "$bp" ] && { log_error "未指定备份路径"; return 1; }
|
||
log_error "========== 测试失败,自动回滚 =========="
|
||
[ "$DRY_RUN" = true ] && { log_dry "回滚到 ${bp}"; return 0; }
|
||
remote "systemctl stop ${SERVICE_NAME}" 2>/dev/null || true
|
||
remote "cp '${bp}' '${REMOTE_DIR}/${PROJECT_NAME}'"
|
||
remote "systemctl start ${SERVICE_NAME}"; sleep 3
|
||
log_info "自动回滚完成"
|
||
log_to_file "AUTO_ROLLBACK to $(basename ${bp})"
|
||
send_webhook "failure" "部署失败,自动回滚: $(basename ${bp})"
|
||
}
|
||
|
||
# ---------- 日志查看 ----------
|
||
view_logs() {
|
||
local lines="${LOG_LINES:-50}"
|
||
log_step "查看后端日志(最后 ${lines} 行)..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "tail -${lines} 日志文件"; return 0; }
|
||
local lf; lf=$(remote "ls -t ${REMOTE_DIR}/logs/rust-backend.*.log 2>/dev/null | head -1" || echo "")
|
||
[ -z "$lf" ] && { log_warn "未找到日志文件"; return 1; }
|
||
echo -e "${BLUE}=== ${lf} ===${NC}"; remote "tail -${lines} '${lf}'"
|
||
}
|
||
|
||
tail_logs() {
|
||
local lines="${LOG_LINES:-100}"
|
||
log_step "实时跟踪日志..."
|
||
[ "$DRY_RUN" = true ] && return 0
|
||
local lf; lf=$(remote "ls -t ${REMOTE_DIR}/logs/rust-backend.*.log 2>/dev/null | head -1" || echo "")
|
||
[ -z "$lf" ] && { log_warn "未找到日志文件"; return 1; }
|
||
ssh -t "${REMOTE_USER}@${REMOTE_HOST}" "tail -f -n ${lines} '${lf}'"
|
||
}
|
||
|
||
status() {
|
||
log_step "服务状态..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "systemctl status ${SERVICE_NAME}"; return 0; }
|
||
echo -e "${BLUE}=== ${SERVICE_NAME} ===${NC}"
|
||
remote "systemctl status ${SERVICE_NAME} --no-pager"
|
||
local lf; lf=$(remote "ls -t ${REMOTE_DIR}/logs/rust-backend.*.log 2>/dev/null | head -1" || echo "")
|
||
[ -n "$lf" ] && { echo -e "${BLUE}=== 最近日志 ===${NC}"; remote "tail -20 '${lf}'"; }
|
||
}
|
||
|
||
# ---------- 确认 ----------
|
||
confirm_deploy() {
|
||
[ "$DRY_RUN" = true ] || [ "$SKIP_CONFIRM" = true ] && return 0
|
||
echo ""; echo -e "${YELLOW}========== 部署确认 ==========${NC}"
|
||
echo -e " 环境: ${APP_ENV}"
|
||
echo -e " 远程目录: ${REMOTE_DIR}"
|
||
echo -e " 服务: ${SERVICE_NAME}"
|
||
echo -e " Git: ${GIT_VERSION}"
|
||
echo -e "================================${NC}"
|
||
read -p "确认部署? [y/N] " -n 1 -r; echo
|
||
[[ ! $REPLY =~ ^[Yy]$ ]] && { log_warn "已取消"; exit 0; }
|
||
}
|
||
|
||
show_deploy_info() {
|
||
echo ""; echo -e "${BLUE}========================================${NC}"
|
||
echo -e "${BLUE} Rust Backend 部署脚本${NC}"
|
||
echo -e "${BLUE}========================================${NC}"
|
||
echo -e " 项目: ${PROJECT_NAME}"
|
||
echo -e " 环境: ${APP_ENV}"
|
||
echo -e " 远程主机: ${REMOTE_USER}@${REMOTE_HOST}"
|
||
echo -e " 远程目录: ${REMOTE_DIR}"
|
||
echo -e " 服务: ${SERVICE_NAME}"
|
||
echo -e " Git 版本: ${GIT_VERSION}"
|
||
[ "$DRY_RUN" = true ] && echo -e " 模式: ${YELLOW}预览 (DRY-RUN)${NC}"
|
||
echo -e "${BLUE}========================================${NC}"
|
||
}
|
||
|
||
# ---------- SSL 证书检查 ----------
|
||
check_ssl_expiry() {
|
||
[ "${APP_ENV}" != "production" ] && return 0 # 非生产环境跳过
|
||
log_step "检查 SSL 证书到期时间..."
|
||
[ "$DRY_RUN" = true ] && { log_dry "openssl x509 -checkend 2592000"; return 0; }
|
||
|
||
# 从 OpenResty 容器内读取证书
|
||
local cert_info; cert_info=$(remote "docker exec 1Panel-openresty-ABu5 openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -enddate -checkend 2592000 2>/dev/null" || echo "")
|
||
|
||
if echo "$cert_info" | grep -q "Certificate will expire"; then
|
||
local expiry; expiry=$(remote "docker exec 1Panel-openresty-ABu5 openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -enddate 2>/dev/null | cut -d= -f2" || echo "未知")
|
||
local days_left; days_left=$(remote "docker exec 1Panel-openresty-ABu5 openssl x509 -in /www/sites/${TEST_DOMAIN#https://}/ssl/fullchain.pem -noout -checkend 86400 2>/dev/null | grep -c 'will expire'" || echo "0")
|
||
log_warn "SSL 证书将于 ${expiry} 到期(不足 30 天)!"
|
||
[ "$days_left" -gt "0" ] && log_warn " 警告:证书将在 1 天内到期,请立即续期!"
|
||
else
|
||
log_info "SSL 证书有效期超过 30 天 ✅"
|
||
fi
|
||
}
|
||
|
||
# ---------- 依赖检查 ----------
|
||
check_dependencies() {
|
||
log_step "检查依赖..."
|
||
for cmd in cargo rsync ssh; do
|
||
command -v "$cmd" &>/dev/null || { log_error "依赖 '$cmd' 未安装"; exit 1; }
|
||
done
|
||
log_info "依赖检查通过"
|
||
}
|
||
|
||
# ---------- 清理 ----------
|
||
cleanup() { ssh_disconnect 2>/dev/null || true; }
|
||
trap cleanup EXIT
|