Files
home-network-k3s-impl/platform/kyverno/servicedefinition/validate-servicedefinition.yaml
T
jakman85andClaude Opus 4.8 8c1368977e Phase 0 clarification follow-ups + reconcile CI integration seams
Adopt the spec's Phase-0 clarifications (spec now at pin a7fc19f) into the impl:
- schema v1.1: enforce exposure:public => auth.mode!=none (positive+negative tested).
- Kyverno: add longhorn=>size and longhorn=>NOT-reacquirable invariants (R-STATE-02/03).
- Ratify the homelab.keithjackson.dev/* label-key convention (now in spec docs/03).
- R-TEST-06 spec-immutability gate: ci/SPEC_PIN + ci/spec-immutability.sh + gate.sh
  stage + Makefile target + a Gitea job (checks spec repo clean & at pin).
- touched-requirements.sh: conformance tags are the authoritative source.

Reconcile 3 CI seams the CI worker flagged (gate was RED, now GREEN):
- create schemas/validate.sh (SCHEMA_VALIDATE_CMD entrypoint) — L0 schema gate runs.
- fix l1.sh/conformance.sh to run.sh's real interface (fixed _artifacts/ paths; no
  --junit/--coverage) and collect artifacts.
- exec bits on new scripts.

make -C ci gate => PASS (l0 skips locally w/o kyverno binary; green in CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-16 00:42:43 -05:00

80 lines
3.9 KiB
YAML

# Serves: R-GITOPS-02 (the ServiceDefinition is the onboarding contract; it must be
# shape-valid before it can be rendered).
# Spec: docs/13-service-catalog.md "Fields (canonical schema)" + "Default posture";
# docs/15 (auth); docs/03 (egress).
#
# ServiceDefinitions are ARBITRARY YAML (their own `kind:` field is container|vm, NOT
# a Kubernetes Kind), so they are validated with kyverno-json (per ADR-0011:
# "kyverno-json for arbitrary YAML/JSON like ServiceDefinitions"), not the resource
# CLI. This policy encodes the docs/13 invariants only; the full JSON Schema +
# renderer contract is WS-GITOPS T-0.3 and is complementary, not a substitute.
#
# NOTE (see servicedefinition/README.md): kyverno-json is pre-1.0. The INVARIANTS
# below are the normative content; the CI agent must confirm the assertion syntax
# against the pinned kyverno-json release.
apiVersion: json.kyverno.io/v1alpha1
kind: ValidatingPolicy
metadata:
name: validate-servicedefinition
annotations:
homelab.keithjackson.dev/requirements: R-GITOPS-02,R-STATE-02,R-STATE-03
spec:
rules:
- name: servicedefinition-shape
assert:
all:
- message: "`name` must be a DNS-1123 label"
check:
(regex_match('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$', name)): true
- message: "`kind` must be 'container' or 'vm'"
check:
(kind == 'container' || kind == 'vm'): true
- message: "`exposure` must be 'internal' or 'public'"
check:
(exposure == 'internal' || exposure == 'public'): true
- message: "`egress` must be 'internet' or 'vpn'"
check:
(egress == 'internet' || egress == 'vpn'): true
- message: "container kind requires `source.image`"
check:
(kind != 'container' || source.image != null): true
- message: "vm kind requires `source.vmTemplate`"
check:
(kind != 'vm' || source.vmTemplate != null): true
- message: "`auth.mode` must be one of oidc|forward-auth|native|none"
check:
(auth.mode == 'oidc' || auth.mode == 'forward-auth' || auth.mode == 'native' || auth.mode == 'none'): true
- message: "public exposure must not use auth.mode 'none' -- no unauthenticated public UI (docs/15, R-IDP-02)"
check:
(exposure != 'public' || auth.mode != 'none'): true
- name: servicedefinition-dependencies
assert:
all:
- message: "each dependency needs `to`, `port` (1-65535) and `protocol` tcp|udp"
check:
~.dependencies:
(to != null): true
(port >= `1` && port <= `65535`): true
(protocol == 'tcp' || protocol == 'udp'): true
- name: servicedefinition-storage
assert:
all:
- message: "each storage entry's `class` must be 'longhorn' or 'bulk'"
check:
~.storage:
(class == 'longhorn' || class == 'bulk'): true
# docs/13 v1.1: longhorn (replicated, irreplaceable) volumes MUST be sized (R-STATE-02).
# bulk may omit size (external mount, sized outside the cluster).
- message: "a `longhorn` volume requires an explicit `size` (docs/13 v1.1; R-STATE-02)"
check:
~.storage:
(class != 'longhorn' || size != null): true
- name: servicedefinition-backup
# docs/13 v1.1: irreplaceable (longhorn-tier) state MUST NOT be `reacquirable` — it must
# reference an off-cluster backup schedule (R-STATE-03). Only bulk-tier data may be reacquirable.
assert:
all:
- message: "backup.policy 'reacquirable' is not allowed when any volume is on the longhorn (irreplaceable) tier; reference an off-cluster schedule (docs/13 v1.1; R-STATE-03)"
check:
(backup.policy == 'reacquirable' && storage != null && contains(storage[].class, 'longhorn')): false