Files
asd-backend/lib/common.sh
milky0217 917d69c171
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
feat: Graceful shutdown (10s);backup 格式兼容 .dump.gz;容器连接监控
2026-07-15 11:05:11 +08:00

486 lines
21 KiB
Bash
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.
# ===========================================
# 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 配置检查通过"
}
# ---------- 蓝绿部署:检查目标是否为当前活动环境 ----------
get_proxy_conf() {
if [ "${APP_ENV}" = "development" ]; then
echo "/etc/nginx/sites-enabled/dev.xmclassmate.top"
else
echo "/etc/nginx/sites-enabled/xmclassmate.top"
fi
}
check_not_active() {
[ -z "$BG_TARGET" ] && return 0 # 非蓝绿部署,跳过
local proxy_conf; proxy_conf=$(get_proxy_conf)
local active_env
active_env=$(remote "sudo 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 "sudo 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 7
else
log_warn "数据库备份失败,继续部署"
fi
}
cleanup_old_db_backups() {
local keep="${1:-7}"
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 "sudo 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 "sudo 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}' | sudo docker exec -i ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}" && \
remote "sudo 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; }
# 读取 SSL 证书
local cert_info; cert_info=$(remote "sudo 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 "sudo 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 "sudo 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