Files
home-network-k3s-impl/schemas/README.md
T
jakman85andClaude Opus 5 2279d14ec5
gate / gate (pull_request) Successful in 1m41s
gate / conformance (pull_request) Has been skipped
feat(schema): declare a host hardware capability, hardware.gpu: intel-quicksync
A ServiceDefinition could not say it needs the host's Intel QuickSync render
device. The schema is `additionalProperties: false`, so there was no authorable
form for it at all, and `apps/` is generated — a hand-edit would be reverted by
the next render.

Adds optional top-level `hardware`, with one optional member `gpu`, an enum
whose only current value is `intel-quicksync` (schema 1.7.0 -> 1.8.0).

The enum is the point. The SD declares WHAT CLASS of device it needs; the
hostPath, the mount and the GID live only in catalog/render.py. Migrating to
the Intel GPU device plugin (`gpu.intel.com/i915` resource requests, no
hostPath) is then a renderer change with zero catalog edits. Letting an SD name
`/dev/dri` directly would look simpler and make that migration a catalog-wide
edit, so both the schema description and the renderer contract say so
explicitly.

When `hardware.gpu == intel-quicksync`, r_deployment emits, on the app
container / pod spec only:

  - a hostPath volume `dri` -> /dev/dri, type DirectoryOrCreate;
  - a read-write volumeMount of it at /dev/dri (VA-API opens the render node
    read-write, so deliberately not readOnly);
  - INTEL_RENDER_GID MERGED into podSecurityContext.supplementalGroups, never
    clobbering what the SD already declares.

`DirectoryOrCreate` is load-bearing. A node that comes back without the render
device (rebuild, BIOS change, driver regression, hardware swap) must degrade
the workload to software transcoding, not fail the mount: apps-wave-0 is
`wait: true` and apps-wave-1 dependsOn it, so a failed hostPath mount stalls
the whole app tier for a feature that is not load-bearing. /dev is tmpfs, so
the empty directory the kubelet creates leaves nothing durable behind. Its cost
— a missing device is silent, and no metric exists to alert on — is recorded in
the contract rather than papered over with an alert that could not fire.

Deliberately NOT emitted: no `privileged: true` (Kyverno restrict-host-
namespaces is Enforce and it is not needed), no nodeSelector (placement is a
separate concern with its own justification), no resource requests (there is no
app-container resource render path).

INTEL_RENDER_GID = 992 is a single named constant carrying its provenance:
measured on all three prod nodes on 2026-09-30, and a *systemd dynamic*
allocation rather than a static GID, so a node rebuild can move it.

No catalog entry uses the field. Re-rendering `--all catalog -o apps` is a zero
diff, which is the proof the change is inert until opted into.

Three self-test cases in the existing check(...) style: the positive, the
negative (an SD without `hardware` gains no volume, no mount, no
securityContext — mirroring the podSecurityContext negative) and the merge case
([44] + 992 -> [44, 992], fsGroup preserved). Note that NO gate stage runs
`render.py --selftest`; they were run by hand and the gap is recorded in the
handoff.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-30 10:49:49 -05:00

82 lines
4.3 KiB
Markdown

# schemas — the ServiceDefinition contract
This directory holds the **ServiceDefinition** interface — the highest-traffic contract in the
platform (spec `agents/coordination-protocol.md` §2). Onboarding a service is adding/editing one
ServiceDefinition under [`../catalog/`](../catalog/); the platform compiles it into concrete
resources. Operators declare **what**; the platform provides the **how**.
**Source of truth:** the read-only spec
[`docs/13-service-catalog.md`](../../home-network-k3s/docs/13-service-catalog.md). This schema matches
docs/13 exactly. Do not diverge from it here — escalate spec problems, don't fork the contract.
## Files
| File | Purpose |
|---|---|
| `servicedefinition.schema.json` | Formal JSON Schema (draft 2020-12) for a ServiceDefinition. Enforces enums, required fields, patterns, and cross-field invariants. |
| `renderer-contract.md` | Authoritative field → rendered-resource mapping, with requirement IDs. The contract other workstreams consume. |
**Schema version:** 1.8.0 (in the schema `description`, which carries the full per-version changelog — this line had said `1.0.0` since 1.1.0 and is now synced). Treat any change as an interface change:
propose on the task board, update dependent renderers/tests in the same change, never silently break a
consumer (coordination-protocol §2).
## Required vs. defaulted fields
- **Required:** `name`, `source` (with `source.image` for `kind: container`, `source.vmTemplate` for
`kind: vm`).
- **Everything else is optional and carries a default** matching docs/13's "default posture"
(`kind: container`, `exposure: internal`, `egress: internet`, `auth.mode: forward-auth`,
`update.channel: stable`, `update.autoRollbackOnFailedHealth: true`, `observability.metrics: true`,
`dependencies: []` ⇒ DNS-only). A renderer MUST materialize defaults before rendering — see
`renderer-contract.md` §"Default posture".
## Cross-field rules the schema enforces
- `kind: vm` ⇒ `source.vmTemplate` required; otherwise `source.image` required.
- `auth.mode: oidc | forward-auth` ⇒ `auth.groups` present and non-empty (R-IDP-06 — access is "in an
authorized group," not merely authenticated).
- `ingressRoutes[].host` must match `<label>.home.keithjackson.dev`.
- `storage[].class ∈ {longhorn, bulk}`; `exposure ∈ {internal, public}`; `egress ∈ {internet, vpn}`;
`auth.mode ∈ {oidc, forward-auth, native, none}`.
- `hardware.gpu ∈ {intel-quicksync}` — a host **capability class**, never a device path. The enum
is the contract: the mechanism that satisfies it lives only in `catalog/render.py` (see
`renderer-contract.md` §`hardware`).
- `additionalProperties: false` everywhere — unknown keys are rejected (catches typos in review/CI).
## Validate examples against the schema — L1, no cluster (R-TEST-02)
Reproducible, single command, no cluster required. Canonical tool: **check-jsonschema** (YAML-native,
pins the schema file, pin the tool version for determinism):
```bash
# one-time (pin the version for reproducibility)
pip install check-jsonschema==0.38.0
# validate every catalog entry against the contract
check-jsonschema --schemafile schemas/servicedefinition.schema.json catalog/*.yaml
# -> "ok -- validation done"
```
Fallback with no extra tool (Python `jsonschema` + `pyyaml` only):
```bash
python3 - <<'PY'
import json, yaml, glob, sys
from jsonschema import Draft202012Validator
schema = json.load(open('schemas/servicedefinition.schema.json'))
Draft202012Validator.check_schema(schema)
v = Draft202012Validator(schema); bad = False
for f in sorted(glob.glob('catalog/*.yaml')):
errs = sorted(v.iter_errors(yaml.safe_load(open(f))), key=lambda e: list(e.path))
print(('PASS ' if not errs else 'FAIL ') + f)
for e in errs: print(' ', list(e.path), e.message); bad = True
sys.exit(1 if bad else 0)
PY
```
Both are pure manifest/schema checks (L0/L1 in spec `docs/14`, table row "L1 Rendering") — they run in
CI with no cluster and are wired into the `.gitea/` schema-validation gate by WS-TEST.
## See also
- `renderer-contract.md` — what each field renders into (Deployment/VM, PVC, CiliumNetworkPolicy,
egress steering, HTTPRoute/Certificate/DNS, ServiceMonitor/alerts, ImagePolicy, Keycloak/oauth2-proxy).
- Example ServiceDefinitions: [`../catalog/pihole.yaml`](../catalog/pihole.yaml),
[`../catalog/keycloak.yaml`](../catalog/keycloak.yaml).