refactor(apps): split monolithic apps Kustomization into dependency waves

Replaces the single `apps` Flux Kustomization with dependency-ordered waves
(apps-wave-0..2) so a crashlooping workload isolates to its wave + downstream
instead of stalling the entire app tier. Parallel within a wave, ordered across.

- catalog/render.py: compute waves by longest dependency-path depth over the
  ServiceDefinition `dependencies:` edges (a `<svc>-db` is a leaf, so its
  consumer lands strictly higher — the "DB below consumer" rule falls out of
  longest-path, no special case). `--emit-waves` generates the per-wave
  kustomizations + Flux CRs; `--wave-layout` places renders into the right dir.
  wave 0: pihole gitea jackett plex qbittorrent nextcloud-db semaphore-db
  wave 1: sonarr radarr nextcloud semaphore gitea-actions
  wave 2: seerr
- apps/*.yaml -> apps/wave-{0,1,2}/ (byte-identical moves; SOPS files intact).
- clusters/base: apps.yaml removed; apps-wave-{0,1,2}.yaml added (dependsOn
  chain: platform-config -> wave-0 -> wave-1 -> wave-2, prune+wait each).
- clusters/dev: one SOPS decryption patch per wave (load-bearing).
- DESIGN-waves.md: wave table, topology, cutover runbook.

Cutover: preceded by disabling prune on `apps` (1bfb0db) so removing that
object here does not GC its resources; the wave Kustomizations SSA-adopt the
byte-identical manifests in place (no restart, no PVC churn).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
jakman85andClaude Opus 4.8 committed 2026-08-26 12:19:00 -05:00
1 parent 1bfb0db6f1
commit 36a1a4e1f9
34 files changed
+793 -93

No files matched your search

+14 -1
View File
@@ -1,3 +1,16 @@
# apps
_Placeholder — populated in Phase 0. See ../home-network-k3s (read-only spec)._
App-tier manifests, rendered from `catalog/*.yaml` ServiceDefinitions by `catalog/render.py`
and split into dependency-ordered **waves** (`wave-<N>/`).
- Each `wave-<N>/` is reconciled by its own Flux Kustomization `apps-wave-<N>`
(`clusters/base/apps-wave-<N>.yaml`), which `dependsOn` wave N-1 (wave 0 dependsOn
`platform-config`) and health-gates (`wait: true`) ONLY that wave. A crashlooping workload
in a later wave can no longer stall an earlier wave or block new revisions.
- `wave-<N>/kustomization.yaml` and `clusters/base/apps-wave-<N>.yaml` are **generated** — run
`python3 catalog/render.py --emit-waves` after onboarding a service; do not hand-edit them.
- Wave assignment = longest dependency-path depth over the catalog `dependencies:` graph; a
co-located CNPG database `<svc>-db` is a leaf and always lands strictly below its consumer.
See [`../DESIGN-waves.md`](../DESIGN-waves.md) for the wave table, Flux topology, and the
cutover runbook.
-41
View File
@@ -1,41 +0,0 @@
---
# Onboarded services (WS-APPS). Each app is rendered from its catalog/ ServiceDefinition by
# catalog/render.py and committed here; the `apps` Flux Kustomization (clusters/base/apps.yaml,
# dependsOn platform-config) reconciles this directory. Image-automation CRs are omitted for now
# (--no-image-automation) until the Flux image controllers are installed.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- pihole.yaml
- gitea.yaml
- gitea-bootstrap-secret.sops.yaml # SOPS: fixed SECRET_KEY + break-glass admin creds (decrypted by Flux)
- gitea-actions.yaml # Phase 6 CI/CD: in-cluster act_runner (ns gitea-actions) + DinD
- gitea-actions-runner-secret.sops.yaml # SOPS: act_runner registration token (decrypted by Flux)
- sonarr.yaml
- radarr.yaml
- plex.yaml
- qbittorrent.yaml
- jackett.yaml
- seerr.yaml
- nextcloud.yaml
- nextcloud-db.yaml # CNPG PostgreSQL Cluster (ns nextcloud-db) — first CNPG-DB app
- nextcloud-db-app.sops.yaml # SOPS: CNPG owner credentials (basic-auth) for initdb
- nextcloud-bootstrap-secret.sops.yaml # SOPS: break-glass admin + mirrored db-password (ns nextcloud)
# RE-ENABLED: the two crashloop root causes are now fixed (both verified against the live image).
# (1) permission-denied writing /etc/semaphore/config.json — the image runs as non-root UID 1001;
# fixed by podSecurityContext.fsGroup=1001 (schema 1.3.0) so the mounted Longhorn PVC is
# group-writable (verified: the pod got past config write and successfully pinged the DB).
# (2) config panic "field 'Port' is not valid: tcp://<svcIP>:3000" — the kubelet injects a
# Docker-link SEMAPHORE_PORT for the same-named Service, colliding with Semaphore's own listen
# port; fixed by an explicit SEMAPHORE_PORT=":3000" env that shadows the service-link var.
# The env-driven config IS non-interactive (the wizard text is the wrapper echoing while auto-
# answering from env). OIDC remains deferred (break-glass local admin), same as gitea/nextcloud.
- semaphore.yaml
- semaphore-db.yaml # CNPG PostgreSQL Cluster (ns semaphore-db) — second CNPG-DB app
- semaphore-db-app.sops.yaml # SOPS: CNPG owner credentials (basic-auth) for initdb
- semaphore-bootstrap-secret.sops.yaml # SOPS: break-glass admin + mirrored db-password + access-key-encryption (ns semaphore)
# Gitea's headless-install env + break-glass admin postStart hook are now first-class in the
# ServiceDefinition (catalog/gitea.yaml env/lifecycle, schema >=1.2.0) and rendered inline into
# apps/gitea.yaml, so the former gitea-bootstrap-patch.yaml strategic-merge patch is retired.
# gitea-bootstrap-secret.sops.yaml is KEPT — the env still sources the fixed SECRET_KEY + admin
# creds from it via secretKeyRef.
File renamed without changes.
File renamed without changes.
+18
View File
@@ -0,0 +1,18 @@
---
# Wave 0 of the app tier — GENERATED by `catalog/render.py --emit-waves`.
# Reconciled by the Flux Kustomization `apps-wave-0` (clusters/base/apps-wave-0.yaml), which dependsOn `platform-config` and health-gates
# (wait:true) ONLY this wave — a crashloop here cannot stall an earlier wave.
# DO NOT hand-edit: re-run `render.py --emit-waves` after onboarding a service.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gitea-bootstrap-secret.sops.yaml
- gitea.yaml
- jackett.yaml
- nextcloud-db-app.sops.yaml
- nextcloud-db.yaml
- pihole.yaml
- plex.yaml
- qbittorrent.yaml
- semaphore-db-app.sops.yaml
- semaphore-db.yaml
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+16
View File
@@ -0,0 +1,16 @@
---
# Wave 1 of the app tier — GENERATED by `catalog/render.py --emit-waves`.
# Reconciled by the Flux Kustomization `apps-wave-1` (clusters/base/apps-wave-1.yaml), which dependsOn `apps-wave-0` and health-gates
# (wait:true) ONLY this wave — a crashloop here cannot stall an earlier wave.
# DO NOT hand-edit: re-run `render.py --emit-waves` after onboarding a service.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gitea-actions-runner-secret.sops.yaml
- gitea-actions.yaml
- nextcloud-bootstrap-secret.sops.yaml
- nextcloud.yaml
- radarr.yaml
- semaphore-bootstrap-secret.sops.yaml
- semaphore.yaml
- sonarr.yaml
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+9
View File
@@ -0,0 +1,9 @@
---
# Wave 2 of the app tier — GENERATED by `catalog/render.py --emit-waves`.
# Reconciled by the Flux Kustomization `apps-wave-2` (clusters/base/apps-wave-2.yaml), which dependsOn `apps-wave-1` and health-gates
# (wait:true) ONLY this wave — a crashloop here cannot stall an earlier wave.
# DO NOT hand-edit: re-run `render.py --emit-waves` after onboarding a service.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- seerr.yaml
File renamed without changes.