#!/usr/bin/env bash # ============================================================ # Novalon CI/CD 恢复脚本(backup-cicd.sh 的逆向) # # 铁律:未做过恢复演练的备份等于没有备份。 # 建议每季度在临时目录做一次 --dry-run 级别演练。 # # 用法: # ./restore-cicd.sh --from /home/novalon/backups/cicd/20260914_033000 # 全量恢复 # ./restore-cicd.sh --from --component jenkins # 只恢复 Jenkins # ./restore-cicd.sh --from --component gitea # ./restore-cicd.sh --from --dry-run # 只校验归档,不写入 # # ⚠️ 恢复前必须先停服务,否则会写入半一致状态。脚本默认会停,用 --no-stop 可跳过。 # ============================================================ set -Eeuo pipefail FROM="" COMPONENT="all" # all | gitea | jenkins DRY_RUN=0 NO_STOP=0 GITEA_CONTAINER="${GITEA_CONTAINER:-gitea}" PG_CONTAINER="${PG_CONTAINER:-postgresql}" JENKINS_CONTAINER="${JENKINS_CONTAINER:-jenkins}" GITEA_DB_NAME="${GITEA_DB_NAME:-gitea}" GITEA_DB_USER="${GITEA_DB_USER:-gitea}" COMPOSE_DIR="${COMPOSE_DIR:-/home/novalon/docker-app/novalon-cicd}" while [[ $# -gt 0 ]]; do case "$1" in --from) FROM="$2"; shift 2 ;; --component) COMPONENT="$2"; shift 2 ;; --dry-run) DRY_RUN=1; shift ;; --no-stop) NO_STOP=1; shift ;; -h|--help) sed -n '2,22p' "$0"; exit 0 ;; *) echo "未知参数: $1" >&2; exit 2 ;; esac done log() { echo "[restore] $*"; } fail() { echo "[restore] ❌ $*" >&2; exit 1; } trap 'fail "第 $LINENO 行发生未捕获错误"' ERR [[ -n "$FROM" && -d "$FROM" ]] || fail "请指定有效的 --from 备份目录" log "备份源: $FROM" log "范围: $COMPONENT (dry-run=$DRY_RUN)" cat "$FROM/MANIFEST.txt" 2>/dev/null || log "⚠️ 无 MANIFEST.txt,无法核对版本" # ---------- 定位产物 ---------- DB_DUMP="$(ls "$FROM"/gitea-db-*.sql.gz 2>/dev/null | tail -1 || true)" REPO_TAR="$(ls "$FROM"/gitea-repos-*.tar.gz 2>/dev/null | tail -1 || true)" JENKINS_TAR="$(ls "$FROM"/jenkins-config-*.tar.gz 2>/dev/null | tail -1 || true)" # ---------- 归档校验 ---------- log "----- 归档完整性校验 -----" [[ "$COMPONENT" == "jenkins" ]] || { [[ -s "$DB_DUMP" ]] && gzip -t "$DB_DUMP" || fail "gitea-db 归档无效: ${DB_DUMP:-缺失}"; } [[ "$COMPONENT" == "jenkins" ]] || { [[ -s "$REPO_TAR" ]] && tar -tzf "$REPO_TAR" >/dev/null || fail "gitea-repos 归档无效: ${REPO_TAR:-缺失}"; } [[ "$COMPONENT" == "gitea" ]] || { [[ -s "$JENKINS_TAR" ]] && tar -tzf "$JENKINS_TAR" >/dev/null || fail "jenkins 归档无效: ${JENKINS_TAR:-缺失}"; } # Jenkins 凭据可解密性校验(最容易踩的坑) if [[ "$COMPONENT" != "gitea" ]]; then if tar -tzf "$JENKINS_TAR" | grep -q "secret.key"; then log "✅ 归档含 secret.key,凭据可解密" else fail "归档缺少 secret.key —— 恢复后所有 Jenkins 凭据将不可解密。请改用含 secret.key 的备份。" fi fi log "✅ 全部归档校验通过" if (( DRY_RUN == 1 )); then log "dry-run 完成,未写入任何数据。" exit 0 fi # ---------- 二次确认 ---------- echo echo "⚠️ 即将覆盖现有 Gitea / Jenkins 数据,此操作不可撤销。" read -r -p "请输入 'RESTORE' 以继续: " CONFIRM [[ "$CONFIRM" == "RESTORE" ]] || fail "未确认,已中止" # ---------- 停服务 ---------- if (( NO_STOP == 0 )); then log "停止服务..." docker stop "$JENKINS_CONTAINER" >/dev/null 2>&1 || true docker stop "$GITEA_CONTAINER" >/dev/null 2>&1 || true # PostgreSQL 保持运行(恢复需要它在线),只停 Gitea 与 Jenkins docker start "$PG_CONTAINER" >/dev/null 2>&1 || true fi # ============================================================ # Gitea 恢复 # ============================================================ if [[ "$COMPONENT" != "jenkins" ]]; then log "----- 恢复 PostgreSQL (Gitea 库) -----" # 先建一个安全网:把当前库导出一份(若库还存在) if docker exec "$PG_CONTAINER" pg_isready -U "$GITEA_DB_USER" >/dev/null 2>&1; then docker exec "$PG_CONTAINER" pg_dump -U "$GITEA_DB_USER" -d "$GITEA_DB_NAME" -Fc \ | gzip -9 > "/tmp/gitea-db-pre-restore-$(date +%s).sql.gz" \ && log "已保存恢复前快照到 /tmp" || log "⚠️ 恢复前快照失败(继续)" fi gunzip -c "$DB_DUMP" | docker exec -i "$PG_CONTAINER" \ pg_restore -U "$GITEA_DB_USER" -d "$GITEA_DB_NAME" --clean --if-exists --no-owner \ || log "⚠️ pg_restore 返回非 0(--clean 对不存在的对象会告警,通常可忽略)" log "✅ 数据库恢复完成" log "----- 恢复 Gitea 仓库 -----" docker run --rm --volumes-from "$GITEA_CONTAINER" -v "$(dirname "$REPO_TAR"):/in" alpine \ sh -c 'tar xzf /in/'"$(basename "$REPO_TAR")"' -C /data' \ || fail "仓库恢复失败" log "✅ 仓库恢复完成" fi # ============================================================ # Jenkins 恢复 # ============================================================ if [[ "$COMPONENT" != "gitea" ]]; then log "----- 恢复 Jenkins 配置 -----" docker run --rm --volumes-from "$JENKINS_CONTAINER" -v "$(dirname "$JENKINS_TAR"):/in" alpine \ sh -c 'cd /var/jenkins_home && tar xzf /in/'"$(basename "$JENKINS_TAR")" \ || fail "Jenkins 恢复失败" log "✅ Jenkins 配置恢复完成" fi # ---------- 起服务 ---------- log "启动服务..." docker start "$GITEA_CONTAINER" >/dev/null 2>&1 || true docker start "$JENKINS_CONTAINER" >/dev/null 2>&1 || true log "等待健康检查..." sleep 20 docker ps --filter "name=$GITEA_CONTAINER" --filter "name=$JENKINS_CONTAINER" \ --format "{{.Names}}: {{.Status}}" log "========== 恢复完成 ==========" log "请人工核验:" log " 1. https://git.f.novalon.cn 仓库列表与最近提交" log " 2. https://ci.f.novalon.cn 作业 novalon-website-ci-cd 与凭据 gitea-credentials" log " 3. 触发一次构建验证 webhook 链路"