S37: Postgres deploy scripts (WAL archive + daily backup + hook grants)
- SHA
5cdd4d4488b5713146f87e238961e0378673ac7f- Parents
-
4b67e74 - Tree
fdd9521
5cdd4d4
5cdd4d4488b5713146f87e238961e0378673ac7f4b67e74
fdd9521| Status | File | + | - |
|---|---|---|---|
| A |
deploy/postgres/archive_command.sh
|
24 | 0 |
| A |
deploy/postgres/backup-daily.sh
|
35 | 0 |
| A |
deploy/postgres/hook-role-grants.sql
|
46 | 0 |
deploy/postgres/archive_command.shadded@@ -0,0 +1,24 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# SPDX-License-Identifier: AGPL-3.0-or-later | |
| 3 | +# | |
| 4 | +# Postgres archive_command. Postgres calls this with two args: | |
| 5 | +# $1 = absolute path to the WAL segment in pgdata | |
| 6 | +# $2 = the segment filename | |
| 7 | +# | |
| 8 | +# Contract: exit 0 ONLY when the file is durably stored. Postgres | |
| 9 | +# refuses to recycle the segment until we report success — getting | |
| 10 | +# this wrong fills the disk. We use rclone copyto with --no-update- | |
| 11 | +# modtime so the bucket is the source of truth on retention. | |
| 12 | +# | |
| 13 | +# Wired by deploy/ansible/roles/postgres/templates/postgresql.conf.j2. | |
| 14 | + | |
| 15 | +set -euo pipefail | |
| 16 | + | |
| 17 | +SRC="$1" | |
| 18 | +NAME="$2" | |
| 19 | +BUCKET="${SHITHUB_WAL_BUCKET:-spaces-prod:shithub-wal}" | |
| 20 | + | |
| 21 | +# Atomic-ish: rclone copyto streams to a temp object, then renames. | |
| 22 | +rclone --config /root/.config/rclone/rclone.conf \ | |
| 23 | + --quiet \ | |
| 24 | + copyto "$SRC" "$BUCKET/$(date +%Y/%m/%d)/$NAME" | |
deploy/postgres/backup-daily.shadded@@ -0,0 +1,35 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# SPDX-License-Identifier: AGPL-3.0-or-later | |
| 3 | +# | |
| 4 | +# Daily logical backup. Run from cron (or a systemd timer) as the | |
| 5 | +# postgres user. We take a custom-format pg_dump of the shithub DB, | |
| 6 | +# stream it to Spaces, and keep one local copy in /var/backups for | |
| 7 | +# the operator to grab in a hurry. Lifecycle on the bucket prunes | |
| 8 | +# anything older than 30 days; PITR rolls forward from the WAL | |
| 9 | +# archive (see archive_command.sh). | |
| 10 | +# | |
| 11 | +# Exit non-zero on any failure so the systemd timer surfaces it | |
| 12 | +# (OnFailure= → alertmanager). | |
| 13 | + | |
| 14 | +set -euo pipefail | |
| 15 | + | |
| 16 | +DB="${SHITHUB_DB:-shithub}" | |
| 17 | +BUCKET="${SHITHUB_BACKUP_BUCKET:-spaces-prod:shithub-backups}" | |
| 18 | +LOCAL_DIR="${SHITHUB_BACKUP_LOCAL:-/var/backups/shithub}" | |
| 19 | +STAMP="$(date -u +%Y%m%dT%H%M%SZ)" | |
| 20 | +NAME="${DB}-${STAMP}.dump" | |
| 21 | + | |
| 22 | +mkdir -p "$LOCAL_DIR" | |
| 23 | + | |
| 24 | +pg_dump --format=custom --compress=9 --no-owner --no-privileges \ | |
| 25 | + --file="$LOCAL_DIR/$NAME" "$DB" | |
| 26 | + | |
| 27 | +# Verify the dump is structurally sound before we ship it. | |
| 28 | +pg_restore --list "$LOCAL_DIR/$NAME" >/dev/null | |
| 29 | + | |
| 30 | +rclone --config /root/.config/rclone/rclone.conf \ | |
| 31 | + copyto "$LOCAL_DIR/$NAME" "$BUCKET/daily/$(date -u +%Y/%m/%d)/$NAME" | |
| 32 | + | |
| 33 | +# Local retention: keep the last 7 dumps; bucket lifecycle handles | |
| 34 | +# the long tail. | |
| 35 | +ls -1t "$LOCAL_DIR"/*.dump 2>/dev/null | tail -n +8 | xargs -r rm -f | |
deploy/postgres/hook-role-grants.sqladded@@ -0,0 +1,46 @@ | ||
| 1 | +-- SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | +-- | |
| 3 | +-- Standalone hook-role grants. The Ansible postgres role applies | |
| 4 | +-- the same grants idempotently; this file exists so an operator | |
| 5 | +-- can re-apply (or audit) the exact write surface without running | |
| 6 | +-- the full playbook. | |
| 7 | +-- | |
| 8 | +-- Contract: shithub_hook is the role assumed by `shithubd hook ...` | |
| 9 | +-- subprocesses (post-receive, pre-receive). It MUST NOT have any | |
| 10 | +-- access beyond what's listed here. If a hook subcommand needs a | |
| 11 | +-- new table, add it here in the same PR — grep `shithub_hook` in | |
| 12 | +-- cmd/shithubd/hook.go to confirm. | |
| 13 | +-- | |
| 14 | +-- Apply as the shithub DB owner: | |
| 15 | +-- psql -U shithub -d shithub -f hook-role-grants.sql | |
| 16 | + | |
| 17 | +BEGIN; | |
| 18 | + | |
| 19 | +-- The role is created idempotently by the Ansible role; if you're | |
| 20 | +-- applying this by hand on a fresh DB, uncomment: | |
| 21 | +-- CREATE ROLE shithub_hook LOGIN PASSWORD :'hook_password'; | |
| 22 | + | |
| 23 | +-- Read surface: the hook needs to look up the pushing user, the | |
| 24 | +-- target repo, and the collaborator/permission rows to authorize | |
| 25 | +-- the push. | |
| 26 | +GRANT SELECT ON users TO shithub_hook; | |
| 27 | +GRANT SELECT ON repos TO shithub_hook; | |
| 28 | +GRANT SELECT ON repo_collaborators TO shithub_hook; | |
| 29 | +GRANT SELECT ON orgs TO shithub_hook; | |
| 30 | +GRANT SELECT ON org_members TO shithub_hook; | |
| 31 | + | |
| 32 | +-- Write surface: every row the hook subcommand inserts. Nothing | |
| 33 | +-- here gets UPDATE or DELETE — those happen out-of-band through | |
| 34 | +-- the web app or worker. | |
| 35 | +GRANT INSERT ON push_events TO shithub_hook; | |
| 36 | +GRANT INSERT ON jobs TO shithub_hook; | |
| 37 | +GRANT INSERT ON domain_events TO shithub_hook; | |
| 38 | +GRANT INSERT ON auth_audit_log TO shithub_hook; | |
| 39 | + | |
| 40 | +-- Sequences for the SERIAL/BIGSERIAL ids on the insert tables. | |
| 41 | +GRANT USAGE, SELECT ON SEQUENCE push_events_id_seq TO shithub_hook; | |
| 42 | +GRANT USAGE, SELECT ON SEQUENCE jobs_id_seq TO shithub_hook; | |
| 43 | +GRANT USAGE, SELECT ON SEQUENCE domain_events_id_seq TO shithub_hook; | |
| 44 | +GRANT USAGE, SELECT ON SEQUENCE auth_audit_log_id_seq TO shithub_hook; | |
| 45 | + | |
| 46 | +COMMIT; | |