Files
novalon-website/infra/cicd/backup/restore-cicd.sh
T
zhangxiang 4ab2f3cd8e chore(infra): 新增 Gitea+Jenkins CI/CD 部署与凭据整改
- infra/cicd:docker-compose(gitea/jenkins)、JCasC(凭据统一 ${ENV} 注入,无硬编码)、
  备份/恢复、健康检查、凭据轮换与 git 历史清除脚本
- docs/deployment/cicd:安装、高可用备份监控、凭据事故复盘
- .env.example 仅为占位模板;真实 .env 由 .gitignore 排除
2026-09-20 10:37:57 +08:00

142 lines
5.9 KiB
Bash
Executable File
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.
#!/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 <dir> --component jenkins # 只恢复 Jenkins
# ./restore-cicd.sh --from <dir> --component gitea
# ./restore-cicd.sh --from <dir> --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 链路"