--- # 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 exist first, AND # platform-config-cdi so the CDI StorageProfiles do (2026-09-05: the StorageProfiles MOVED OUT of # platform-config into their own tier, so `dependsOn: platform-config` alone NO LONGER guarantees # them — see the dependsOn block). 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 # LOAD-BEARING, not decoration. apps/vms/unifi/20-datavolume.yaml provisions a DataVolume on # storageClassName `longhorn-vm`, and CDI reads StorageProfile/longhorn-vm AT PROVISIONING TIME to # decide the claim's accessModes/volumeMode. Without that profile's `claimPropertySets` # (ReadWriteMany + Block) the PVC is stamped RWO/Filesystem and the VM disk is PERMANENTLY not # live-migratable — a silent, unrecoverable-without-recreate loss. The profiles used to arrive # with platform-config; since 2026-09-05 they arrive with `platform-config-cdi`, so the edge has # to move with them. platform-config-cdi runs wait:true and StorageProfile has no status # conditions (kstatus -> Current on apply), so this edge cannot stall this tier. # # HONEST LIMIT: CDI provisions from the profile's STATUS, which its storageprofile-controller # derives from our spec. This edge proves the spec is applied, not that CDI has folded it into # status — that is a watch-triggered round-trip of seconds against a ~30s dependency requeue, so # it is a timing argument rather than a guarantee. See clusters/base/platform-config-cdi.yaml. # # This edge also makes the StorageProfile prune/recreate transition safe: platform-config's prune # deletes the two profiles for the ~30-60s before platform-config-cdi re-applies them, and this # dependsOn is what guarantees Flux cannot create a DataVolume inside that window. # # The platform-config edge above is kept although platform-config-cdi already dependsOn it: the # edges are a set, there is no cycle, and stating both means a future edit to platform-config-cdi # cannot silently drop this tier's KubeVirt/StorageClass gate. - name: platform-config-cdi 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