#!/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 链路"