fix(gitea): expose bundle backup restore readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m1s
CD Pipeline / build-and-deploy (push) Successful in 4m25s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Failing after 29s
CD Pipeline / post-deploy-checks (push) Successful in 1m45s
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m1s
CD Pipeline / build-and-deploy (push) Successful in 4m25s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Failing after 29s
CD Pipeline / post-deploy-checks (push) Successful in 1m45s
This commit is contained in:
149
scripts/backup/gitea-bundle-sample-restore-dry-run.sh
Executable file
149
scripts/backup/gitea-bundle-sample-restore-dry-run.sh
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify one Gitea bundle can be restored into a temporary mirror clone.
|
||||
# This is a dry-run verifier only. It does not restore into production, mutate
|
||||
# Gitea, read credentials, delete repositories, or change repo visibility.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${AIOPS_GITEA_BUNDLE_ROOT:-/home/ollama/backup/110/gitea/git-bundles/latest-private-complete}"
|
||||
REPO="${AIOPS_GITEA_BUNDLE_SAMPLE_REPO:-wooo/awoooi}"
|
||||
HOST_LABEL="${AIOPS_HOST_LABEL:-188}"
|
||||
TEXTFILE_PATH="${AIOPS_GITEA_BUNDLE_RESTORE_TEXTFILE:-/home/ollama/node_exporter_textfiles/gitea_bundle_restore_dry_run.prom}"
|
||||
WRITE_TEXTFILE=0
|
||||
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-120}"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/backup/gitea-bundle-sample-restore-dry-run.sh [options]
|
||||
|
||||
Options:
|
||||
--root PATH Bundle root directory.
|
||||
--repo OWNER/NAME Repository sample to verify. Default: wooo/awoooi.
|
||||
--write-textfile Write node-exporter textfile metric after verification.
|
||||
--textfile PATH Textfile metric path.
|
||||
-h, --help Show this help.
|
||||
|
||||
This verifier clones the bundle into a temporary mirror and runs git fsck.
|
||||
It never restores to production, never contacts Gitea, and never reads tokens.
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--root)
|
||||
shift
|
||||
ROOT="${1:?--root requires PATH}"
|
||||
;;
|
||||
--repo)
|
||||
shift
|
||||
REPO="${1:?--repo requires OWNER/NAME}"
|
||||
;;
|
||||
--write-textfile)
|
||||
WRITE_TEXTFILE=1
|
||||
;;
|
||||
--textfile)
|
||||
shift
|
||||
TEXTFILE_PATH="${1:?--textfile requires PATH}"
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
slug="${REPO//\//__}"
|
||||
repo_name="${REPO##*/}"
|
||||
bundle_path=""
|
||||
checksum_path=""
|
||||
|
||||
if [ -f "${ROOT}/${slug}.bundle" ]; then
|
||||
bundle_path="${ROOT}/${slug}.bundle"
|
||||
elif [ -f "${ROOT}/manifest.remote.tsv" ] || [ -f "${ROOT}/manifest.tsv" ]; then
|
||||
manifest="${ROOT}/manifest.remote.tsv"
|
||||
[ -f "$manifest" ] || manifest="${ROOT}/manifest.tsv"
|
||||
bundle_path="$(awk -F '\t' -v repo="$REPO" 'NR > 1 && $1 == repo {print $4; exit}' "$manifest")"
|
||||
if [ -n "$bundle_path" ] && [ ! -f "$bundle_path" ] && [ -f "${ROOT}/$(basename "$bundle_path")" ]; then
|
||||
bundle_path="${ROOT}/$(basename "$bundle_path")"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$bundle_path" ] || [ ! -f "$bundle_path" ]; then
|
||||
first_match="$(find "$ROOT" -maxdepth 1 -type f -name "${repo_name}-local-*.bundle" | sort | tail -n 1)"
|
||||
bundle_path="$first_match"
|
||||
fi
|
||||
|
||||
ok=0
|
||||
ref_count=0
|
||||
error_reason=""
|
||||
tmp_root=""
|
||||
|
||||
if [ -z "$bundle_path" ] || [ ! -f "$bundle_path" ]; then
|
||||
error_reason="bundle_not_found"
|
||||
else
|
||||
checksum_path="${bundle_path}.sha256"
|
||||
if [ -f "$checksum_path" ]; then
|
||||
expected="$(awk 'NR == 1 {print $1}' "$checksum_path")"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual="$(sha256sum "$bundle_path" | awk '{print $1}')"
|
||||
else
|
||||
actual="$(shasum -a 256 "$bundle_path" | awk '{print $1}')"
|
||||
fi
|
||||
if [ "$expected" != "$actual" ]; then
|
||||
error_reason="checksum_mismatch"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$error_reason" ]; then
|
||||
tmp_root="$(mktemp -d "${TMPDIR:-/tmp}/gitea-bundle-restore.XXXXXX")"
|
||||
trap 'rm -rf "$tmp_root"' EXIT
|
||||
if ! timeout "$TIMEOUT_SECONDS" git clone --mirror --quiet "$bundle_path" "${tmp_root}/restore.git"; then
|
||||
error_reason="clone_failed"
|
||||
elif ! timeout "$TIMEOUT_SECONDS" git -C "${tmp_root}/restore.git" fsck --connectivity-only >/dev/null; then
|
||||
error_reason="fsck_failed"
|
||||
else
|
||||
ref_count="$(git -C "${tmp_root}/restore.git" for-each-ref --format='%(refname)' | wc -l | tr -d ' ')"
|
||||
ok=1
|
||||
fi
|
||||
fi
|
||||
|
||||
timestamp="$(date +%s)"
|
||||
bundle_name="$(basename "${bundle_path:-missing}")"
|
||||
root_label="${ROOT//\\/\\\\}"
|
||||
root_label="${root_label//\"/\\\"}"
|
||||
repo_label="${REPO//\\/\\\\}"
|
||||
repo_label="${repo_label//\"/\\\"}"
|
||||
bundle_label="${bundle_name//\\/\\\\}"
|
||||
bundle_label="${bundle_label//\"/\\\"}"
|
||||
error_label="${error_reason//\\/\\\\}"
|
||||
error_label="${error_label//\"/\\\"}"
|
||||
|
||||
if [ "$WRITE_TEXTFILE" -eq 1 ]; then
|
||||
mkdir -p "$(dirname "$TEXTFILE_PATH")"
|
||||
tmp_textfile="${TEXTFILE_PATH}.$$"
|
||||
{
|
||||
echo '# HELP awoooi_gitea_bundle_sample_restore_dry_run_ok Whether sample Gitea bundle restore dry-run succeeded.'
|
||||
echo '# TYPE awoooi_gitea_bundle_sample_restore_dry_run_ok gauge'
|
||||
printf 'awoooi_gitea_bundle_sample_restore_dry_run_ok{host="%s",repo="%s",root="%s",bundle="%s",error="%s"} %s\n' "$HOST_LABEL" "$repo_label" "$root_label" "$bundle_label" "$error_label" "$ok"
|
||||
printf 'awoooi_gitea_bundle_sample_restore_dry_run_timestamp{host="%s",repo="%s"} %s\n' "$HOST_LABEL" "$repo_label" "$timestamp"
|
||||
printf 'awoooi_gitea_bundle_sample_restore_dry_run_ref_count{host="%s",repo="%s"} %s\n' "$HOST_LABEL" "$repo_label" "$ref_count"
|
||||
} > "$tmp_textfile"
|
||||
mv "$tmp_textfile" "$TEXTFILE_PATH"
|
||||
fi
|
||||
|
||||
printf 'GITEA_BUNDLE_SAMPLE_RESTORE_DRY_RUN_OK=%s\n' "$ok"
|
||||
printf 'REPO=%s\n' "$REPO"
|
||||
printf 'BUNDLE=%s\n' "${bundle_path:-missing}"
|
||||
printf 'REF_COUNT=%s\n' "$ref_count"
|
||||
printf 'TEXTFILE_WRITTEN=%s\n' "$WRITE_TEXTFILE"
|
||||
if [ -n "$error_reason" ]; then
|
||||
printf 'ERROR=%s\n' "$error_reason"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "gitea-bundle-sample-restore-dry-run.sh"
|
||||
|
||||
|
||||
def run(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(command, cwd=cwd, text=True, capture_output=True, check=True)
|
||||
|
||||
|
||||
def test_gitea_bundle_sample_restore_dry_run_writes_success_metric(tmp_path: Path) -> None:
|
||||
source = tmp_path / "source"
|
||||
bundle_root = tmp_path / "bundles"
|
||||
textfile = tmp_path / "gitea_bundle_restore_dry_run.prom"
|
||||
source.mkdir()
|
||||
bundle_root.mkdir()
|
||||
|
||||
run(["git", "init", "-q"], source)
|
||||
run(["git", "config", "user.email", "codex@example.invalid"], source)
|
||||
run(["git", "config", "user.name", "Codex"], source)
|
||||
(source / "README.md").write_text("restore dry-run\n", encoding="utf-8")
|
||||
run(["git", "add", "README.md"], source)
|
||||
run(["git", "commit", "-q", "-m", "seed"], source)
|
||||
bundle = bundle_root / "wooo__awoooi.bundle"
|
||||
run(["git", "bundle", "create", str(bundle), "--all"], source)
|
||||
digest = hashlib.sha256(bundle.read_bytes()).hexdigest()
|
||||
(bundle_root / "wooo__awoooi.bundle.sha256").write_text(
|
||||
f"{digest} wooo__awoooi.bundle\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bash",
|
||||
str(SCRIPT),
|
||||
"--root",
|
||||
str(bundle_root),
|
||||
"--repo",
|
||||
"wooo/awoooi",
|
||||
"--write-textfile",
|
||||
"--textfile",
|
||||
str(textfile),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
assert "GITEA_BUNDLE_SAMPLE_RESTORE_DRY_RUN_OK=1" in result.stdout
|
||||
rendered = textfile.read_text(encoding="utf-8")
|
||||
assert 'awoooi_gitea_bundle_sample_restore_dry_run_ok{host="188",repo="wooo/awoooi"' in rendered
|
||||
assert 'bundle="wooo__awoooi.bundle",error=""} 1' in rendered
|
||||
assert "awoooi_gitea_bundle_sample_restore_dry_run_ref_count" in rendered
|
||||
@@ -218,10 +218,13 @@ def test_backup_alerts_cover_188_gitea_mirror_subtree() -> None:
|
||||
assert 'awoooi_backup_coverage_domain_expected_info{host="188"}' in alerts
|
||||
assert "BackupCoverageDomainMetricMissing188" in alerts
|
||||
assert "awoooi_backup_coverage_domain_fresh == 0" in alerts
|
||||
assert "GiteaPrivateBundleRestoreDryRunMissing" in alerts
|
||||
assert "awoooi_gitea_bundle_sample_restore_dry_run_ok" in alerts
|
||||
|
||||
|
||||
def test_gitea_repo_bundle_backup_is_non_interactive_and_manifested() -> None:
|
||||
script = read("scripts/backup/gitea-repo-bundle-backup.sh")
|
||||
restore_script = read("scripts/backup/gitea-bundle-sample-restore-dry-run.sh")
|
||||
|
||||
assert "GIT_TERMINAL_PROMPT=0" in script
|
||||
assert "bundle create" in script
|
||||
@@ -231,3 +234,7 @@ def test_gitea_repo_bundle_backup_is_non_interactive_and_manifested() -> None:
|
||||
assert "gitea dump" in script
|
||||
assert "does not replace `gitea dump`" in script
|
||||
assert "tokens or passwords" in script
|
||||
assert "git clone --mirror --quiet" in restore_script
|
||||
assert "fsck --connectivity-only" in restore_script
|
||||
assert "awoooi_gitea_bundle_sample_restore_dry_run_ok" in restore_script
|
||||
assert "never reads tokens" in restore_script
|
||||
|
||||
Reference in New Issue
Block a user