Files
home-network-k3s-impl/catalog
jakman85andClaude Opus 5.5 f13d336630
gate / gate (pull_request) Successful in 1m12s
gate / conformance (pull_request) Skipped
fix(vpn): enrol keith-phone-home and admit the WireGuard subnet to PiHole DNS
Add the at-home peer (10.90.0.3) and list ${WG_CLIENT_CIDR} in the PiHole
DNS frontends' loadBalancerSourceRanges. wg0 is a Cilium device, so tunnel
clients reach the source-range check untranslated; a LAN-only list is
predicted to drop their DNS. Adds an L0 guard that both ranges are present
and every range is RFC1918, and corrects the README's SNAT and IPv6 claims.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 20:45:48 -06:00
..

catalog — ServiceDefinition entries (the onboarding surface)

One YAML file per service. Each file is a ServiceDefinition conforming to ../schemas/servicedefinition.schema.json. Adding or editing a service here is the only onboarding step — the platform renders compute, storage, network policy, egress, TLS, DNS, monitoring, and update policy from it (see ../schemas/renderer-contract.md). Serves R-GITOPS-02.

Source of truth for the initial catalog: spec docs/13-service-catalog.md (14-service table + SSO/egress/dependency intent).

Current examples

File docs/13 row exposure egress auth Notes
pihole.yaml 6 internal internet forward-auth admin UI behind oauth2-proxy; config on Longhorn (DR-critical); also platform DNS
keycloak.yaml 0 public internet native the IdP itself (own admin login + MFA); PostgreSQL on Longhorn; DR-critical

The remaining catalog services (plex, sonarr, radarr, jackett, seerr, nextcloud, minecraft, unifi, semaphore, kasm, gitea, qbittorrent) are onboarded in later phases; they follow the same schema and inherit every guarantee automatically.

Authoring rules

  • Only name and source are mandatory; every other field defaults to docs/13's default posture (kind: container, exposure: internal, egress: internet, auth.mode: forward-auth, stable auto-update, DNS-only default-deny). Set a field only to override the default.
  • Any exposed UI must have an auth block: forward-auth (default) or oidc, each with a non-empty auth.groups; native/none are documented exceptions only.
  • Irreplaceable state (class: longhorn configs/DBs) must set backup.policy to an off-cluster schedule — never reacquirable (R-STATE-03). Only bulk media may be reacquirable.
  • Shell braces must be escaped $${...}. Anything rendered verbatim onto the workload — a lifecycle postStart script, an env value — lands under apps/wave-<N>/, and every apps-wave-<N> Flux Kustomization declares postBuild.substituteFrom: cluster-vars. Flux runs envsubst over the WHOLE manifest at reconcile time, so an unescaped ${VAR:-} meant for the shell is replaced with the empty string before the container ever starts (this silently killed gitea's and nextcloud's OIDC registration for days). Bare $VAR and $(cmd) are left alone and need no escaping; a genuine environment knob is declared in clusters/base/cluster-vars.yaml and written bare ${KNOB}. Full contract: ../clusters/base/apps-vms.yaml. render.py REFUSES to emit a manifest that breaks this rule (Flux's strict substitution mode cannot: it deliberately exempts the ${V:-} / ${V:=x} / ${V:?} default-operator forms).

Validate before commit — single command, no cluster (R-TEST-02)

check-jsonschema --schemafile schemas/servicedefinition.schema.json catalog/*.yaml

See ../schemas/README.md for install/pinning and a pure-Python fallback. This is the L0/L1 schema gate (spec docs/14); CI (.gitea/) runs it on every change.

Rendering — render.py (the ServiceDefinition renderer, T-0.3)

render.py compiles a ServiceDefinition into the concrete manifest set defined by ../schemas/renderer-contract.md: a Namespace (with the app-namespace/gateway-access labels), the default-deny baseline (R-NET-01), a Deployment+Service or a KubeVirt VirtualMachine + CDI DataVolume (R-PLAT-02/03), PVCs (R-STATE), one CiliumNetworkPolicy egress-allow per dependency (R-NET-02), the egress-class steering label (R-NET-05), HTTPRoute(s) on the shared Gateway (R-TLS/R-DNS), ServiceMonitor + SLI alerts (R-OBS-01), the Flux ImageRepository/ImagePolicy update plumbing (R-UPD), and the auth-mode / backup wiring hints (R-IDP/R-STATE-03).

python3 catalog/render.py catalog/pihole.yaml        # render one SD to stdout
python3 catalog/render.py --all catalog -o out/      # render every SD to out/<name>.yaml
python3 catalog/render.py --selftest                 # deterministic self-test (no cluster)
  • Deterministic: identical input → byte-identical output (golden-diffable). The L1 render conformance tests (conformance/tests/l1-rendering/*) diff the output against committed goldens — run offline by conformance/run.sh --layer l1 and ci/l1.sh (no cluster).
  • How CI/Flux consume it: CI invokes render.py in the L1 stage; a GitOps flow renders --all catalog -o <rendered-dir> and Flux reconciles the rendered directory. Design choice (renderer-contract is silent on mechanism): a render-then-consume script is the simplest reproducible option — see the header of render.py.
  • Dependency: PyYAML (the renderer's single dependency; the conformance harness stays stdlib-only and only shells out to this script + diffs text).