#!/usr/bin/env bash
# pg-dump-nightly.sh — nightly dump of the Woodpecker CI Postgres (ALL CI state:
# repos, pipelines, agents, the org secrets). Wave-2 P0.1 — backups as code.
#
#   docker exec pg_dump | gzip → integrity-check → prune (14 days) → heartbeat.
#
# INSTALL (woodpecker-prod, as root): copy to /usr/local/bin/pg-dump-nightly.sh
# (0750) + cron /etc/cron.d/mgf-backup-pg — see
# examples/woodpecker-pg-backup-cloud-init-fragment.yml. Heartbeat endpoint +
# token live in /etc/mgf/backup-heartbeat.{url,token} (operator-pushed; NEVER
# committed). Restore: docs/backup-restore-runbook.md §3. NOTE: /opt/woodpecker/
# .env (the OAuth/agent/postgres secrets) is deliberately NOT in this dump's
# scope as a committed artifact — after a restore, re-create .env and re-enter
# any rotated org secrets per the CI runbook §4.
#
# Dead-man's-switch: heartbeat only on success; the watcher pages on a miss.
set -euo pipefail
umask 077   # the dump holds ALL CI state + the org secrets — never world-readable

BACKUP_DIR="${BACKUP_DIR:-/opt/backups}"
KEEP_DAYS="${KEEP_DAYS:-14}"
PG_CONTAINER="${PG_CONTAINER:-woodpecker-woodpecker-postgres-1}"
PG_USER="${PG_USER:-woodpecker}"
PG_DB="${PG_DB:-woodpecker}"
LOG="${LOG:-/opt/backups/backup.log}"   # local trail, uniform with config-snapshot-nightly.sh
HEARTBEAT_URL_FILE="${HEARTBEAT_URL_FILE:-/etc/mgf/backup-heartbeat.url}"
HEARTBEAT_TOKEN_FILE="${HEARTBEAT_TOKEN_FILE:-/etc/mgf/backup-heartbeat.token}"

post_heartbeat() {
  # Best-effort emit: a failed POST must not fail a succeeded backup — the
  # watcher pages on the MISSED heartbeat either way.
  job="$1"; artifact="$2"; bytes="$3"
  if [ ! -r "$HEARTBEAT_URL_FILE" ] || [ ! -r "$HEARTBEAT_TOKEN_FILE" ]; then
    echo "WARNING: heartbeat url/token file missing — no heartbeat sent (the watcher WILL page)" >&2
    return 0
  fi
  payload=$(printf '[{"job":"%s","status":"ok","host":"%s","artifact":"%s","bytes":%s,"ts":"%s"}]' \
    "$job" "$(hostname)" "$artifact" "${bytes:-0}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)")
  curl -fsS -m 20 -X POST -H 'Content-Type: application/json' \
    -H "Authorization: $(cat "$HEARTBEAT_TOKEN_FILE")" \
    -d "$payload" "$(cat "$HEARTBEAT_URL_FILE")" >/dev/null \
    || echo "WARNING: heartbeat POST failed (the watcher WILL page)" >&2
}

ts="$(date -u +%Y%m%d-%H%M%S)"
out="$BACKUP_DIR/woodpecker-pg-$ts.sql.gz"
mkdir -p "$BACKUP_DIR"

# pipefail (set above) makes a failed pg_dump fail the pipeline, not just gzip.
# docker exec targets the compose-deterministic container name — no cwd/compose
# context needed (cron-safe), unlike `docker compose exec`.
docker exec "$PG_CONTAINER" pg_dump -U "$PG_USER" "$PG_DB" | gzip > "$out"
# an empty dump is a failed dump — never heartbeat on one
bytes=$(stat -c %s "$out")
[ "$bytes" -gt 1024 ] || { echo "ERROR: dump suspiciously small ($bytes bytes)" >&2; exit 1; }
gunzip -t "$out"   # integrity: a corrupt gzip is a failed backup, not a heartbeat
echo "woodpecker pg dump OK: $out ($bytes bytes)"

find "$BACKUP_DIR" -name 'woodpecker-pg-*.sql.gz' -mtime "+$KEEP_DAYS" -delete

echo "[$ts] HEARTBEAT pg-backup OK $(du -h "$out" | cut -f1)" >> "$LOG"
post_heartbeat "pg-dump" "$(basename "$out")" "$bytes"
