# 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