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 and the mount 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 exactly two things,
on the app container 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).
Mounting the device is sufficient. In particular NO
podSecurityContext.supplementalGroups is emitted, and adding one would be a
regression rather than a hardening. Measured in the running container on
prod-2, 2026-09-30:
PID 1 s6-svscan Uid: 0 Gid: 0 Groups: 0
pid 246 Plex Media Server Uid: 1000 Gid: 1000 Groups: 100 1000
The service is started with `exec s6-setuidgid plex ...`, and the child's group
list is exactly the /etc/group account-database list — root's gid 0, which
PID 1 holds, is gone. That is a setgroups() that REPLACES rather than appends,
so anything Kubernetes supplies is discarded before the app starts. What works
is the image's own /etc/cont-init.d/45-plex-hw-transcode-and-connected-tuner,
which stats each /dev/dri char device, groupadds a matching group inside the
container and usermods the app user into it, every start, nothing hardcoded.
An inert supplementalGroups would have read as the mechanism to whoever
debugged this next, so the absence is documented at the emission site and
asserted by --selftest. Corollary: the hook re-stats on every start, so host
GID drift is self-healing and no GID is pinned anywhere in this repo.
`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.
Also 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).
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.
Self-tests in the existing check(...) style: the positive (volume + mount, and
no securityContext / privileged / nodeSelector), the negative (an SD without
`hardware` gains no volume, no mount, no securityContext — mirroring the
podSecurityContext negative) and a pass-through case (an SD's own
podSecurityContext survives byte-for-byte). 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]>
82 lines
4.3 KiB
Markdown
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).
|