railiance-platform/tools/cmd/forgejo-backup
codex d0c7249a0d
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Reject unfinished or truncated Forgejo backup archives before encryption
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
2026-09-05 22:10:22 +02:00

137 lines
No EOL
4.9 KiB
Bash
Executable file

#!/usr/bin/env bash
# tools/cmd/forgejo-backup — Forgejo Option A backup (RAIL-HO-WP-0005 T04/T09)
#
# Daily: forgejo dump (blob state) + pg_dump (forgejo-db) → age → Nextcloud forgejo/
# Retention: 14 daily + 4 weekly uploads (Sunday); local cache keep 7 per type.
#
# Env:
# KUBECONFIG (default ~/.kube/config-hosteurope)
# RAILIANCE_BACKUP_NC_TOKEN Nextcloud WebDAV token
# RAILIANCE_BACKUP_DRY_RUN=1 skip upload and remote side effects
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=lib/railiance-print.sh
source "${ROOT}/lib/railiance-print.sh"
# shellcheck source=lib/railiance-backup-common.sh
source "${ROOT}/lib/railiance-backup-common.sh"
KUBECONFIG="${KUBECONFIG:-${HOME}/.kube/config-hosteurope}"
export KUBECONFIG
DRY_RUN="${RAILIANCE_BACKUP_DRY_RUN:-0}"
FORGEJO_NAMESPACE="${FORGEJO_NAMESPACE:-forgejo}"
FORGEJO_RELEASE="${FORGEJO_RELEASE:-forgejo}"
FORGEJO_DB_NAMESPACE="${FORGEJO_DB_NAMESPACE:-databases}"
FORGEJO_DB_CLUSTER="${FORGEJO_DB_CLUSTER:-forgejo-db}"
kubectl_get_pod() {
local ns="$1" selector="$2" attempt out
for attempt in 1 2 3 4 5 6 7 8 9 10; do
out="$(kubectl get pods -n "${ns}" -l "${selector}" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
if [[ -n "${out}" ]]; then
echo "${out}"
return 0
fi
sleep $((attempt * 3))
done
return 1
}
BACKUP_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/railiance/backups/forgejo"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
DOW="$(date -u +%u)" # 7 = Sunday
DUMP_PLAIN="${BACKUP_DIR}/forgejo-dump-${TS}.zip"
DUMP_AGE="${DUMP_PLAIN}.age"
DB_PLAIN="${BACKUP_DIR}/forgejo-db-${TS}.sql"
DB_AGE="${DB_PLAIN}.age"
cleanup_plain() {
rm -f "${DUMP_PLAIN}" "${DB_PLAIN}"
}
trap cleanup_plain EXIT
if [[ "${DRY_RUN}" != "1" ]]; then
railiance_backup_require_openbao_lane
fi
railiance_backup_require_tools
FORGEJO_DB_POD="${FORGEJO_DB_POD:-$(kubectl_get_pod "${FORGEJO_DB_NAMESPACE}" \
"cnpg.io/cluster=${FORGEJO_DB_CLUSTER}" || true)}"
if [[ -z "${FORGEJO_DB_POD}" ]]; then
bad "preflight" "forgejo-db pod not found (cluster ${FORGEJO_DB_CLUSTER})"
exit 1
fi
if ! kubectl wait --for=condition=Ready "pod/${FORGEJO_DB_POD}" \
-n "${FORGEJO_DB_NAMESPACE}" --timeout=180s >/dev/null 2>&1; then
bad "preflight" "forgejo-db pod not Ready (check: make forgejo-db-status)"
exit 1
fi
PROD_POD="$(kubectl_get_pod "${FORGEJO_NAMESPACE}" \
"app.kubernetes.io/instance=${FORGEJO_RELEASE}" || true)"
if [[ -z "${PROD_POD}" ]]; then
bad "preflight" "Forgejo app pod not found in ${FORGEJO_NAMESPACE}"
exit 1
fi
mkdir -p "${BACKUP_DIR}"
print_hdr "Forgejo backup — ${TS}"
ok "kubeconfig" "${KUBECONFIG}"
ok "forgejo pod" "${FORGEJO_NAMESPACE}/${PROD_POD}"
ok "forgejo-db" "${FORGEJO_DB_NAMESPACE}/${FORGEJO_DB_POD}"
# 1. Application dump (repos, packages, attachments, LFS, avatars)
ok "forgejo dump" "running in pod…"
python3 "${ROOT}/scripts/capture_forgejo_archive.py" \
--namespace "${FORGEJO_NAMESPACE}" --pod "${PROD_POD}" --output "${DUMP_PLAIN}"
ok "forgejo dump" "$(du -h "${DUMP_PLAIN}" | awk '{print $1}') $(basename "${DUMP_PLAIN}")"
# 2. PostgreSQL logical dump (plain SQL to stdout; CNPG root FS is read-only)
ok "pg_dump" "database forgejo…"
kubectl exec -n "${FORGEJO_DB_NAMESPACE}" "${FORGEJO_DB_POD}" -c postgres -- \
pg_dump -U postgres -d forgejo --no-owner --no-acl > "${DB_PLAIN}"
ok "pg_dump" "$(du -h "${DB_PLAIN}" | awk '{print $1}') $(basename "${DB_PLAIN}")"
# 3. Encrypt
railiance_backup_encrypt "${DUMP_PLAIN}" "${DUMP_AGE}"
railiance_backup_encrypt "${DB_PLAIN}" "${DB_AGE}"
ok "age" "encrypted dump + db"
if [[ "${DRY_RUN}" == "1" ]]; then
warn "dry-run" "skipping Nextcloud upload"
else
railiance_backup_nc_upload "${DUMP_AGE}" "forgejo-dump-${TS}.zip.age"
ok "upload" "forgejo-dump-${TS}.zip.age"
railiance_backup_nc_upload "${DB_AGE}" "forgejo-db-${TS}.sql.age"
ok "upload" "forgejo-db-${TS}.sql.age"
if [[ "${DOW}" == "7" ]]; then
railiance_backup_nc_upload "${DUMP_AGE}" "forgejo-dump-weekly-${TS}.zip.age"
railiance_backup_nc_upload "${DB_AGE}" "forgejo-db-weekly-${TS}.sql.age"
ok "weekly" "Sunday copies uploaded"
fi
railiance_backup_record_success "${BACKUP_DIR}" "${TS}"
fi
# 4. Local cache prune (keep 7 per type)
railiance_backup_prune_local "${BACKUP_DIR}" 'forgejo-dump-*.zip.age' 7
railiance_backup_prune_local "${BACKUP_DIR}" 'forgejo-db-*.sql.age' 7
railiance_backup_prune_local "${BACKUP_DIR}" 'forgejo-dump-*.zip' 3
railiance_backup_prune_local "${BACKUP_DIR}" 'forgejo-db-*.sql' 3
ok "cache" "local copies pruned"
echo
ok "done" "Forgejo backup complete — ${TS}"
if [[ "${DRY_RUN}" != "1" ]]; then
echo " Dump: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-dump-${TS}.zip.age"
echo " DB: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-db-${TS}.sql.age"
echo
echo " Decrypt with ~/.config/age/railiance-backup.key"
echo " Restore drill: railiance-infra tools/forgejo-restore-drill.sh"
fi