R-ENV-02 promises dev and prod differ ONLY by scoped cluster-vars overrides. In fact
dev LAN addresses were hardcoded into base manifests, so clusters/prod would have come
up ANNOUNCING DEV ADDRESSES (192.168.1.x) on the prod LAN (192.168.2.x).
New cluster-vars knobs (base default = dev literal, per the file's existing convention):
K3S_API_HOST platform/cilium/helmrelease.yaml (+ the values.yaml mirror)
CILIUM_LB_POOL_START platform-config/cilium/l2-ip-pool.yaml
CILIUM_LB_POOL_STOP "
UNIFI_L4_VIP apps/vms/unifi/50-service-l4.yaml
CRAFTY_L4_VIP platform/crafty/service-l4.yaml
S3_BACKUP_ENDPOINT platform/keycloak/postgres-cluster.yaml, apps/wave-0/{nextcloud,
semaphore}-db.yaml, and platform/velero/helmrelease.yaml
Also fixes GATEWAY_VIP's base default, which was still the RETIRED libvirt address
10.73.0.240, and adds the GATEWAY_VIP patch that clusters/prod was missing entirely.
clusters/base/apps-vms.yaml gains postBuild.substituteFrom (it was the one consuming
Kustomization without it, so a ${TOKEN} there would have shipped as a literal string).
The envsubst contract is documented there: ${VAR} substitutes and escapes as $${VAR};
$VAR and $(cmd) are left alone; ${ARR[0]} hard-errors. The three UOS_URL expansions in
the unifi cloud-init are escaped accordingly.
PROD ADDRESS PLAN (operator, 2026-08-31):
nodes .10/.11/.12 · control-plane VIP .9 (Ansible-side) · GATEWAY_VIP .20
Cilium LB pool .20-.30
CONTAINMENT INVARIANT, now documented at the pool and in the prod overlay: GATEWAY_VIP
MUST equal CILIUM_LB_POOL_START. The shared Gateway pins no address, so Cilium LB-IPAM
gives it the first free address of a matching pool block. An initial prod proposal put
.20 outside a .21-.30 pool; the Gateway would have come up on .21 while split-horizon
coredns-custom kept answering *.home.keithjackson.dev with .20 — an address nothing
announces — blackholing in-cluster OIDC hairpin traffic while reporting healthy. The
pool was widened to .20-.30. The control-plane VIP (.9) stays outside the pool so
kube-vip and LB-IPAM can never contend.
Values prod still cannot know stay INVALID `TBD-PROD-*` placeholders on purpose: base
defaults are dev values, so an omitted patch means prod silently inherits a dev address
— far worse than a loud failed apply.
DEV IS BYTE-IDENTICAL. Verified by kustomize build over 13 paths before/after, then
flux envsubst with the dev cluster-vars values applied to both sides: zero drift. The
only deltas are the intended additive keys in the overlay ConfigMap itself. No
unresolved ${TOKEN} survives. render-golden green (10 goldens); yamllint clean.
Serves: R-ENV-02. Task: T-7.1 (prerequisite). Gate: MERGE GATE: PASS.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
44 lines
2.1 KiB
YAML
44 lines
2.1 KiB
YAML
---
|
|
# Flux Kustomization: VM tier (KubeVirt VirtualMachines). Reconciles ./apps/vms.
|
|
#
|
|
# NON-GATING (wait:false): unlike the app waves, this does NOT health-wait. A VM's first-boot image
|
|
# import (CDI, minutes) + in-guest software install mean a VM can be intentionally long-not-Ready, so
|
|
# health-gating it would wedge reconciliation for no benefit. VMs have no in-cluster dependents.
|
|
# dependsOn platform-config so KubeVirt/CDI + the longhorn-vm StorageClass + CDI StorageProfiles
|
|
# exist first. prune:true — this Kustomization solely owns the VM manifests (no adoption concern).
|
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
|
kind: Kustomization
|
|
metadata:
|
|
name: apps-vms
|
|
namespace: flux-system
|
|
spec:
|
|
interval: 10m
|
|
retryInterval: 2m
|
|
timeout: 5m
|
|
prune: true
|
|
wait: false
|
|
dependsOn:
|
|
- name: platform-config
|
|
sourceRef:
|
|
kind: GitRepository
|
|
name: flux-system
|
|
path: ./apps/vms
|
|
# postBuild.substituteFrom ENABLED 2026-08-31 (R-ENV-02). It was previously omitted because Flux's
|
|
# envsubst runs over the ENTIRE manifest — including VM cloud-init shell scripts — and would mangle
|
|
# every `${...}`; the omission was safe only while no VM manifest referenced a cluster-var. It now
|
|
# does: apps/vms/unifi/50-service-l4.yaml pins the UniFi L4 VIP from ${UNIFI_L4_VIP} instead of the
|
|
# hardcoded dev-LAN literal that would have shipped 192.168.1.54 onto the PROD LAN.
|
|
#
|
|
# The escape contract the old comment prescribed is now in force and MUST be honoured by every VM
|
|
# manifest under this path (verified with `flux envsubst`):
|
|
# `${VAR}` -> substituted (empty when unset) => escape shell braces as `$${VAR}`, which
|
|
# envsubst emits back as a literal `${VAR}`.
|
|
# `$VAR` -> left alone by envsubst => bare shell vars need NO escaping.
|
|
# `$(cmd)` -> left alone by envsubst => command substitution needs NO escaping.
|
|
# `${ARR[0]}` (e.g. ${PIPESTATUS[0]}) -> HARD ERROR "missing closing brace", which fails this
|
|
# whole Kustomization. NEVER use an indexed expansion here, escaped or not.
|
|
postBuild:
|
|
substituteFrom:
|
|
- kind: ConfigMap
|
|
name: cluster-vars
|