Files
jakman85andClaude Opus 5 0aa1a0a78a
gate / gate (pull_request) Successful in 1m49s
gate / conformance (pull_request) Has been skipped
feat(plex): one advertised hostname for local+remote, its own LE cert, and a LAN definition
Two symptoms after the publishing fix landed:

1. LAN clients were reported as Remote. Measured root cause:
   LanNetworksBandwidth was ABSENT, so Plex had no notion of a local
   network and classified everything WAN -- 468 requests logged
   "Allowed Network (WAN)", ZERO "(LAN)", including Seerr from inside the
   cluster. allowedNetworks only waives AUTH; it does not make a client
   local. Two settings, and only one has an env hook.
2. The advertised URL hard-coded both the VIP and the server's cert hash
   into a cluster-var -- two values nothing kept in sync.

Operator design: ONE hostname, plex-vip.${BASE_DOMAIN}, split-horizon --
PiHole answers it with ${PLEX_L4_VIP} on the LAN, Cloudflare answers it
publicly with the WAN address NATed to the same VIP.

- catalog/plex.yaml: ADVERTISE_IP -> https://plex-vip.${BASE_DOMAIN}:32400,
  plus a level-triggered postStart hook (pihole/qbittorrent idiom) that
  re-asserts LanNetworksBandwidth and the three customCertificate* prefs
  every start, converting PEM -> PKCS#12 so cert renewals need no extra part.
- apps/wave-0/plex-cert.yaml: cert-manager Certificate for exactly that
  name from letsencrypt-prod, so the advertised https URL is servable.
- schema 1.11.0: `optional` on secretKeyRef/configMapKeyRef. Load-bearing --
  the cert is issued INTO the plex namespace, which the rendered plex.yaml
  itself creates, so it lands in the same reconcile; hard refs would have
  held Plex in CreateContainerConfigError and, since apps-wave-0 is
  wait:true, stalled the whole app tier on a slow DNS01 challenge.
- catalog/pihole.yaml: the plex-vip split-horizon answer. Required --
  measured, the name resolved to the Gateway VIP where nothing listens
  on 32400 (http=000).
- PLEX_ADVERTISE_URL deleted; the hostname removes the cert-hash fragility.

Corrects my own earlier claim that a SAN match is REQUIRED: it is not
(httpsRequired=0, secureConnections defaults to Preferred, so clients can
fall back to cleartext). A matching cert keeps the remote leg ENCRYPTED.
Hence: obtain a real cert for the advertised name rather than pick the name
to suit Plex's built-in one.

UNVERIFIED, and stated plainly in the handoff: nothing here has run. Cert
issuance for this name is inferred from the wildcard's success, and whether
Plex accepts an OpenSSL-3 PKCS#12 bundle is untested -- the largest risk.

Touches clusters/** -> sensitive paths -> needs a non-author approval or the
operator's /approve-override; deliberately not self-posted (CLAUDE.md 1).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-10-04 03:37:57 -05:00
..

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/; 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. 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):

# 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):

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/keycloak.yaml.