- infra/cicd:docker-compose(gitea/jenkins)、JCasC(凭据统一 ${ENV} 注入,无硬编码)、
备份/恢复、健康检查、凭据轮换与 git 历史清除脚本
- docs/deployment/cicd:安装、高可用备份监控、凭据事故复盘
- .env.example 仅为占位模板;真实 .env 由 .gitignore 排除
142 lines
5.9 KiB
Bash
Executable File
142 lines
5.9 KiB
Bash
Executable File
#!/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 链路"
|