feat(pocket-id): move the identity provider's state into Postgres (#35) #72
No reviewers
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner!72
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/35-pocket-id-postgres"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Pocket ID kept its whole world in
/app/data/pocket-id.db: one user, twopasskeys and three OIDC client registrations, on a 5 GiB
persistentclaimbeside a 4 MiB WAL. All of it becomes the
pocket_iddatabase in thepostgrescluster — with the same shape Woodpecker's cut-over established,and two differences that are this app's rather than a variant of the same
edit.
Nothing is ported, and that is a decision rather than a shortcut. Pocket ID
has no export/import and its migrations are per-provider, so the alternative
is hand-written SQL across 32 tables it owns, to preserve about twenty
minutes of §5c-§5e browser work. It is also the one cut-over that cannot lock
anyone out: §5c's
/setupreopens whenever the database holds no users, andno path to the cluster runs through the provider (kubectl uses the Talos
kubeconfig; Vault still answers to its root token).
data/postgres/{roles,databases}.yaml: aDatabaseRoleand aDatabasenamed
pocket-id— Kubernetes naming, because the app is spelled with ahyphen — whose
spec.nameispocket_id, which is what PostgreSQL parses.Owner,
login, and both reclaim policiesretain, exactly as Woodpecker'spair.
secrets/pocket-id-database.yaml: one Vault path(
secret/pocket-id/database), two Secrets, because CNPG'spasswordSecretis a LocalObjectReference and Pocket ID reads a DSN in itsown namespace. The
postgrescopy is basic-auth and carriesk8s.enterprisedb.io/reload(the rotation mechanism); thepocket-idcopyis Opaque and
DB_CONNECTION_STRINGis templated from the same two fields,so they cannot drift. Hex password,
sslmode=require— pgx'srequire,whose own parser was read rather than borrowing lib/pq's meaning from
Woodpecker's DSN.
apps/pocket-id/helmrelease.yaml: the DSN arrives as oneenventry addedby a postRenderer. The chart leaves no other route —
config.*rendersinto a ConfigMap (a password in a ConfigMap is what the Vault-first rule
exists to prevent) and its single
existingSecretslot is already spent onENCRYPTION_KEY, with noextraEnv. Worth the awkwardness because of howPocket ID reads it: the provider is inferred from the DSN's scheme, so no
DSN at all means SQLite at
data/pocket-id.db— a silent fallback, not astartup error, onto the very file this change retires.
Uploads stay on the claim (avatars are images, not rows).
The cut-over is a hand-cut recorded in docs/pocket-id.md §5g, including what
lives outside the database: the three clients' secrets (Vault's
.envcopy,the console's
oz-secrets, Headlamp's Terraform variable) and the two groupnames that must come back spelled the same. Its hard step is the last one —
renaming the retired file rather than deleting it, because a copy left where
the app looks is how a future DSN regression comes back up on months-old
identities instead of an empty database that asks for a passkey.
Drills follow the data again:
drills/check.shnow counts rows per databaseand keys them by it (
rows_<db>_<table>), so a second app's rows cannotsilently shadow the first's, and a database that does not exist yet reads
?— a difference rather than silence (verified live: 45 predicates, Woodpecker's
counts and
rows_pocket_id_*as?).Two stale C1 leftovers are corrected here rather than left to rot:
docs/logging.md still said Woodpecker's build logs stream into SQLite on a
persistent PVC, and ARCHITECTURE.md's durable budget said 51 GiB of 2 TiB —
it is 270 GiB, the Nix store's 200 GiB having never been counted. Also fixed
in this change, both found by running what the docs claim: §5f's
v auth listis denied to the scoped token the section tells you to load (auth methods are
admin-level; it now checks the key path that token can read), and §5's
internal-URL story was wrong —
INTERNAL_APP_URLis derived fromAPP_URL,which is why every published endpoint is the public one.
Guard: tests/test_chart_sources.py renders the chart and applies this
postRenderer with kustomize, asserting the variable lands on the container as
a
secretKeyRef— a patch that quietly stopped applying would leave the apprunning on the retired file. 220 tests pass, also under a CI environment.
Pocket ID kept its whole world in `/app/data/pocket-id.db`: one user, two passkeys and three OIDC client registrations, on a 5 GiB `persistent` claim beside a 4 MiB WAL. All of it becomes the `pocket_id` database in the `postgres` cluster — with the same shape Woodpecker's cut-over established, and two differences that are this app's rather than a variant of the same edit. Nothing is ported, and that is a decision rather than a shortcut. Pocket ID has no export/import and its migrations are per-provider, so the alternative is hand-written SQL across 32 tables it owns, to preserve about twenty minutes of §5c-§5e browser work. It is also the one cut-over that cannot lock anyone out: §5c's `/setup` reopens whenever the database holds no users, and no path to the cluster runs through the provider (kubectl uses the Talos kubeconfig; Vault still answers to its root token). - `data/postgres/{roles,databases}.yaml`: a `DatabaseRole` and a `Database` named `pocket-id` — Kubernetes naming, because the app is spelled with a hyphen — whose `spec.name` is `pocket_id`, which is what PostgreSQL parses. Owner, `login`, and both reclaim policies `retain`, exactly as Woodpecker's pair. - `secrets/pocket-id-database.yaml`: one Vault path (`secret/pocket-id/database`), two Secrets, because CNPG's `passwordSecret` is a LocalObjectReference and Pocket ID reads a DSN in its own namespace. The `postgres` copy is basic-auth and carries `k8s.enterprisedb.io/reload` (the rotation mechanism); the `pocket-id` copy is Opaque and `DB_CONNECTION_STRING` is templated from the same two fields, so they cannot drift. Hex password, `sslmode=require` — pgx's `require`, whose own parser was read rather than borrowing lib/pq's meaning from Woodpecker's DSN. - `apps/pocket-id/helmrelease.yaml`: the DSN arrives as one `env` entry added by a postRenderer. The chart leaves no other route — `config.*` renders into a ConfigMap (a password in a ConfigMap is what the Vault-first rule exists to prevent) and its single `existingSecret` slot is already spent on `ENCRYPTION_KEY`, with no `extraEnv`. Worth the awkwardness because of how Pocket ID reads it: the provider is inferred from the DSN's *scheme*, so no DSN at all means SQLite at `data/pocket-id.db` — a silent fallback, not a startup error, onto the very file this change retires. - The encryption key does not move: same Vault path, same Secret, same job. Uploads stay on the claim (avatars are images, not rows). The cut-over is a hand-cut recorded in docs/pocket-id.md §5g, including what lives *outside* the database: the three clients' secrets (Vault's `.env` copy, the console's `oz-secrets`, Headlamp's Terraform variable) and the two group names that must come back spelled the same. Its hard step is the last one — renaming the retired file rather than deleting it, because a copy left where the app looks is how a future DSN regression comes back up on months-old identities instead of an empty database that asks for a passkey. Drills follow the data again: `drills/check.sh` now counts rows per database and keys them by it (`rows_<db>_<table>`), so a second app's rows cannot silently shadow the first's, and a database that does not exist yet reads `?` — a difference rather than silence (verified live: 45 predicates, Woodpecker's counts and `rows_pocket_id_*` as `?`). Two stale C1 leftovers are corrected here rather than left to rot: docs/logging.md still said Woodpecker's build logs stream into SQLite on a persistent PVC, and ARCHITECTURE.md's durable budget said 51 GiB of 2 TiB — it is 270 GiB, the Nix store's 200 GiB having never been counted. Also fixed in this change, both found by running what the docs claim: §5f's `v auth list` is denied to the scoped token the section tells you to load (auth methods are admin-level; it now checks the key path that token *can* read), and §5's internal-URL story was wrong — `INTERNAL_APP_URL` is derived from `APP_URL`, which is why every published endpoint is the public one. Guard: tests/test_chart_sources.py renders the chart and applies this postRenderer with kustomize, asserting the variable lands on the container as a `secretKeyRef` — a patch that quietly stopped applying would leave the app running on the retired file. 220 tests pass, also under a CI environment.