Files
home-network-k3s-impl/schemas/renderer-contract.md
T
2026-08-16 00:08:30 -05:00

156 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Renderer Contract — ServiceDefinition → rendered resources
**Status:** authoritative Phase-0 interface contract. **Consumed by** the manifest renderer
(WS-GITOPS), the network/egress renderer (WS-NET), storage (WS-STATE), TLS/DNS (WS-EDGE), identity
(WS-SEC), observability (WS-OBS), updates (WS-UPD), and the conformance/policy/CI agents (WS-TEST).
**Source of truth:** spec [`docs/13-service-catalog.md`](../../home-network-k3s/docs/13-service-catalog.md)
(the ServiceDefinition schema and default posture). This document maps each field of
[`servicedefinition.schema.json`](./servicedefinition.schema.json) to the concrete resources the
platform must render, and cites the requirement each mapping satisfies. Changing this contract is an
**interface change** — coordinate per the spec's `agents/coordination-protocol.md` (§2).
> The renderer MUST first materialize the schema **defaults** (below), then apply this mapping. A
> field absent from a ServiceDefinition renders from its default posture, not "not at all."
## Default posture (applied before mapping) — docs/13 §"Default posture"
| Field | Default | Requirement |
|---|---|---|
| `kind` | `container` | R-PLAT-02/03 |
| `exposure` | `internal` | R-DNS/R-NET |
| `egress` | `internet` | R-NET-05 |
| `storage` | `[]` (config PVC still expected for stateful svc) | R-STATE-02 |
| `dependencies` | `[]` → reaches **only DNS** (default-deny) | R-NET-01 |
| `auth.mode` | `forward-auth` for any exposed UI | R-IDP-02 |
| `update.channel` | `stable` | R-UPD-01 |
| `update.autoRollbackOnFailedHealth` | `true` | R-UPD-03 |
| `observability.metrics` | `true` | R-OBS-01 |
| backup of irreplaceable state | off-cluster (never `reacquirable`) | R-STATE-03 |
---
## Field → resource mapping
### `name`
- **Renders:** the service Namespace (trust zone), Deployment/VM name, Service name, and the DNS label
`<name>.home.keithjackson.dev`.
- **Requirements:** R-DNS-01 (naming model, docs/09), R-NET (namespace = isolation boundary, docs/03).
### `kind` + `source`
- `container` + `source.image` → **Deployment** (+ Service) using the image ref.
- `vm` + `source.vmTemplate` → **KubeVirt VirtualMachine** with a **CDI**-imported root disk.
- The schema enforces exactly-one-of (`image` for container, `vmTemplate` for vm).
- **Requirements:** R-PLAT-02/03 (PaaS/IaaS, docs/13); VM disks inherit Longhorn durability (docs/04).
### `exposure` (`internal` | `public`)
- Both classes render an **HTTPRoute** on the shared Gateway and are reachable on the LAN via internal
split-horizon DNS.
- `public` **additionally** signals the documented **manual edge port-forward** of 80/443 (and any
service-specific port, e.g. Plex 32400, Minecraft 25565) to the ingress VIP, and a **public**
ExternalDNS record for `<name>.home.keithjackson.dev`.
- `internal` renders **no public DNS record** and is never port-forwarded (PiHole resolves it to the
internal Gateway VIP).
- Exposure does **not** relax east-west default-deny (docs/09 §"Interaction with default-deny").
- **Requirements:** R-DNS-02/03 (docs/09), R-NET-04 (docs/03).
### `egress` (`internet` | `vpn`) — egress steering
- `internet` → pod egress allow to the default WAN route (node → SOHO edge → internet).
- `vpn` → the pod is **steered through the shared in-cluster Gluetun/PIA (WireGuard) egress gateway**
(Cilium Egress Gateway / routing). The pod gets **no independent egress path**; the
**kill-switch (fail-closed)** and allow rules are enforced at the gateway + namespace/policy level,
**outside** the workload.
- **Requirements:** **R-NET-05** (egress class), **R-NET-06** (VPN kill-switch, mandatory & central).
VPN-class set is normative: sonarr, radarr, jackett, qbittorrent (docs/03/13).
### `dependencies[]` (`to` / `port` / `protocol`) — east-west policy
- Each namespace first gets the **default-deny baseline** (deny all ingress + egress) applied by
GitOps at namespace creation, with a standing allow only for **DNS to the in-cluster resolver** and
for **observability scraping** from the monitoring namespace.
- Each `dependencies[]` entry renders one **CiliumNetworkPolicy** egress allow: this service → `to`
service on `port`/`protocol`. Everything undeclared stays denied.
- Empty `dependencies` ⇒ the service can reach **only DNS**.
- **Requirements:** **R-NET-01** (default-deny baseline), **R-NET-02** (explicit allow / deny
undeclared). Auditable via Hubble (R-SEC-03, R-OBS-04).
### `storage[]` (`name` / `size` / `class`) — PVCs
- `class: longhorn` → a **PVC** on the 3-replica Longhorn StorageClass sized by `size` (replicated
config/DB/app state). For a `vm`, the disk is a Longhorn volume via CDI.
- `class: bulk` → binding to the external **bulk** media tier (NFS/SMB indirection, sized outside the
cluster; `size` optional). Not 3× replicated.
- State separation rule: anything that must survive a rebuild lives on a named volume here, never on
ephemeral fs/hostPath — so the workload can be destroyed and re-applied from git with data intact.
- **Requirements:** **R-STATE-01** (rebuild survival), **R-STATE-02** (Longhorn replication),
**R-STATE-04** (bulk tier). (docs/04)
### `ingressRoutes[]` (`host` / `port`) — HTTPRoute + Certificate + DNS
- Each entry renders an **HTTPRoute** attaching `host` → the service `port` on the shared Gateway.
- TLS: the Gateway terminates with the cert-manager **wildcard `*.home.keithjackson.dev`** certificate
(Let's Encrypt via **DNS-01**, scoped Cloudflare token), so no per-service cert step — trusted,
non-self-signed, auto-renewed, including for `internal` services. Per-service `Certificate` only
where isolation warrants.
- DNS: **ExternalDNS** watches the HTTPRoute and manages the `<host>` record (public IP for `public`,
internal Gateway VIP via PiHole split-horizon for `internal`).
- The `host` pattern is enforced to `<label>.home.keithjackson.dev` by the schema.
- **Requirements:** **R-TLS-01/02/03** (docs/08), **R-DNS-01/02/03** (docs/09).
### `auth` (`mode` / `groups[]`) — identity integration (Keycloak / oauth2-proxy)
- `oidc` → register the app as a **Keycloak OIDC client** (declarative realm/client config, GitOps;
client secret SOPS-encrypted). App does its own login redirect.
- `forward-auth` → protect the service's `ingressRoutes` at the **Gateway** with **oauth2-proxy →
Keycloak** so auth happens before traffic reaches the app (bare UIs without usable OIDC).
- `native` → no platform SSO wiring; app owns its auth (documented exception, e.g. Plex/Seerr; and
Keycloak **itself** as the IdP, admin login local + MFA).
- `none` → no auth wiring (internal-only, discouraged).
- `groups[]` → the Keycloak groups permitted access, enforced by the OIDC client mappers /
oauth2-proxy `allowed_groups`. **Schema requires ≥1 group for `oidc`/`forward-auth`** — access is
"in an authorized group," not merely "logged in."
- Keycloak, oauth2-proxy, and app→Keycloak / proxy→Keycloak / Keycloak→DB flows are ordinary
workloads under default-deny with explicit allows (ties back to `dependencies`).
- **Requirements:** **R-IDP-02** (protect OIDC-capable and bare UIs), **R-IDP-06** (group-based
authorization). (docs/15, ADR-0014)
### `update` (`channel` / `range` / `cadence` / `autoRollbackOnFailedHealth` / `pin`)
- container `kind` → a Flux **ImagePolicy** (+ ImageRepository) encoding `channel`/`range`; Flux
image-automation advances the running version on its scan interval (`cadence`). `pin` freezes the
service (stops automation). No human/AI edits a tag.
- `vm` `kind` → in-guest unattended package updates on `cadence` (cloud-init), per docs/11 §3.
- `autoRollbackOnFailedHealth: true` → health-gated rollout that reverts on failed health and alerts.
- **Requirements:** **R-UPD-01** (stable only), **R-UPD-02** (no human/AI authoring), **R-UPD-03**
(health-gated auto-rollback). (docs/11, ADR-0009)
### `observability` (`metrics` / `slis[]`) — ServiceMonitor + alerts
- `metrics: true` → a **ServiceMonitor/PodMonitor** so Prometheus scrapes the service; logs are
shipped to Loki by the node/pod agent by default.
- each entry in `slis[]` → at least one **alert rule** (e.g. `up` → service-down alert) routed by
Alertmanager to email; a per-service Grafana dashboard row.
- **Requirements:** **R-OBS-01** (per-service SLIs/alerts). Platform-health alerts (R-OBS-04) are
rendered platform-wide, not per ServiceDefinition. (docs/10)
### `backup` (`policy`)
- a schedule ref (e.g. `daily-offcluster`) → **Velero + Longhorn** off-cluster backup schedule for
this service's volumes/namespace.
- literal `reacquirable` → **no** 1:1 backup (bulk media only). **Irreplaceable** state (configs, DBs,
Nextcloud, Gitea, PiHole config, Keycloak DB) MUST NOT be `reacquirable`.
- **Requirements:** **R-STATE-03** (off-cluster DR backup). (docs/04)
---
## Cross-field invariants the renderer / L0-L1 tests rely on
1. **kind ↔ source:** container⇒`image`, vm⇒`vmTemplate` (schema-enforced).
2. **auth ↔ groups:** `oidc`/`forward-auth` ⇒ non-empty `groups` (schema-enforced; R-IDP-06).
3. **default-deny is unconditional:** every namespace gets the deny baseline regardless of
`dependencies`; `dependencies` only *adds* allows (R-NET-01/02).
4. **egress is enforced outside the workload:** `vpn` renders gateway steering + kill-switch, never a
per-service sidecar the app could bypass (R-NET-05/06).
5. **TLS is never self-signed and needs no per-service step:** the wildcard Gateway cert covers every
`ingressRoutes.host`, `internal` included (R-TLS-01/03).
6. **irreplaceable ⇒ off-cluster backup:** DR-critical services must not carry `backup.policy:
reacquirable` (R-STATE-03). *(Advisory: not enforced by the schema — an L0 policy check should
assert it; see "Ambiguities / escalations" in schemas/README.md.)*
## Requirement traceability (contract-level)
Primary **R-GITOPS-02** (single reviewable object wires every requirement). Field-level: R-NET-01/02
(dependencies), R-NET-05/06 (egress), R-STATE-01/02/04 (storage), R-STATE-03 (backup),
R-TLS-01/02/03 + R-DNS-01/02/03 (ingressRoutes + exposure), R-OBS-01 (observability),
R-UPD-01/02/03 (update), R-IDP-02/06 (auth), R-PLAT-02/03 (kind/source).