Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
10 KiB
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
(the ServiceDefinition schema and default posture). This document maps each field of
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 (
imagefor container,vmTemplatefor 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.
publicadditionally 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.internalrenders 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 →toservice onport/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 bysize(replicated config/DB/app state). For avm, the disk is a Longhorn volume via CDI.class: bulk→ binding to the external bulk media tier (NFS/SMB indirection, sized outside the cluster;sizeoptional). 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 serviceporton the shared Gateway. - TLS: the Gateway terminates with the cert-manager wildcard
*.home.keithjackson.devcertificate (Let's Encrypt via DNS-01, scoped Cloudflare token), so no per-service cert step — trusted, non-self-signed, auto-renewed, including forinternalservices. Per-serviceCertificateonly where isolation warrants. - DNS: ExternalDNS watches the HTTPRoute and manages the
<host>record (public IP forpublic, internal Gateway VIP via PiHole split-horizon forinternal). - The
hostpattern is enforced to<label>.home.keithjackson.devby 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'singressRoutesat 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-proxyallowed_groups. Schema requires ≥1 group foroidc/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) encodingchannel/range; Flux image-automation advances the running version on its scan interval (cadence).pinfreezes the service (stops automation). No human/AI edits a tag. vmkind→ in-guest unattended package updates oncadence(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 bereacquirable. - Requirements: R-STATE-03 (off-cluster DR backup). (docs/04)
Cross-field invariants the renderer / L0-L1 tests rely on
- kind ↔ source: container⇒
image, vm⇒vmTemplate(schema-enforced). - auth ↔ groups:
oidc/forward-auth⇒ non-emptygroups(schema-enforced; R-IDP-06). - default-deny is unconditional: every namespace gets the deny baseline regardless of
dependencies;dependenciesonly adds allows (R-NET-01/02). - egress is enforced outside the workload:
vpnrenders gateway steering + kill-switch, never a per-service sidecar the app could bypass (R-NET-05/06). - TLS is never self-signed and needs no per-service step: the wildcard Gateway cert covers every
ingressRoutes.host,internalincluded (R-TLS-01/03). - 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).