The 7 falsely-firing platform alerts (KeycloakDown, KeycloakDatabaseDown,
KubeAPIDown, KubeletDown, VPNTunnelDown, KubeVirtControlPlaneDown,
CDINoDefaultStorageClass) share ONE root cause, not per-rule label mismatches:
the Prometheus PVC is 100% full ("no space left on device" on every WAL commit).
Evidence from the live dev cluster:
- /prometheus is 4.8G/5Gi, 0 bytes free; every scrape logs "Scrape commit
failed ... write to WAL: no space left on device". Scrapes still return 200
(targets show healthy, lastScrape=now) but samples are never stored, so the
TSDB head froze at 2026-08-28 13:45 UTC while the pod clock reads ~19:33 UTC.
- Instant queries at "now" therefore return an EMPTY vector, so every
absent()/==0 platform rule trips. Evaluated at the last-good-sample time each
expr is correctly FALSE (keycloak-0/keycloak-db-1 ready=1, apiserver+kubelet
up=1 x3, tun0 series present, kubevirt control-plane expr empty, CDI default
present) — the rules are correct; the monitor was simply blind.
`retention` alone prunes only by AGE: this cluster is 3d2h old with 3d dev
retention, so time-pruning has NEVER run and 3d of block churn (~1.2Gi/day) plus
a WAL that ballooned past 1Gi overran the 5Gi disk. Fix adds a size-based
retention cap (new MONITORING_PROMETHEUS_RETENTION_SIZE knob) kept below each
env's PVC so Prometheus drops oldest blocks before the disk fills, and bumps the
dev PVC 5Gi->8Gi for real WAL/head/compaction headroom.
- platform/monitoring/kube-prometheus-stack.yaml: retentionSize: ${knob}
- clusters/base/cluster-vars.yaml: knob default 8GiB (base 10Gi PVC)
- clusters/dev/kustomization.yaml: PVC 5Gi->8Gi, retentionSize 5GiB
- clusters/prod/kustomization.yaml: retentionSize 45GiB (50Gi PVC)
- platform/monitoring/README.md: knob table + rationale
Deploy note (PM): the PVC is already 100% full, so Prometheus cannot self-prune
until it can write. The 5Gi->8Gi bump must land as a Longhorn online expansion
(operator PVC expansion / manual patch); once free space returns, retentionSize
keeps it healthy. Watchdog left untouched.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
clusters/
Flux entrypoints and the environment-overlay model (R-ENV-02, R-GITOPS-01/02/03).
Governing spec: docs/05-gitops-and-delivery.md,
docs/02-environments-and-provisioning.md,
ADR-0004 (Flux).
Phase 0 scaffolding only. These are structurally-correct stubs. No cluster exists — nothing here reconciles until WS-INFRA delivers a cluster and bootstraps Flux (roadmap Phase 1).
Layout — base + overlay (never fork the base)
clusters/
base/ shared Flux objects (one authoring, both environments)
flux-system.yaml GitRepository — the reconciliation source (bootstrap → Gitea)
platform.yaml Flux Kustomization → ./platform (reconciles first)
apps-wave-0.yaml Flux Kustomization → ./apps/wave-0 (dependsOn platform-config)
apps-wave-1.yaml Flux Kustomization → ./apps/wave-1 (dependsOn apps-wave-0)
apps-wave-2.yaml Flux Kustomization → ./apps/wave-N (dependsOn apps-wave-N-1)
cluster-vars.yaml ConfigMap of overlay knobs (BASE defaults)
kustomization.yaml
dev/ ACTIVE build target — patches cluster-vars with dev values only
kustomization.yaml
prod/ DESIGN-ONLY until roadmap Phase 7 — patches cluster-vars with prod values only
kustomization.yaml
How the overlay model satisfies R-ENV-02
The base authors the reconciliation graph once. platform.yaml and apps.yaml (Flux
Kustomizations) both declare postBuild.substituteFrom the cluster-vars ConfigMap. Platform and
app manifests use ${TOKEN} placeholders for every value that legitimately differs between
environments (issuer, replica counts, storage sizes, retention, node placement).
clusters/dev and clusters/prod differ ONLY by the values they patch into cluster-vars —
they add no resources of their own and never copy/fork a base manifest. Promotion to prod
(docs/02) is a change to the
prod overlay's variables, nothing else.
How Flux is pointed at an environment
flux bootstrap (run by WS-INFRA in Phase 1) targets one environment's path per cluster:
# dev cluster
flux bootstrap ... --path=clusters/dev
# prod cluster (Phase 7 only — design-only until then)
flux bootstrap ... --path=clusters/prod
Bootstrap materializes the flux-system Kustomization that renders the chosen overlay, which pulls
in base/ and applies the env-scoped cluster-vars.
Reconciliation order
GitRepository (flux-system)
│
▼
Kustomization: platform (./platform, substituteFrom cluster-vars)
│ healthy
▼
Kustomization: apps-wave-0 (./apps/wave-0, dependsOn platform-config, substituteFrom cluster-vars)
│ healthy
▼
Kustomization: apps-wave-1 (./apps/wave-1, dependsOn apps-wave-0)
│ healthy
▼
Kustomization: apps-wave-N (./apps/wave-N, dependsOn apps-wave-N-1)
Wave 0 dependsOn platform-config (transitively platform) so CNI/storage/TLS/DNS/gateway
exist before any workload reconciles. The app tier is split into dependency-ordered WAVES
(catalog/render.py --emit-waves): each apps-wave-<N> health-gates (wait:true) ONLY its own
wave, so a crashloop in a later wave can no longer stall an earlier wave or block new revisions.
See DESIGN-waves.md for the wave table + cutover runbook.
TODOs (later phases — do not action in Phase 0)
- Source cutover (WS-GITOPS, Phase 6):
base/flux-system.yamlcurrently points at the bootstrap source. After Gitea is onboarded, repointspec.urltogitea.home.keithjackson.devand keep an off-cluster mirror (specdocs/05"Source-of-truth",CONTRIBUTING.md). - SOPS decryption (WS-SEC, Phase 1): add
spec.decryption.provider: sops(age secret name only, never plaintext) to the platform/apps Kustomizations once the age key is bootstrapped (ADR-0007). - Split ordered Flux Kustomizations if intra-platform ordering needs to be explicit (e.g. a
separate
cilium/longhornKustomization othersdependsOn).