Postgres backup, restore and verification. A backup nobody has restored is
a rumour; backup verify — the drill — is what turns it into a fact, on a
schedule.
cmd/pgctl/main.go— the command surface (backup list|run|verify,restore,version), flag parsing, per-operation timeouts, signal handling.internal/config/—config.Load, the target file, and its validation.internal/pg/— the whole implementation:runner.go—Runner, the one interface: build an argv, run it on the far end, read stdout.SSHRunner(default) andLocalRunner(--local, the recovery image).backup.go— list, take, prune, and the backup-selection guards.verify.go— the drill.restore.go— point-in-time restore.
Build: go build ./... · test: go test ./... · vet: go vet ./....
One dependency (goccy/go-yaml, for the config). Add another only for a real
reason.
- Over ssh (default). Suits a database that only listens on its own host:
pgctl ssh's in, then
docker execs into the postgres container — which already holds the object-storage credentials barman needs. pgctl handles no secrets. - Locally (
--local). Inside the recovery image, which carries barman and postgres alongside the binary. For the day the host you would have ssh'd into is the thing you are recovering from.
Runner.HasBarman() decides how a barman command is spelled. Getting it wrong
is silent, and it fails at the moment someone is trying to recover.
Read the comments before changing these — each one is a bug that shipped:
- SQL goes through stdin, never argv. ssh flattens argv into one string for the remote shell, and quoting does not survive the hop.
- An empty read is never a zero. Not knowing what a database holds must never be treated as knowing it is empty — that is what licenses a drop.
- Guards fail closed. Anything unverifiable is a failure, never an assumption.
- Prune only after the backup succeeds, or a run of failures eats the last good backup.
This repo uses OpenSpec. Baseline
specs of current behavior live in openspec/specs/<capability>/spec.md;
project context and spec rules are in openspec/config.yaml. Known
ambiguities/possible bugs are tracked as GitHub issues.
New features, fixes, and any behavior changes MUST follow the OPSX workflow (https://github.com/Fission-AI/OpenSpec/blob/main/docs/opsx.md):
/opsx:propose— create a change (proposal → spec deltas → design → tasks)/opsx:apply— implement the change from its artifacts/opsx:archive— merge the deltas intoopenspec/specs/when shipped
Don't edit openspec/specs/ directly for new behavior — that directory
reflects what is built and deployed; it changes via archived change deltas
(or /opsx:sync). Keep specs observable-behavior-only, with GIVEN/WHEN/THEN
scenarios per openspec/config.yaml rules.
Specs are strictly implementation-agnostic: they describe functional,
externally observable behavior and MUST NOT mention programming languages,
frameworks, libraries, file or directory names in the source tree, or
class/function names. The litmus test: the implementation could be rewritten in
a different language without editing any spec. Implementation notes belong in
change design.md/tasks.md artifacts and this file, never in specs.
The deliberate exception is documented in openspec/config.yaml: the external
programs pgctl drives and the on-disk artifacts it produces for them (barman's
commands, PG_VERSION, recovery.signal, postgresql.auto.conf) are
observable behavior — a rewrite must still produce exactly those, or the
restored database does not work.
Specs use example fixtures, never real infrastructure. No real hostnames,
containers, buckets, endpoints, roles, or schema/table names —
example-production, s3://example-backups, SELECT count(*) FROM public.users. This tool is published; strangers read its specs.
cli-surface— commands, flags, target selection, execution mode, version, timeouts, signal handling.backup-management— list, take, prune, and the selection/staleness guards.restore-drill— the drill, end to end, and every guard in it.point-in-time-restore— restore into a kept directory, optionally to a time.