Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
156 lines
10 KiB
Markdown
156 lines
10 KiB
Markdown
# 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).
|