Add Postgres Database to Milner #35
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#35
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Naina:
Overview
Deploy a cluster-wide PostgreSQL via the CloudNativePG (CNPG) operator on a single-replica
Cluster, backed by a new dedicated worker data volume that survives teardowns (un-managed per the READMEremoved-block pattern). Because Talos ships no StorageClass and CNPG provisions via PVCs (not hostPath like Vault), this also deploys the Rancher local-path-provisioner — which is exactly thelocal-pathStorageClass the Woodpecker PR (#37) already assumes but nothing provides today. No existing service can migrate yet (Vault/Pocket ID don't use Postgres, Woodpecker isn't deployed), so this ships the substrate + one ready-to-consume database.Technical Approach
worker/main.tf)Attach as the next free virtio disk (
vdctoday,vddif #34's pocket-id disk merges first — order by index, never a hard-coded letter) and add a third/fourth TalosUserVolumeConfig:Tighten every data-disk selector to be mutually exclusive — vault's bare
!system_disk(worker/main.tf) matches any non-system disk and will grab the new one1. Size bands vs. the fixed capacities (vault 20 GiB2, pocket-id 10 GiB if #34 lands3, postgres 50 GiB):Whatever the merge order with #34,
worker/main.tfmust converge on this one exclusive set. Mount activates only at boot → apply thenterraform -chdir=worker apply -replace='libvirt_domain.worker'(OPERATIONS.md §4e vault precedent4).destroynever removes them5:The qcow2 stays in the CP-owned
talos-imagespool (external 4 TB_unverified_data_root_path)6 across workerdestroy/cleanup.shand re-provisioning; a recreated worker re-attaches and re-mounts it with data intact. Exact adopt-sequence (create while managed → addremoved, drop the declaration → laterterraform importon rebuild) is an implement-time reconcile against Terraform 1.7 semantics7 — the acceptance criterion below pins the outcome.postgres/Terraform root — copy the remote-state/providers preamble fromvault/main.tfverbatim, then three namespaces:8local-path-storageprivilegedcnpg-systempostgresrestrictedlocal-path-storage)1112postgres/values.yaml— provision from the new qcow2 mount, keep the class namedlocal-path(woodpecker's values already target it13), not default:Deploy the CNPG operator (helm_release, ns
cnpg-system) — chartcloudnative-pg0.29.0 (operator 1.30.0)14 fromhttps://cloudnative-pg.github.io/charts; K8s v1.34 is in its supported matrix15 (Milner runs v1.34.016).values.yamlsetsconfig.clusterWide: true(default17 — one operator serves future Clusters in other roots) andresources.requests: {cpu: 100m, memory: 200Mi}.Declare the single-instance
Cluster(kubernetes_manifest, nspostgres, depends_on the operator release so CRDs exist18)Cluster-wide reachability is CNPG's auto-created ClusterIP service19 — no Ingress, no ProxyGroup:
tests/test_static.py: addpostgres/to theterraform fmtdirs,postgres/values.yamlto the yamllint list; addtest_terraform_test_postgres()totests/test_terraform.pyif the Woodpecker PR has landed it by implement time. Update README.md workload list, ARCHITECTURE.md topology/service-table/storage-distribution, AGENTS.md repo map, and add an OPERATIONS.md Postgres section (connect creds, worker-replacenote, emergencypg_dump).Implementation Details
postgres/main.tf,postgres/values.yaml,postgres/README.md,postgres/.gitignore(copy of vault's),postgres/tests/plan.tftest.hcl+postgres/tests/fixtures/cp.tfstate(copy of vault's fixture).worker/main.tf(volume, disk attach, UserVolumeConfig, selector tightening,removedblock),worker/tests/plan.tftest.hcl,tests/test_static.py, possiblytests/test_terraform.py,README.md,ARCHITECTURE.md,AGENTS.md,OPERATIONS.md.user_volume_config_is_in_patches(asserts 2 patches + vaultmatch == "!system_disk"),worker_domain_cdrom_is_scsi(disks[2]cdrom →disks[3]),installer_iso_ejects_when_disabled(post-eject disk count 2 → 3)20. Add runs for the postgres volume (capacity/format/name), the vdc/vdd attach, and thepostgresUserVolumeConfig + selector exclusivity.cloudnative-pg0.29.0fromhttps://cloudnative-pg.github.io/charts(operator 1.30.0, supports K8s 1.34/1.35/1.36)21; local-path-provisioner chart0.0.37(OCIghcr.io/rancher/local-path-provisioner/charts, imagerancher/local-path-provisioner:v0.0.37, arm64 multi-arch verified); CNPG default operandghcr.io/cloudnative-pg/postgresql:18.4(multi-arch, not pinned).psqlviakubectl execinto the CNPG pod (flake.nix unchanged22);postgresnamespace CRD ordering handled bydepends_onon the operator release; secrets stay in the CNPG-generatedpostgres-appSecret23 (no VSO yet, no hand-run bootstrap).postgres-rwservice +postgres-appsecret are the contract a future app (e.g. evolved-npcs) consumes; Woodpecker can flip toWOODPECKER_DATABASE_*postgres env later if wanted.Service type=LoadBalancer+loadBalancerClass: tailscaleadd-on.local-pathStorageClass is cluster-shared substrate: woodpecker's PVCs will bind to the same/var/mnt/postgresvolume (50 GiB covers CNPG 20Gi + known ~13 GiB consumers with headroom; a second provisioner root/volume is a future storage issue).pg_dumpis the emergency runbook.removedblock today24) — fixing it (and pocket-id's, post-#34) is repo-wide follow-up, not this issue.enableSuperuserAccessstays defaultfalse.Acceptance Criteria
worker/main.tfshows the 50 GiBpostgresqcow2 + virtio attach +postgresUserVolumeConfig (mount/var/mnt/postgres); all data-disk selectors (vault, postgres, + pocket-id if merged) are mutually exclusive; updatedterraform -chdir=worker testis green.terraform -chdir=worker destroy/worker/cleanup.shleavesmilner-worker-1-postgres.qcow2and its data in the pool; re-provisioning the worker re-attaches and the data is still there (manual verify).postgres/root plan tests green:local-path-storageprivileged +postgresrestricted PSA labels, provisioner helm release (chart/version/root path), operator helm release (chart/repo/version), Cluster CR fields (instances 1,storageClass: local-path,20Gi,initdbapp/app,node-role: worker).postgres-1pod Ready on the worker, PVC Bound onlocal-path,postgres-appsecret exists; a throwaway pod in another namespace connects topostgres-rw.postgres.svc.cluster.local:5432, creates/drops a table, and the data survives a pod restart and a worker-replace.terraform fmtincl.postgres/, yamllint onpostgres/values.yaml); README/ARCHITECTURE/AGENTS/OPERATIONS updated and cross-references consistent.Testing Plan
python -m pytest tests/— terraform fmt now includespostgres/; yamllint coverspostgres/values.yaml; ruff unchanged.terraform -chdir=postgres test— namespace PSA labels, helm chart/repo/version + provisionernodePathMap, Cluster manifest fields (mock kubernetes/helm providers, fixture cp.tfstate copied from vault).terraform -chdir=worker test— new postgres volume/attach/UserVolumeConfig runs; updated index-sensitive runs (patch count, disk positions, eject disk count); selector-exclusivity assertion across allUserVolumeConfigs.kubectl get pods,pvc -n postgres;kubectl exec -n postgres postgres-1 -- psql -U app -d app -c 'create table t(n int); insert into t values (1);'; connect from a second namespace viapostgres-rw;kubectl -n postgres delete pod postgres-1(operator recreates, data intact); worker-replacethen re-verify; OPERATIONS.mdpg_dump/restore dry-run.Related Issues/PRs
worker/main.tfdisk selectors; selectors must converge on one mutually exclusive set.local-pathStorageClass this issue provisions.Research Findings
docker.io/bitnamicatalog (Sep 2025), so the Bitnami Helm chart's images no longer pull freely. CNPG (CNCF, EDB-originated) is the actively developed successor to Zalando/Bitnami and the de-facto standard.UserVolumeConfigmount (/var/mnt/...), with the provisioner's namespace labeledprivileged(Talos issues #10817, #11251). CNPG pods themselves are hardened non-root and satisfy PSArestricted.cloudnative-pg:1.30.0and operandpostgresql:18.4are linux/amd64 + linux/arm64; CNPG 1.30.x supports K8s 1.34 (Milner runs v1.34.0). local-path-provisioner v0.0.37 image and chart are arm64 multi-arch (chart OCI atghcr.io/rancher/local-path-provisioner/charts).kubernetes.io/basic-auth<cluster>-appSecret and auto-creates<cluster>-rw/-ro/-rClusterIP services on 5432; apps should use the service names. Vault/VSO sync is a later integration (VSO not yet deployed). External TCP exposure is aService type=LoadBalancer+loadBalancerClass: tailscaleadd-on, not Ingress.pg_dumpis the right homelab tier; ScheduledBackup/PITR waits until an S3-compatible endpoint exists.Pre-Mortem
!system_diskwildcard (or a stale index-sensitive test) in place, the postgres qcow2 gets grabbed by the wrong volume at boot and/var/mnt/postgresnever mounts. The fix half-lands against the other branch and the exclusive-selector acceptance criterion quietly slips.removed-block/import adopt-sequence is reconciled wrong, so teardown durability fails — Terraform'sdestroy = falseinsideremovedplus the laterimporton rebuild has subtle semantics, and a mis-step either deletes the qcow2 ondestroy(the issue's hard requirement) or leaves a rebuilt worker unable to re-attach, so the manual durability check fails and the PR is reworked repeatedly./var/mnt/postgresroot (PSAprivilegedlabel misplaced, Talos local-path regression, or the disk-selector/mount race at boot), leaving the CNPG PVC Pending andpostgres-1never Ready, which only surfaces in the live smoke after all plan tests pass.