Repository navigation
kind: run Postgres like the deployed database - #485
Conversation
fd230d3 to
20c0205
Compare
20c0205 to
6efcff2
Compare
Kind's Postgres now behaves like the database deployments run, so the Kind suites exercise the production connection and provisioning paths: - PostgreSQL 17 (`postgres:17`, which CI takes from the image mirror at a pinned digest). - TLS on the server with a self-signed certificate generated per pod start, and `sslmode=require` on every Kind connection: the services and the migrate Job through the `space-postgres` Secret's connection string, and the harness's psql helpers over loopback TLS in the pod. pg_hba.conf refuses unencrypted TCP, so no Kind client runs in plaintext and one that sets sslmode=disable fails. (A client that omits sslmode still negotiates TLS: libpq and sqlx default to prefer.) - The connection string names a master user that is not a superuser, as on a managed database: `space_admin` with CREATEDB CREATEROLE, `rds_superuser` WITH ADMIN OPTION and ownership of the database. An init script creates it, the `rds_superuser` role and that role's SET privilege on log_statement, which bootstrap-roles needs before it writes passwords. bootstrap-roles now takes its managed-service branch, and the migrator assumes space_migrator from a non-superuser session. The image's superuser is left to the image's init and the harness's direct-state psql. - POSTGRES_POOL_MAX_CONNECTIONS 100 for space-api and space-admin-api and 30 for internal-api, the default elsewhere. max_connections 1400 holds every replica's pools at their HPA maxima plus a rollout surge, so a service exhausts its own pool before the server refuses it; the memory limit rises from 512 MiB to 8 GiB to fit that many backends (measured at about 3.3 MiB each; 4 GiB was OOM-killed at about 1,280). log_lock_waits is on. SPACE_DB_APP_LOCK_TIMEOUT stays unset, which leaves the roles' lock_timeout off as deployed. - The migrate Job fails fast: backoffLimit 0, restartPolicy Never, and its activeDeadlineSeconds passed to the migrator as SPACE_DB_MIGRATE_JOB_DEADLINE_SECONDS. bootstrap.sh stops on a failed Job with its logs instead of waiting out its timeout, and rerunning it replaces the Job. The README's differences table and topology describe the new shape. A cluster created before this change must reset its database (README): PostgreSQL 17 cannot start on the old data directory, and the init script runs only on an empty one. A new check-postgres-data.sh reads the data directory's PG_VERSION on the node that holds the volume and stops bootstrap.sh, the smoke bring-up (SKIP_BOOTSTRAP=1 included) and the Tiltfile with the reset commands on a mismatch, before they apply the chart or query the database.
6efcff2 to
ff45e93
Compare
nickdujay-pplx
left a comment
There was a problem hiding this comment.
Reviewed ff45e93 against its base, including the stacked consumers. No blocking issue found.
Independently ran the rendered Postgres configuration on real PostgreSQL 17.11: TLS accepted, plaintext TCP rejected, space_admin was non-superuser with the required role-management privileges, and bootstrap-roles → migrations → bootstrap-roles succeeded. A further bootstrap was convergent; all 72 public tables belonged to space_migrator, and a database restart preserved the migrated state and TLS access. Reviewed the migration deadline/failure handling and the PG_VERSION guard in all three bring-up paths. Exact-head chart renders passed, and all four Kind CI shards passed.
Upgrade/rollback constraint: an existing PG16 Kind database requires the documented deliberate reset. Reverting the image to PG16 cannot reuse PG17 data; recreate/reset the local database when reverting across that boundary. This is not an in-place database upgrade or downgrade. I did not independently rerun the Linux/KVM suite or the maximum-connection memory stress test.
Summary
Kind's Postgres now behaves like the database SPACE deployments run, so a green Kind run covers the production connection and provisioning paths that Kind used to skip.
What Kind now does like deployments, and the code paths it exercises
postgres:17(17.11); CI takes it from the GHCR mirror at a pinned linux/amd64 digest (ci/mirrored-images.txt)sslmode=requireon every client connectionspace-postgresSecret's connection string carriessslmode=require, so api, admin-api, internal-api, cluster-controller, credmgr-authority and the migrate Job all connect over TLS. So do the harness'spsqlhelpers (kind_stack_psql,space_smoke/adapters.py,smoke-kind-sandbox-fork.py,run-local-smokes.sh,bootstrap-prod-seed.sh), over loopback TLS inside the pod. The server's certificate is self-signed per pod start, andpg_hba.confrefuses unencrypted TCPdatabase_connect_optionskeeps the URL's TLS options when it swaps in the managed identity), the migrator's TLS connect,PgListenerover TLSrds_superuserspace_admin, which hasCREATEDB CREATEROLE, holdsrds_superuserWITH ADMIN OPTION and owns the database. The pod's init script creates that setupbootstrap_roles.rs:converge_superadmin_attributesgrantsrds_superuserinstead ofSUPERUSER;converge_role_attributesverifies the privileged attributes instead of resetting them. The migrator'sassume_migrator_roledoes itsSET ROLEfrom a non-superuser sessionPOSTGRES_POOL_MAX_CONNECTIONSis 100 for space-api and space-admin-api, 30 for internal-api, and unset elsewhere.max_connectionsis 1400, which holds the sum of every replica's pool maxima (HPA maxima plus a rollout surge; the arithmetic is inspace-postgres/values.yaml) plus the Job and headroom. The memory limit goes from 512 MiB to 8 GiB, measured to hold that many backends (see Validation); the request stays 256 MiBpostgres_pool_options(crates/libs/common/svc/src/sql/mod.rs) with non-default sizes. Exhausting a pool, not a server refusal, is now what limits a service. On main, Kind'smax_connectionsof 100 was below the sum of the default poolslog_lock_waits = on; the roles'lock_timeoutofflog_lock_waits=on;SPACE_DB_APP_LOCK_TIMEOUTstays unset, which leaveslock_timeoutoff the same waybackoffLimit: 0,restartPolicy: Never,activeDeadlineSeconds: 1800passed to the migrator asSPACE_DB_MIGRATE_JOB_DEADLINE_SECONDS; bootstrap → migrate → bootstrap unchanged.bootstrap.shstops on a Failed Job and prints its logs, where it used to wait out its timeoutmigration_retry::LockRetryPolicy::for_job_deadlinesized against the Job's real deadlineThe managed-service role setup, and what only a managed service provides
A stock
postgres:17has none of this, sospace-postgres/templates/configmap.yamlcreates it once, when the image initializes an empty data directory. I checked every line by dropping it and rerunningbootstrap-rolesas the master:rds_superuser(the role itself). Without it,bootstrap-rolestakes the plain non-superuser path, which fails below.CREATEROLEon the master: role creation and memberships.CREATEDBon the master: without it,ALTER ROLE space_superadmin CREATEDBfails with "permission denied to alter role".rds_superuserWITH ADMIN OPTION: without it,GRANT rds_superuser TO space_superadminfails with "permission denied to grant role".GRANT SET ON PARAMETER log_statement TO rds_superuser. This is the one thing a managed service provides that stock PostgreSQL needs emulated:prepare_sessionmutes statement logging before it writes role passwords and refuses to continue otherwise ("permission denied to set parameter").public(pg_database_owner). Without it,bootstrap-rolesstill reportsconverged, but theGRANT … ON SCHEMA publicstatements only raise "no privileges were granted" warnings andspace_migratornever receives CREATE (see Observations).The image's own superuser remains, used only by the image's init and the harness's direct-state
psql. That is a README row.Clusters from before this change
PostgreSQL 17 cannot start on the PostgreSQL 16 data directory of a cluster brought up from
main, and the init script runs only on an empty one, so such a cluster resets its database. A newinfra/space/kind/scripts/check-postgres-data.shreads the data directory'sPG_VERSIONon the node that holds the volume (the local-path PV's host path), so it answers the same whether the old server is still running, the new one is crash-looping on the old data, or no pod runs. On a mismatch it prints the reset commands and exits 1, changing nothing.bootstrap.sh(beside the other existing-cluster checks), the smoke bring-up (kind_stack_assert_postgres_data, beside the mount check, soSKIP_BOOTSTRAP=1runs it too) and theTiltfile(a load-timelocal(), like the other cluster checks) all run it before they apply the chart or query the database.What stays different (README rows)
requiredoes not verify it, as deployed clients do not. Kind also refuses unencrypted TCP, so no Kind client runs in plaintext and one that setssslmode=disablefails. A client that omitssslmodestill connects over TLS (libpq and sqlx default toprefer), so Kind cannot catch a connection string that losessslmode=require.Validation
All runs were on one Linux/KVM devbox with CI's settings (
SPACE_KIND_TELEMETRY_MODE=otel,DATA_PLANE_URL=http://127.0.0.1:8090, the admin token file and cold-receipts directory created as inkind-ci.yml). Before every suite a health gate waited for Tilt to be quiet, no ReplicaSet younger than 180 s, every node Ready and published (wait-node-publication.sh), then a 120 s settle.The whole sequence below ran twice. The first time was on 7f65b45 and then fd230d3, based on 0f1d3be. The second time was on the pushed head 20c0205, based on 9254f09, from a torn-down cluster. The results were the same both times; the numbers are from the second run. The pushed head's own diff is identical to fd230d3's (only hunk offsets moved in the rebase).
Stale bench. Brought up with
bootstrap.shfrommain9254f09:postgres:16-alpine,server_version_num160015,ssloff.main'ssmoke-kind-e2e.shpassed on it first, so it had real state.SKIP_BOOTSTRAP=1server does not support SSL, but SSL was required, thencould not read node_k8s_tombstones. This is the reported failureSKIP_BOOTSTRAP=1bootstrap.shpostgres:16-alpinetilt upcheck-postgres-data.shafter about 2 s, with the reset commands; nothing applieddatabase files are incompatible with server), and the check still refuses (rc 1)kubectl delete), thentilt trigger "(Tiltfile)"Fresh (reset) bench, from the head. Pod image IDs of every SPACE service were the same before and after the suites; the only difference is the sandbox pods'
pauseimage. The deployed migrator and API reportgit_sha20c0205.smoke-kind-e2e.sh(full bring-up path, Tilt live)ecw-surface:local.concurrent_restore_converges_to_one_executionnative-surface:native_concurrency.create_idempotencynative-surface:native_recovery.queued_create_survives_api_restartecw-surface:local.suspend_restore_cross_node_from_object_storageecw-surface:ecw.sandbox_list_paginationPostgres on the live head cluster:
SHOW sslison,max_connections1400,log_lock_waitson,server_version17.11.pg_stat_ssl×pg_stat_activity: every client backend is TLS 1.3 (space_user,space_credsand the harnesspsql). A sampler every 30 s through the scenarios saw 186 backend observations, all encrypted, peak 69.no pg_hba.conf entry … no encryptionlines (431) came in the 52 s after the reset. They came from two pod IPs that no pod holds now: the stale bench'smain-era control-plane pods, still on the old Secret'ssslmode=disable, until Tilt replaced them. None came after that.POSTGRES_POOL_MAX_CONNECTIONSis100on space-api and space-admin-api,30on internal-api, and unset on cluster-controller and credmgr-authority. The migrate Job hasbackoffLimit0,restartPolicyNever andactiveDeadlineSeconds1800, and it completed. The only superuser is the image's own (spaceuser), and all 72 public tables are owned byspace_migrator.postgresaccount, chowned by name. The server accepted it.Memory limit (method and result). Each test session is a
psqlover TLS that reads every public table once (SELECT count(*)on all 72) and then sits inpg_sleep. Memory is the Postgres container's cgroupmemory.currentandmemory.peak.OOMKilled, exit 137). Postgres restarted on its volume and the stack recovered.postgres:17with the same flags, TLS and the Kind database's schema, with no limit: 1,397 sessions (max_connectionsminus the superuser reserve) settle at 5.3 GiB (4.6 GiB anon). That is linear at about 3.3 MiB per backend (1.66 GiB anon at 500, 3.3 GiB at 1,000).smoke-kind-e2e.shpasses afterwards.Not rerun this round: the full typed suite (an earlier revision passed all 102 scenarios on both
mainand this branch), and the earlier round's fail-fast drill for the migrate Job (its chart andbootstrap.shcode did not change this round).Review follow-up
Restack onto
main(after #391 and #241):space-cluster.dbPasswordinclude is kept, withPOSTGRES_POOL_MAX_CONNECTIONSafter it. Inbootstrap-kind-stack.sh, Unify Kind telemetry and log routing under observability profiles #241's detached bring-up text is kept besidekind_stack_assert_postgres_data, which still returns early for a missing cluster. The commit's content is otherwise unchanged.Earlier rounds:
REPO_ROOTfirst, which failedscripts/tests/test-kind-tombstone-sweep.sh(it sources the library without one).sslmodeclaim: fixed. An omittedsslmodedefaults toprefer(libpq, sqlx 0.9) and connects over TLS, so the README, theconfigmap.yamlcomment and this text now say the server refuses plaintext andsslmode=disablefails. I added no live check: apg_stat_sslcheck seespreferasssl = t, so it could not catch a droppedsslmode.check-postgres-data.shruns inbootstrap.sh, the smoke bring-up (SKIP_BOOTSTRAP=1too) and the Tiltfile. It readsPG_VERSIONfrom the volume, not the server's socket, which also covers the crash-looping re-apply state.runAsUser: 999: fixed. The init container chowns the key topostgresby name instead of pinning a uid; its comment also notes that the alpine images ship noopensslCLI.Observations (not changed here)
cargo testfixtures still usepostgres:16-alpine(crates/libs/db-test-fixtures/src/lib.rs,crates/libs/db/migrator/src/pg_test_support.rs,ci/scripts/lib/test-deps.sh, and the test-container line inci/mirrored-images.txt). Moving them to 17 changes every Rust test run, so it belongs in its own follow-up.public,bootstrap-rolesreportsconvergedeven though its schema GRANTs only raised "no privileges were granted" warnings, and the failure surfaces later in migrations.bootstrap-roleslogs nothing at the default log level. Only sqlx notices atinfoshow which branch ran.infra/space/aws/reference/addons/charts/postgresql/templates/secret.yaml) still connects withsslmode=disable.Rollback
This is not an in-place database upgrade or downgrade. A Kind cluster whose Postgres data an earlier chart initialized (PostgreSQL 16) needs the documented reset, and the new preflight says so. Going back is the same: a PostgreSQL 16 chart, for example a branch from before this PR on a cluster created after it, cannot use PostgreSQL 17 data, and the old chart has no check to name the problem. Reset the database, or recreate the cluster, whenever you cross that boundary in either direction.
This change was prepared with AI assistance (Claude Code) and validated as described above.
🤖 Generated with Claude Code