From e014fd5b71fa5eb5ddb3a9f49ed3e7ffbb4bce3f Mon Sep 17 00:00:00 2001 From: chrisfu Date: Thu, 14 May 2026 22:46:46 -0700 Subject: [PATCH] feat(k3s): add pg.prole.org as CNPG postgres endpoint with split-horizon DNS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - knoe-db.yaml: switch to CNPG-managed TLS cert with serverAltDNSNames (pg.prole.org + knoe-db-rw cluster service) — removes static serverTLSSecret/serverCASecret - dns.yml: add pg.prole.org A record to prole_k3s_dns_records (10.0.0.3, 10.0.0.6) for Ansible-managed split-horizon DNS via Samba AD DC - k3s.cfg: align KNOE_HOME paths to ~/dev/prole, add PROLE_KDC_* vars, remove hardcoded KUBECTL_CONTEXT (kubeconfig current-context is authoritative) - prod.cfg: add PROLE_KDC_STORAGE_CLASS = prole-iscsi - onepassword.py: skip vault check gracefully when no 1Password session active (non-TTY) - CLAUDE.md: document production postgres connection string and DNS/CA cert ops Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 33 +++++++++++++++++++ conf/k3s.cfg | 19 +++++++---- conf/service/prod.cfg | 12 ++++--- .../inventory/group_vars/all/dns.yml | 3 ++ k8s/knoe/knoe-db.yaml | 5 +-- knoe/core/onepassword.py | 17 +++++++++- 6 files changed, 74 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 998b89d..760efcb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -206,3 +206,36 @@ Quick reference. Each entry links to the master index where context, owner, and **Closed in 2026-04-29 stabilization session:** Garage on DB cluster removed; cluster pods migrated to `cnpg-backup-sa` via CNPG v1.29.0 `spec.serviceAccountName`; both operators restarted clean. **Closed 2026-05-01:** R4 — installer env-contamination guard now live in `deploy.sh` (strict) + `install.sh` (informational notice). Helper at [`etc/preflight_kubecontext.sh`](etc/preflight_kubecontext.sh). + +--- + +## k3s CNPG database (production) + +The prole.org k3s CNPG cluster is this project's **production** PostgreSQL database. + +```bash +psql "host=pg.prole.org port=5432 user=chrisfu dbname=postgres sslmode=verify-full sslrootcert=$HOME/.knoe/knoe-db-ca.crt" +``` + +| Detail | Value | +|---|---| +| External hostname | `pg.prole.org:5432` | +| Internal service | `knoe-db-rw.knoe-db.svc.cluster.local:5432` | +| kubectl context | `prole-service-cluster` | +| Namespace | `knoe-db` | +| CA cert | `~/.knoe/knoe-db-ca.crt` | +| sslmode | `verify-full` | + +**DNS**: `pg.prole.org` resolves internally via split-horizon DNS on `myrddin.prole.org` (Samba AD DC) to the k3s ServiceLB node IPs (`10.0.0.3`, `10.0.0.6`). External DNS resolves to the public IP — do not access from outside the LAN without a VPN. + +**CA cert refresh** (after CNPG cert rotation): +```bash +kubectl --context=prole-service-cluster -n knoe-db \ + get secret knoe-db-ca -o jsonpath='{.data.ca\.crt}' | base64 -d > ~/.knoe/knoe-db-ca.crt +``` + +**Node mobility**: to move the postgres LoadBalancer to a different node, update the Samba DNS A records: +```bash +ssh myrddin.prole.org "sudo samba-tool dns delete myrddin.prole.org prole.org pg A -U Administrator" +ssh myrddin.prole.org "sudo samba-tool dns add myrddin.prole.org prole.org pg A -U Administrator" +``` diff --git a/conf/k3s.cfg b/conf/k3s.cfg index c9e0836..d0b2c2e 100644 --- a/conf/k3s.cfg +++ b/conf/k3s.cfg @@ -154,13 +154,17 @@ OP_KDC_GUEST_PASSWORD_REF = K3S_SERVER = https://myrddin.prole.org:6443 K3S_TOKEN = ${KNOE_SECRET:v1:-cslrAhG8WhxxJLY:vAx5MGOBcU1NBMjf4U-3y1djMDmwwOjiWkrutxPa4Li5P8RBjCBhEOCEdGP2CiPJJ6UQK-ietX-mu_5nO3yNNjpwDququT4U6lWHPEInSvUNH6ImU-HzPvm_diL2FwJtZm3sY5HgecIy2dwXB_vLnYaA_7VXt9zf0T70rw==} KNOE_DB_USER = root -KNOE_HOME = $HOME/dev/knoe +KNOE_HOME = $HOME/dev/prole KNOE_IMAGE_REGISTRY = registry.prole.org +PROLE_ENABLE_LOCAL_REGISTRY = true MONITORING_STORAGE_CLASS = merlin-local-iscsi-prometheus OPENTOFU_URL = http://127.0.0.1:8080 OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES = 2 PROLE_K3S_SERVER = https://myrddin.prole.org:6443 PROLE_K3S_TOKEN = ${KNOE_SECRET:v1:-cslrAhG8WhxxJLY:vAx5MGOBcU1NBMjf4U-3y1djMDmwwOjiWkrutxPa4Li5P8RBjCBhEOCEdGP2CiPJJ6UQK-ietX-mu_5nO3yNNjpwDququT4U6lWHPEInSvUNH6ImU-HzPvm_diL2FwJtZm3sY5HgecIy2dwXB_vLnYaA_7VXt9zf0T70rw==} +PROLE_KDC_TRUST_REALM = PROLE.ORG +PROLE_KDC_TRUST_ADMIN = administrator@PROLE.ORG +PROLE_KDC_STORAGE_CLASS = prole-iscsi PROTECTED_DB_HOSTS = db.prole.org PROTECTED_GIT_HOSTS = git.prole.org REDIS_HOST = redis-master.knoe-system.svc.cluster.local @@ -203,11 +207,11 @@ PORT_FORWARD_K3S_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/promethe PORT_FORWARD_K3S_MAPPING_9 = id=gitea-http;namespace=gitea;target=svc/gitea-http;address=0.0.0.0;hostPort=13000;servicePort=3000;protocol=TCP;description=Gitea Web [System Environment] -KNOE_CONF = $HOME/dev/knoe/conf -KNOE_HOME = $HOME/dev/knoe -KNOE_SERVICE = $HOME/dev/knoe/etc -PROLE_DATA = $HOME/dev/knoe/data -PROLE_LOGS = $HOME/dev/knoe/logs +KNOE_CONF = $HOME/dev/prole/conf +KNOE_HOME = $HOME/dev/prole +KNOE_SERVICE = $HOME/dev/prole/etc +PROLE_DATA = $HOME/dev/prole/data +PROLE_LOGS = $HOME/dev/prole/logs [Monitoring] MONITORING_STORAGE_CLASS = merlin-local-iscsi-prometheus @@ -249,7 +253,8 @@ CLUSTER_ENV = knoe-service-cluster DISPLAY_NAME = knoe-service-cluster K3S_SERVER_URL = https://myrddin.prole.org:6443 K3S_TOKEN = ${KNOE_SECRET:v1:-cslrAhG8WhxxJLY:vAx5MGOBcU1NBMjf4U-3y1djMDmwwOjiWkrutxPa4Li5P8RBjCBhEOCEdGP2CiPJJ6UQK-ietX-mu_5nO3yNNjpwDququT4U6lWHPEInSvUNH6ImU-HzPvm_diL2FwJtZm3sY5HgecIy2dwXB_vLnYaA_7VXt9zf0T70rw==} -KUBECTL_CONTEXT = prole-service-cluster +; k3s kubeconfig current-context is authoritative — do not set KUBECTL_CONTEXT here +; KUBECTL_CONTEXT = prole-service-cluster MODE = k3s PIPELINE_URL = http://127.0.0.1:8080 diff --git a/conf/service/prod.cfg b/conf/service/prod.cfg index 653633d..3d72f2e 100644 --- a/conf/service/prod.cfg +++ b/conf/service/prod.cfg @@ -128,6 +128,8 @@ GOOGLE_CLIENT_ID = GOOGLE_CLIENT_SECRET = PROLE_KDC_TRUST_REALM = PROLE.ORG PROLE_KDC_TRUST_ADMIN = administrator@PROLE.ORG +; k3s has no default StorageClass; use the static prole-iscsi SC (backed by /synology/d001/kdc) +PROLE_KDC_STORAGE_CLASS = prole-iscsi SERVICE_INGRESS_TLS_ENABLED = 1 MONITORING_RELEASE = prometheus @@ -157,11 +159,11 @@ PORT_FORWARD_K3D_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-graf PORT_FORWARD_K3D_MAPPING_9 = id=supabase-kong;namespace=supabase;target=svc/kong;address=0.0.0.0;hostPort=8000;servicePort=8000;protocol=TCP;description=Supabase API (Kong) [System Environment] -KNOE_CONF = /Users/chrisfu/dev/knoe/conf -PROLE_DATA = /Users/chrisfu/dev/knoe/data -KNOE_HOME = /Users/chrisfu/dev/knoe -PROLE_LOGS = /Users/chrisfu/dev/knoe/logs -KNOE_SERVICE = /Users/chrisfu/dev/knoe/etc +KNOE_CONF = /home/chrisfu/dev/prole/conf +PROLE_DATA = /home/chrisfu/dev/prole/data +KNOE_HOME = /home/chrisfu/dev/prole +PROLE_LOGS = /home/chrisfu/dev/prole/logs +KNOE_SERVICE = /home/chrisfu/dev/prole/etc [Monitoring] ; No configuration values captured yet for this section. diff --git a/infrastructure/inventory/group_vars/all/dns.yml b/infrastructure/inventory/group_vars/all/dns.yml index 9b1e453..a46c763 100644 --- a/infrastructure/inventory/group_vars/all/dns.yml +++ b/infrastructure/inventory/group_vars/all/dns.yml @@ -206,6 +206,9 @@ prole_k3s_dns_records: ipv4s: [10.0.0.3, 10.0.0.6] - fqdn: supabase.internal.prole.org ipv4s: [10.0.0.3, 10.0.0.6] + # Direct postgres access (bypasses Traefik — port 5432 via CNPG ServiceLB) + - fqdn: pg.prole.org + ipv4s: [10.0.0.3, 10.0.0.6] prole_k3s_cname_records: - fqdn: registry.prole.org diff --git a/k8s/knoe/knoe-db.yaml b/k8s/knoe/knoe-db.yaml index 87226f8..749b410 100644 --- a/k8s/knoe/knoe-db.yaml +++ b/k8s/knoe/knoe-db.yaml @@ -92,8 +92,9 @@ spec: - GRANT anon TO authenticator; certificates: - serverTLSSecret: knoe-db-tls - serverCASecret: knoe-db-ca + serverAltDNSNames: + - pg.prole.org + - knoe-db-rw.knoe-db.svc.cluster.local enableSuperuserAccess: true env: diff --git a/knoe/core/onepassword.py b/knoe/core/onepassword.py index b5436c7..445181a 100644 --- a/knoe/core/onepassword.py +++ b/knoe/core/onepassword.py @@ -68,8 +68,23 @@ def ensure_op_signed_in() -> None: subprocess.run(["op", "signin"], check=True) +def _op_session_active() -> bool: + """Return True if op has an active session.""" + r = subprocess.run(["op", "whoami"], capture_output=True, text=True) + return r.returncode == 0 + + def ensure_knoey_vault() -> None: - """Create the 'knoey' vault if it does not already exist.""" + """Create the 'knoey' vault if it does not already exist. + + Skips silently when running non-interactively without an op session. + """ + import sys + + if not _op_session_active(): + if not sys.stdin.isatty(): + print("[WARN] No active 1Password session — skipping vault check.", flush=True) + return result = _op("vault", "list", "--format", "json", check=True) try: vaults = json.loads(result.stdout or "[]")