Filed in response to the 2026-04-28 14:00 UTC backup outage. An
`install.sh --mode k3d` run with the shell pointed at GKE silently
overwrote the GKE cluster's GCS-backed ObjectStore + ScheduledBackup
with k3d-mode defaults; Garage filled up and CNPG backups failed for
hours before the next manual check. The class of bug is "config says
target cluster A, shell context says target cluster B, installer
proceeds against B without warning."
New shared bash helper at etc/preflight_kubecontext.sh with two
functions:
- verify_kubecontext_matches_config <cfg-path>
Strict gate. Reads [Global] APP_CLUSTER_KUBECONTEXT from the
config and exits 1 if `kubectl config current-context` differs.
Skipped silently when the config has no baked APP_CLUSTER_KUBECONTEXT
(e.g. fresh k3d.cfg) or when there's no live current-context.
- print_kubecontext_notice
Informational. Prints what's about to be inherited so the user
can abort before the TUI launches if it looks wrong. Never fails.
Wiring:
- deploy.sh sources the helper and calls the strict gate against
${PROLE_DEPLOY_CFG:-conf/gke.cfg} before invoking Python.
Unattended path -> hard refusal on mismatch.
- install.sh sources the helper and calls the informational notice
(gated on not-`--min`) right after entering the local-checkout
branch. The TUI is interactive, so the strict mode-aware gate is
a follow-up once the welcome screen records a mode in
state.inputs.
Bypass for deliberate cross-cluster maintenance:
KNOE_SKIP_KUBECONTEXT_GUARD=true ./deploy.sh
End-to-end verified:
- deploy.sh with current=cnpg-0, gke.cfg=app-0 -> exit 1, clear msg
- deploy.sh with KNOE_SKIP_...=true -> bypasses, prints
"skipping check"
- install.sh --min -> notice skipped
- install.sh (no flag) and install.sh --silent -> notice printed
Doc updates:
- CLAUDE.md §"Env-contamination warning" rewritten to describe the
live guard (was a forward-looking TODO).
- CLAUDE.md drift table row R4 removed; "Closed 2026-05-01" line added.
- docs/TODO.md queue item #1 archived to Done; R4 dropped from the
reality-vs-intent table. Queue numbering retained (no #1 placeholder)
so the docs/plans/junie/<NN>-...md filenames still match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.2 KiB
CLAUDE.md — knoe project context
Loaded automatically by Claude Code (CLI and IntelliJ plugin) as project context.
Repo role: customer deploy of knoe-db
This working tree (prole) is being converged into a customer-deploy branch of the upstream platform repo knoe-db (remote: git@git-ssh.knoe.dev:knoe.dev/knoe-db.git, configured here as the knoe remote). Platform code — authority/, knoe/, etc/init_*.sh, deploy/gcp/gke/*, the test pipeline — is not maintained in this repo; its source of truth is knoe-db/main. Customer-specific divergence (config, branding, on-prem prole.org manifests) is what stays on the customer branch.
Before changing anything in this tree, check whether the change belongs upstream. If it's platform code, land it on knoe-db/main and rebase the customer branch.
Active plans
docs/plans/customer-deploy-resync.md— The active plan for convergingproleontoknoe-db/mainas a customer-deploy branch. Inventory + cherry-pick + rebase cadence. Read this first if you are about to touch anything in this tree.docs/plans/README.md— Index and conventions for this directory.
For platform-level plans (knoe-auth, deployment modes, etc.), read knoe-db/docs/plans/ (path relative to your ~/dev/ root). Those documents are the architectural reference for the shipped platform features and are versioned with the upstream code, not here.
Dual-cluster GKE architecture
This project uses two separate GKE Standard clusters in us-west3, both currently provisioned with e2-standard-2 × 3 nodes (2 vCPU / 8 GB each, ~7.1 GB allocatable). Verify with gcloud container clusters list — knoe/core/actions.py still carries an e2-small default for the app cluster but the live cluster is e2-standard-2.
| Cluster | Context | Role |
|---|---|---|
knoe-dev-0 |
gke_plenary-truck-485623-p7_us-west3_knoe-dev-0 |
App cluster — Garage, registry, OpenBao, Kong, GitLab, monitoring |
knoe-cnpg-0 |
gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0 |
DB cluster — CNPG/PostgreSQL only (note: conf/gke.cfg currently deploys this as knoe-dev-cnpg-0; the code default remains knoe-cnpg-0) |
Storage quota: the project has SSD_TOTAL_GB = 300 GB in us-west3, fully consumed by CNPG. All non-CNPG PVCs must use standard (pd-standard / HDD) — not standard-rwo / premium-rwo, which are SSD-backed and will fail to provision with a quota error. GITLAB_GITALY_STORAGE_CLASS = standard is set in conf/gke.cfg accordingly.
Resource allocation
| Resource | Cluster | Namespace |
|---|---|---|
| CNPG operator | knoe-cnpg-0 |
cnpg-system |
PostgreSQL cluster (knoe-db) |
knoe-cnpg-0 |
knoe-db-0 |
| Barman Cloud plugin | knoe-cnpg-0 |
cnpg-system |
| cert-manager | knoe-cnpg-0 |
cert-manager |
| Garage (S3 object store) | knoe-dev-0 |
knoe-system |
| Registry | knoe-dev-0 |
knoe-system |
| OpenBao | knoe-dev-0 |
knoe-system |
| Kong API gateway | knoe-dev-0 |
knoe-system |
| Monitoring | knoe-dev-0 |
monitoring |
Garage runs ONLY in knoe-dev-0. Do NOT deploy Garage to knoe-cnpg-0.
CNPG backups → GCS (not Garage)
Backups use GCS with Workload Identity (not Garage):
- Data bucket:
gs://knoe-0-backups/ - WAL bucket:
gs://knoe-0-wal/ - GCP SA:
cnpg-backup@plenary-truck-485623-p7.iam.gserviceaccount.com - K8s SA:
cnpg-backup-sainknoe-db-0(annotated with WI) - ObjectStore manifest:
k8s/knoe/knoe-db-barman-objectstore-gcs.yaml
Setup script: etc/init_cnpg_gke.sh (creates buckets, GCP SA, WI binding, applies CNPG cluster).
Service mesh (Cloud Service Mesh / Istio)
Both clusters are registered in the knoe-0 GCP fleet with automatic Cloud Service Mesh management. This is automated in scripts/reset_clusters.sh (Phase 7) — no longer requires GCP web console.
# Check mesh provisioning status (~10 min after cluster creation):
gcloud container fleet mesh describe --project=plenary-truck-485623-p7
# Manual re-registration if needed:
gcloud container fleet memberships register knoe-dev-0 \
--gke-cluster=us-west3/knoe-dev-0 \
--enable-workload-identity \
--project=plenary-truck-485623-p7
gcloud container fleet mesh update \
--management=automatic \
--memberships=knoe-dev-0 \
--project=plenary-truck-485623-p7
install.sh / deploy.sh pre-flight checklist
conf/gke.cfg (interactive installer — ./install.sh)
Before running ./install.sh (especially "Initialization Scripts"), confirm these are correct:
[Inputs]
init_cluster.app_cluster_kubecontext = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
init_cluster.db_cluster_kubecontext = gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0
env_setup.APP_CLUSTER_KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
env_setup.DB_CLUSTER_KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0
[Global]
KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
APP_CLUSTER_KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
DB_CLUSTER_KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0
CNPG_ELIGIBLE_NODES = <comma-separated node names from knoe-cnpg-0>
conf/service/prod.cfg (unattended deploy — ./deploy.sh)
Same cluster context entries are required here too:
[Inputs]
init_cluster.app_cluster_kubecontext = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
init_cluster.db_cluster_kubecontext = gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0
env_setup.APP_CLUSTER_KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
env_setup.DB_CLUSTER_KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0
[Global]
KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
APP_CLUSTER_KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
DB_CLUSTER_KUBECONTEXT = gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0
ARTIFACT_REGISTRY = us-west3-docker.pkg.dev/plenary-truck-485623-p7/knoe-system
SERVICE_NAMESPACE = knoe-system
REGISTRY_NAMESPACE = knoe-system
Why these matter: Milestone._get_script_env() (in knoe/milestone.py) reads these to set KUBECONTEXT=app_ctx for common services and DB_CLUSTER_KUBECONTEXT=db_ctx for CNPG ops. Without them, all kubectl calls use the ambient context, which may be the DB cluster.
Missing init_cluster.app_cluster_kubecontext → _cluster_kubecontext("app") returns "" → installer falls back to Global.KUBECONTEXT for both app and db environments → Garage deploys to knoe-cnpg-0 (wrong).
Env-contamination guard (live):
deploy.shcallsetc/preflight_kubecontext.shand refuses to proceed whenkubectl config current-contextdoesn't match the[Global] APP_CLUSTER_KUBECONTEXTof the active config.install.shprints the inherited context up-front (mode-aware strict gate is the Python TUI's responsibility once the welcome screen records a mode). Bypass withKNOE_SKIP_KUBECONTEXT_GUARD=truefor deliberate cross-cluster maintenance. History: the guard was filed in response to the 2026-04-28 14:00 UTC outage — aninstall.sh --mode k3drun with the shell pointed at GKE replaced the GCS-backed ObjectStore with a Garage-backed one, then Garage filled up and backups silently failed for hours. Closes drift R4 / queue item #1.
Get current CNPG node names
kubectl --context=gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0 get nodes -o name
Key scripts
| Script | Purpose |
|---|---|
scripts/reset_clusters.sh |
Delete + recreate both GKE clusters (e2-standard-2 × 3 each) |
scripts/patch_garage_cross_cluster.sh |
Remove garage from DB cluster, configure GCS barman ObjectStore |
etc/init_cnpg_gke.sh |
Provision CNPG on GKE with GCS backup via Workload Identity |
etc/init_cnpg_backup.sh |
Configure barman plugin + trigger initial backup |
Cluster code constants (knoe/core/actions.py)
DEFAULT_APP_CLUSTER_NAME = "knoe-dev-0"
DEFAULT_DB_CLUSTER_NAME = "knoe-cnpg-0" # also: DEFAULT_CNPG_CLUSTER_NAME
DEFAULT_APP_CLUSTER_MACHINE_TYPE = "e2-small" # STALE — live app cluster is e2-standard-2
DEFAULT_DB_CLUSTER_MACHINE_TYPE = "e2-standard-2"
GCP project
- Project:
plenary-truck-485623-p7 - Region:
us-west3 - Artifact Registry:
us-west3-docker.pkg.dev/plenary-truck-485623-p7/knoe-system - CPU quota: 16 vCPUs (all regions) — 2 clusters × 3 ×
e2-standard-2= 12 vCPUs used - SSD quota:
SSD_TOTAL_GB = 300 GB— fully consumed by CNPG; all other PVCs must usestandard(pd-standard / HDD)
Reality TODOs / Drift Log
Quick reference. Each entry links to the master index where context, owner, and rank live.
| # | Drift | Where described above | Where tracked |
|---|---|---|---|
| (none currently) |
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.