feat(gitea): add capability and backup receipt readbacks

This commit is contained in:
ogt
2026-07-09 23:21:54 +08:00
parent 7ee0a17ab7
commit 391a1577d0
15 changed files with 3587 additions and 114 deletions

View File

@@ -14,6 +14,73 @@ SERVICE="gitea"
GITEA_CONTAINER="gitea"
LOCAL_REPO="${BACKUP_BASE}/gitea"
DUMP_DIR="/tmp/gitea-backup-$$"
TEXTFILE_DIR="${NODE_EXPORTER_TEXTFILE_DIR:-/home/wooo/node_exporter_textfiles}"
TEXTFILE_PATH="${GITEA_FULL_BACKUP_RECEIPT_TEXTFILE:-${TEXTFILE_DIR}/gitea_full_backup_receipt.prom}"
HOST_LABEL="${AIOPS_HOST_LABEL:-110}"
GITEA_FULL_BACKUP_COMPONENTS="database repositories app_settings lfs_objects package_registry attachments hooks_custom_assets"
label_escape() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
write_gitea_full_backup_receipt() {
local status="$1"
local timestamp="$2"
local duration="$3"
local snapshot_id="${4:-unknown}"
local tmp
local host
local service_label
local status_label
local component
local ok=0
local failed=1
if [ "${status}" = "success" ]; then
ok=1
failed=0
fi
host="$(label_escape "${HOST_LABEL}")"
service_label="$(label_escape "${SERVICE}")"
status_label="$(label_escape "${status}")"
install -d -m 755 "${TEXTFILE_DIR}" || return 0
tmp="$(mktemp "${TEXTFILE_PATH}.tmp.XXXXXX")" || return 0
{
echo "# HELP awoooi_gitea_full_backup_last_success_timestamp Unix timestamp of the latest successful Gitea full-server backup receipt."
echo "# TYPE awoooi_gitea_full_backup_last_success_timestamp gauge"
echo "# HELP awoooi_gitea_full_backup_last_run_failed Whether the latest Gitea full-server backup run failed."
echo "# TYPE awoooi_gitea_full_backup_last_run_failed gauge"
echo "# HELP awoooi_gitea_full_backup_duration_seconds Duration of the latest Gitea full-server backup run."
echo "# TYPE awoooi_gitea_full_backup_duration_seconds gauge"
echo "# HELP awoooi_gitea_full_backup_dump_file_present Whether the Gitea dump file was produced before restic backup."
echo "# TYPE awoooi_gitea_full_backup_dump_file_present gauge"
echo "# HELP awoooi_gitea_full_backup_restic_snapshot_present Whether a restic snapshot id was recorded for the Gitea dump."
echo "# TYPE awoooi_gitea_full_backup_restic_snapshot_present gauge"
echo "# HELP awoooi_gitea_full_backup_component_receipt Whether the Gitea full-server dump includes a redacted component receipt."
echo "# TYPE awoooi_gitea_full_backup_component_receipt gauge"
echo "# HELP awoooi_gitea_full_backup_restore_drill_performed Whether this receipt performed a production restore drill."
echo "# TYPE awoooi_gitea_full_backup_restore_drill_performed gauge"
echo "# HELP awoooi_gitea_full_backup_secret_value_collected Whether this receipt collected secret values."
echo "# TYPE awoooi_gitea_full_backup_secret_value_collected gauge"
echo "# HELP awoooi_gitea_full_backup_production_restore_performed Whether this receipt restored anything into production."
echo "# TYPE awoooi_gitea_full_backup_production_restore_performed gauge"
echo "awoooi_gitea_full_backup_last_success_timestamp{host=\"${host}\",service=\"${service_label}\"} $([ "${ok}" = "1" ] && echo "${timestamp}" || echo 0)"
echo "awoooi_gitea_full_backup_last_run_failed{host=\"${host}\",service=\"${service_label}\",status=\"${status_label}\"} ${failed}"
echo "awoooi_gitea_full_backup_duration_seconds{host=\"${host}\",service=\"${service_label}\"} ${duration}"
echo "awoooi_gitea_full_backup_dump_file_present{host=\"${host}\",service=\"${service_label}\"} ${ok}"
echo "awoooi_gitea_full_backup_restic_snapshot_present{host=\"${host}\",service=\"${service_label}\",snapshot_id=\"$(label_escape "${snapshot_id}")\"} ${ok}"
for component in ${GITEA_FULL_BACKUP_COMPONENTS}; do
echo "awoooi_gitea_full_backup_component_receipt{host=\"${host}\",service=\"${service_label}\",component=\"$(label_escape "${component}")\",receipt=\"gitea_dump_restic\"} ${ok}"
done
echo "awoooi_gitea_full_backup_restore_drill_performed{host=\"${host}\",service=\"${service_label}\"} 0"
echo "awoooi_gitea_full_backup_secret_value_collected{host=\"${host}\",service=\"${service_label}\"} 0"
echo "awoooi_gitea_full_backup_production_restore_performed{host=\"${host}\",service=\"${service_label}\"} 0"
} >"${tmp}"
mv "${tmp}" "${TEXTFILE_PATH}" || return 0
chmod 0644 "${TEXTFILE_PATH}" || true
}
cleanup() {
rm -rf "${DUMP_DIR}"
@@ -37,6 +104,8 @@ main() {
docker exec -u git "${GITEA_CONTAINER}" rm -f /tmp/gitea-dump.zip
log_success "Gitea dump 完成"
else
duration=$(($(date +%s) - start_time))
write_gitea_full_backup_receipt "dump_failed" "0" "${duration}" "none" || true
log_error "Gitea dump 失敗"
notify_clawbot "failed" "${SERVICE}" "Gitea dump 失敗"
exit 1
@@ -44,13 +113,25 @@ main() {
if [ ! -d "${LOCAL_REPO}/data" ]; then
log_info "初始化本地 Restic 倉庫..."
restic -r "${LOCAL_REPO}" init --password-file "${RESTIC_PASSWORD_FILE}"
if ! restic -r "${LOCAL_REPO}" init --password-file "${RESTIC_PASSWORD_FILE}"; then
duration=$(($(date +%s) - start_time))
write_gitea_full_backup_receipt "restic_init_failed" "0" "${duration}" "none" || true
log_error "Restic 初始化失敗"
notify_clawbot "failed" "${SERVICE}" "Gitea Restic 初始化失敗"
exit 1
fi
fi
tags=$(build_tags "${SERVICE}")
restic -r "${LOCAL_REPO}" backup "${DUMP_DIR}" \
if ! restic -r "${LOCAL_REPO}" backup "${DUMP_DIR}" \
--password-file "${RESTIC_PASSWORD_FILE}" \
${tags}
${tags}; then
duration=$(($(date +%s) - start_time))
write_gitea_full_backup_receipt "restic_backup_failed" "0" "${duration}" "none" || true
log_error "Restic 備份失敗"
notify_clawbot "failed" "${SERVICE}" "Gitea Restic 備份失敗"
exit 1
fi
snapshot_id=$(restic -r "${LOCAL_REPO}" snapshots --latest 1 --json --password-file "${RESTIC_PASSWORD_FILE}" 2>/dev/null | grep -oP '"short_id":"\K[^"]+' | head -1 || true)
log_success "Restic 備份完成: ${snapshot_id:-unknown}"
@@ -61,6 +142,7 @@ main() {
log_info "Offsite copy is handled by sync-offsite-backups.sh; no direct rclone sync here."
duration=$(($(date +%s) - start_time))
write_gitea_full_backup_receipt "success" "$(date +%s)" "${duration}" "${snapshot_id:-unknown}" || true
log_success "========== Gitea 備份完成 (${duration}s) =========="
notify_clawbot "success" "${SERVICE}" "Gitea 備份完成" "${duration}"
}

View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
BACKUP_GITEA = ROOT / "scripts" / "backup" / "backup-gitea.sh"
def test_backup_gitea_writes_full_server_component_receipts() -> None:
text = BACKUP_GITEA.read_text(encoding="utf-8")
assert "write_gitea_full_backup_receipt" in text
assert "awoooi_gitea_full_backup_component_receipt" in text
assert "awoooi_gitea_full_backup_secret_value_collected" in text
assert "awoooi_gitea_full_backup_production_restore_performed" in text
assert "gitea dump -c /data/gitea/conf/app.ini" in text
assert "GITEA_FULL_BACKUP_COMPONENTS" in text
for component in [
"database",
"repositories",
"app_settings",
"lfs_objects",
"package_registry",
"attachments",
"hooks_custom_assets",
]:
assert component in text
def test_backup_gitea_receipt_contract_stays_no_secret_no_restore() -> None:
text = BACKUP_GITEA.read_text(encoding="utf-8")
assert "awoooi_gitea_full_backup_secret_value_collected" in text
assert "awoooi_gitea_full_backup_production_restore_performed" in text
assert (
'secret_value_collected{host=\\"${host}\\",service=\\"${service_label}\\"} 0'
in text
)
assert (
'production_restore_performed{host=\\"${host}\\",service=\\"${service_label}\\"} 0'
in text
)