Skip to content

1.6 Step | SetupPostgres

Omisen edited this page Aug 16, 2026 · 4 revisions

Installs PostgreSQL (when absent), enables it and starts it, reversibly. It lives in src/steps/setup_postgres.rs. A port of setup_postgres from lib/postgres.sh.

This is the case where a single PreState is not enough: PostgreSQL has four orthogonal state axes, each to be restored separately (decision D4).


Four independent PreStates

One PreState would describe “PostgreSQL is there or not”; but the service can be installed-but-stopped, running-but-not-enabled, and so on. Four axes are needed:

struct PostgresSnapshot {
    installed:           PreState,  // was the package already there?
    enabled:             PreState,  // was the service already enabled?
    active:              PreState,  // was the service already running?
    cluster_initialized: PreState,  // was the data directory already initialised?
}

The fourth axis exists for the heaviest divergence between the families: on Debian/Ubuntu the package's postinst creates and starts a cluster, so installing is enough; on Fedora postgresql-server initialises nothing, and without postgresql-setup --initdb the service does not start. Initialising is a mutation that produces an artifact — the data directory — so it gets a PreState of its own, or it would be something coming into existence with nobody recording it. On families that do not need it, it stays Untracked: nothing to create, nothing to remove.

Phase Behaviour
snapshot for each axis: already true before us → Preexisting; otherwise Untracked
run installs/enables/starts only the Untracked axes, each → CreatedByUs. Final check: it must end up active, otherwise an error
undo restores each axis to its snapshot state, in the order stop → disable → (purge)

Undo: per-axis restoration (D4)

The undo touches an axis only if we changed it (CreatedByUs). A service that was already running/enabled before us is left as it was.

Axis If CreatedByUs If Preexisting
active systemctl stop leave it running
enabled systemctl disable leave it enabled
installed see below (gated purge) leave it installed

The purge is dangerous: gated behind a flag (D3, point 2)

A firm decision: by default the undo does stop + disable (both reversible) but does not purge the package, even when installed == CreatedByUs.

Purging PostgreSQL is too destructive for an automatic rollback on a customer machine: it could remove clusters holding data. Stop and disable can be undone; a purge cannot.

The purge happens only with --aggressive-rollback, and with an explicit warning.

Cluster caution (implemented). Even with --aggressive-rollback the purge is declined if the cluster hosts databases beyond ours and the postgres maintenance one — and if that list cannot be obtained, nothing is purged: fail-safe. The same question protects the removal of the data directory, for the same reason: a PGDATA contains all the cluster's databases, not only ours.


One single answer to “where is the cluster”

The data directory's path has two possible sources, and they must not diverge:

  • a constant in the code (/var/lib/pgsql/data on Fedora), which is what drives the snapshot and the undo — and it must stay a constant, because it feeds a remove_dir_all: a path arriving from a file is not trusted data;
  • the systemd unit, which is what postgresql-setup consults to know where to initialise, and which an administrator can move with a drop-in.

If they diverge, the damage is not theoretical: initdb would create the cluster where the unit says, while the undo with --aggressive-rollback would remove the constant — that is, a directory we did not create, and which on precisely such a machine is where a previous customer cluster may live.

So the step refuses when the two answers do not match, naming both, and stops before touching anything. The path read from the unit drives no removal: it exists only to say “I do not know how to work here”. If the unit says nothing — package not yet installed, systemctl not queryable — nothing is concluded and we carry on: blindness is not divergence.

Exercised for real since 3.2.0, where before it lived only on mocks: a CI job installs PostgreSQL, moves PGDATA with a drop-in, and demands the refusal — asserting the message, not merely a non-zero exit, because a run can fail for a dozen reasons and look identical.

Running it on a real machine also settled the branch nobody had checked: with the package not installed, systemctl show -p Environment postgresql.service exits 0 with an empty value — it does not fail and does not say “no such unit”. That is what makes “blindness is not divergence” the common case rather than a rare one, and had systemd answered differently, every virgin machine would have been refused.


The testable boundary

Services (service_is_enabled/is_active/enable/disable/start/stop) and the package manager sit behind the SystemOps boundaries. In tests a stateful mock (start and stop update an internal flag) makes it possible to verify that the post-start check works and that the undo restores the axes, without a real PostgreSQL and without root.


Design notes

  • The final check (service_is_active) turns a failed start into a typed error rather than a silent carry-on.
  • Best-effort undo: every failed action logs a warn and carries on.
  • Tests: installed-but-stopped (start → stop, no disable, no purge), all-absent (install+enable+start → stop+disable, no purge), --aggressive-rollback (purges) vs default (does not), already-running (D4: does not stop).

Clone this wiki locally