Skip to content

feat(postgrest)!: strip chart down to a stateless service - #161

Open
emmanuel wants to merge 1 commit into
mainfrom
refactor/postgrest-stateless
Open

feat(postgrest)!: strip chart down to a stateless service#161
emmanuel wants to merge 1 commit into
mainfrom
refactor/postgrest-stateless

Conversation

@emmanuel

@emmanuel emmanuel commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The chart provisioned a CloudNativePG cluster, ran goose migrations, and shipped a keyserver that minted its own JWTs. None of that belongs in a chart described as "a PostgREST data api". This cuts it back to that, plus the Service and Ingress around it.

A default install is now a Deployment with one init container and one container, a Service, two ConfigMaps and a Secret.

Removed

  • the cluster sub-chart dependency, Chart.lock, and the cluster: values block: roles goose/anon/peek/view/edit, databases, storage
  • templates/database.yaml, the CNPG Database CR
  • goose: templates/migrations.yaml, the goose ConfigMap, the migrations Secret, database.migrations.*, the database.migrations helper, and the migration init container
  • the wait init container, which installed postgresql-client at runtime and polled with the migrations secret
  • keyserver: init container, ConfigMap, Secret, service port 8000, keyserver: values, and the auth.app.localhost ingress rule
  • images/goose/, images/keyserver/, and .github/workflows/docker.yaml, which had nothing left to build
  • postgrest.jwt.claims, unreferenced and already broken: it treated application.jwt.claim as a scalar when it is a map

Ingress is unchanged apart from dropping the keyserver rule.

JWKS

files/fetch-jwks.sh covers the one thing the keyserver did that PostgREST needs: getting a key set onto disk for PGRST_JWT_SECRET. It runs as an init container on reg.mini.dev/curl-fips (Minimus hardened FIPS curl, digest pinned, no runtime package installs), fetches once and exits, so PostgREST does not start without a usable key set and a bad URL blocks the rollout instead of producing a pod that never goes ready.

Responses are validated before being renamed into place, so a failed or malformed fetch leaves working keys alone. The check runs per key, which matters because a document can carry the required kty, use and kid spread across different keys and still satisfy a substring check without holding a usable key. jwks.require.kty/use/kid tighten it; they default to empty, since use and kid are optional in RFC 7517.

jwks.refresh optionally keeps the same script resident to pick up rotations, sending SIGUSR2 so PostgREST re-reads the file. Off by default: it only helps where signing keys actually rotate, and a resident container that crashes drops the pod from its Service endpoints, which a one-shot init container cannot do.

Probes

The chart had none, so the Service routed traffic regardless of whether PostgREST could reach the database, and a rollout reported success as soon as the container was running. startupProbe and livenessProbe now target PostgREST's admin server (/ready and /live).

There is no readiness probe. It would drop the pod from its Service during a database outage, and callers would get a connection refusal rather than PostgREST's own 503, which names the failure. The startup probe covers the case that matters, since it gates the rollout and is not re-evaluated afterwards. Worth revisiting above one replica, where readiness would let you shed a single bad pod.

checksum/config and checksum/secret were added alongside: without them, changing database.connection.* or any PGRST_* setting rewrote the ConfigMap or Secret but left the pod template untouched, so nothing rolled and the change did not take effect.

Breaking

  • The chart no longer provisions a database. database.connection.* must point at one that exists, with the anon role and the roles the claim mapping resolves to already created.
  • keyserver.* is replaced by jwks.*. Service port 8000 and the auth.app.localhost ingress rule are gone.
  • Short-lived peek tokens can no longer be minted. The keyserver generated its own key pair, merged it into the served document and issued them from /peek. A fetch-only script has no equivalent, and /jwks goes with it. The peek role itself is untouched.
  • database.migrations.* and goose are gone. Run migrations out of band.
  • The PostgREST image is digest pinned with appVersion set, rather than floating on latest.

Upgrading an existing release will drop the CNPG Cluster from the release manifest, which Helm will delete. That path has not been worked out here and needs handling before anyone upgrades in place.

Testing

On kind (v1.35.0) against a real Postgres: the gating fetch writes and exits cleanly; a changed kid produces one write and one SIGUSR2; a 200 carrying a non-JWKS is rejected and the previous key set survives; an unreachable JWKS leaves the new pod in Init:Error with the old one still serving; killing the database leaves the pod Ready and answering 503 with PGRST000; and a rollout against an unreachable database leaves the new pod at 0/1 while the old one keeps serving.

Against the real image, the failure matrix (HTTP 500, a 200 carrying HTML, an empty body, a truncated body, 404, unreachable host) leaves existing keys intact in every case.

uki-code/od360#4363 runs the same script for docker-compose; this copy is the authoritative one.

@emmanuel
emmanuel force-pushed the refactor/postgrest-stateless branch from 3ed2512 to 8210a96 Compare August 6, 2026 19:05
@emmanuel emmanuel changed the title feat(postgrest)!: strip chart down to a stateless service feat(postgrest)!: strip to a stateless service and fetch the JWKS with a validating script Aug 7, 2026
@emmanuel emmanuel changed the title feat(postgrest)!: strip to a stateless service and fetch the JWKS with a validating script feat(postgrest)!: strip chart down to a stateless service Aug 8, 2026
Drop the CloudNativePG cluster dependency, the Database CR and goose. The chart no
longer provisions a database; database.connection.* has to point at one that
already exists, and migrations are somebody else's job. The keyserver goes with
them, along with the images/ build contexts and the workflow that built them.

Its remaining job, putting a JWKS on disk for PGRST_JWT_SECRET, is now
files/fetch-jwks.sh on a digest-pinned Minimus curl image. It validates each
response per key and renames it into place, so a bad fetch cannot replace working
keys, and it runs as an init container so PostgREST does not start without a usable
key set. jwks.refresh optionally keeps it resident to pick up rotations, sending
SIGUSR2 so PostgREST re-reads the file. Off by default.

Add probes against PostgREST's admin server, which the chart never had, and
checksum annotations, without which a config or credential change never reached a
running pod.

Short-lived peek tokens can no longer be minted: the keyserver generated its own
key pair and served them, and a fetch-only script has no equivalent.

od360's compose stack runs the same script; keep the two in sync.
@emmanuel
emmanuel force-pushed the refactor/postgrest-stateless branch from 0ed4a35 to f84fa5f Compare August 8, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant