Greenfield spec set for a GitOps-driven PaaS+IaaS home platform: vision & requirements (R-* IDs), architecture, 14 domain specs, 11 ADRs, roadmap (phases + workstreams), and AI-agent coordination (roles, protocol, definition-of-done, task board). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
12 KiB
14 — Testing & Conformance
Serves: R-TEST-01..06, and is the verification mechanism for every other requirement. Related: ADR-0010.
This spec changes what "done" means. Nothing is accepted on a manual demo, a screenshot, or "it reconciled." Every requirement is proven by an automated, reproducible, behavior-based test that a machine can run on demand and that is tagged with the requirement ID(s) it verifies.
Principles
- Behavior-based (R-TEST-01). Tests assert observable system behavior — Given a state, When an action, Then an outcome — not implementation internals. "A pod with no allow rules cannot reach Plex" is a behavior; "a CiliumNetworkPolicy object exists" is not (it may exist and still not enforce). Test the behavior.
- Reproducible (R-TEST-02). Deterministic, hermetic, version-pinned, single-command, and environment-agnostic. Any conforming cluster + a kubeconfig ⇒ the same pass/fail. No flakiness budget; eventual-consistency is handled with bounded polling, not sleeps-and-hope.
- Conformance gates delivery (R-TEST-03). The suite blocks merges (for touched requirements) and blocks phase exits (full phase suite). Green suite is the definition of progress.
- Traceable (R-TEST-04). Each test declares its
R-*IDs; a generated coverage report maps every requirement to its verifying test(s). Uncovered requirements are visible gaps. - Fault-injection for safety-critical behavior (R-TEST-05). Default-deny, VPN kill-switch, data-survival, DR restore, and auto-rollback are proven by breaking things and asserting fail-closed / no-data-loss — not by happy-path checks.
- Specs are the fixed target (R-TEST-06). Tests encode the spec's intent. Agents never weaken a test or a requirement to get green; a genuine inability to meet the spec is an escalation to the human, not a spec/test edit.
Reproducibility contract (what "reproducible" concretely requires)
- Single command runs the suite (or a tagged subset) locally and in CI.
- Hermetic fixtures: each test provisions its own namespace/workloads/probes and tears them down; it never depends on pre-existing mutable state or on another test's side effects.
- Deterministic: fixed inputs, pinned tool/image versions, bounded retries with explicit timeouts for eventual consistency; no reliance on wall-clock timing or ordering.
- Isolated & parallel-safe: unique namespaces/labels; tests can run concurrently without cross-talk.
- Machine-readable output: JUnit (or equivalent) plus a requirement-coverage report; every
result carries the
R-*tags it verifies. - Self-cleaning: on pass or fail, fixtures are removed so a re-run starts clean (leaked fixtures are themselves a test failure).
- Versioned with the code: tests live in the implementation repo next to what they verify and evolve only when the human-owned spec they encode changes.
Test layers
| Layer | Runs | Verifies | Example |
|---|---|---|---|
| L0 Static/policy | Pre-apply, in CI | Manifests/ServiceDefinitions obey invariants | Every namespace has a default-deny baseline; no plaintext secrets; schema-valid ServiceDefinition |
| L1 Rendering | CI, no cluster | ServiceDefinition → rendered resources match golden expectations | egress: vpn renders the kill-switched egress path |
| L2 Behavioral/integration | Against dev/test cluster | Real runtime behavior | Default-deny probe, dependency allow/deny, egress-IP, TLS validity, DNS, PVC rebuild |
| L3 Fault-injection | Against dev/test cluster | Fail-closed / no-data-loss under failure | VPN tunnel forced down ⇒ no leak; node killed ⇒ data survives; bad update ⇒ auto-rollback; DR restore |
| L4 Conformance suite | CI + on-demand | Aggregate, requirement-tagged gate | The union of the above, reported per R-* |
Behavior test catalog (normative — implementers author these as automated tests)
Each maps to requirement IDs; phrased as Given/When/Then intent.
- Default-deny (R-NET-01): Given a fresh workload with no allow rules, When it attempts any in-cluster connection except DNS, Then the connection fails; DNS succeeds.
- Explicit allow / deny-undeclared (R-NET-02): Given declared
dependencies, When the source reaches a declared dest port, Then it succeeds; When it reaches an undeclared dest, Then it fails. - Egress class (R-NET-05): Given
egress: internetvsegress: vpnworkloads, When each queries an external echo of its public IP, Then the internet one shows the home/WAN IP and the vpn one shows the VPN IP. - VPN kill-switch (R-NET-06, L3): Given a
vpnworkload with a healthy tunnel, When the tunnel is forced down, Then the workload has zero egress to the plain internet andinternet-class workloads are unaffected; When the tunnel recovers, connectivity returns. - Egress flip (R-NET-07): Given a running service, When its
egressclass is changed in git and reconciled, Then its observed egress path changes with no workload rewrite. - Data survival (R-STATE-01, L3): Given a stateful service with written data, When the workload is destroyed and re-applied from git, Then the data is intact.
- Node loss (R-STATE-02, L3): Given a 3-replica volume, When a node is removed, Then the volume stays available and the cluster reconverges.
- DR restore (R-STATE-03, L3): Given an off-cluster backup, When a namespace + volumes are restored into a clean state, Then the service returns with its data.
- Trusted TLS (R-TLS-01/02): Given any exposed endpoint (incl.
internal), When its cert is inspected, Then it is valid, non-self-signed, unexpired; renewal occurs before expiry with no manual step. - DNS/exposure (R-DNS-*): Given a service, When resolved, Then the record exists and
internal clients hit the internal Gateway;
internalis unreachable publicly,publicis reachable + TLS-valid. - Hands-off update (R-UPD-01/02, L3): Given an out-of-date service under a stable policy, When the scan interval elapses, Then it advances with no human/AI edit; Given a deliberately bad update, Then health-gating auto-rolls-back and an email alert fires; Given a pinned service, Then it does not advance.
- VM lifecycle (R-PLAT-03, L2/L3): Given a
kind: vmServiceDefinition, When create/start/ stop/destroy viakubectl/virtctl, Then it behaves; its disk survives rebuild; default-deny + egress class apply. - Alerting (R-OBS-02/04, L3): Given a monitored condition, When a fault is injected, Then an email alert is delivered and the dashboard reflects it.
- GitOps drift (R-GITOPS-03, L3): Given a reconciled resource, When it is mutated live, Then Flux reverts it to the git-declared state.
- Onboarding completeness (WS-APPS): Given only a merged ServiceDefinition, Then the service is fully wired (compute/storage/policy/egress/TLS/DNS/monitoring/update) with no manual step — asserted by the union of the relevant tests above against that service.
Gating rules (R-TEST-03)
- Per-change (merge gate): the conformance subset tagged with the requirement IDs the change touches MUST pass before merge.
- Per-phase (exit gate): the full conformance suite for the phase's requirements MUST pass
before the phase's exit criteria are considered met (
../roadmap/phases.md). - Regression: the growing suite runs continuously against dev/test; a previously-green requirement going red blocks further delivery until fixed.
- Coverage: a requirement with no passing test is not satisfied, regardless of implementation state.
Tooling (pinned — ADR-0011)
The approach (ADR-0010) is realized with two tools from the Kyverno ecosystem:
- Kyverno — L0 static/policy gate and in-cluster admission enforcer. In CI, the Kyverno CLI
(
kyverno apply/kyverno test, incl.kyverno-jsonfor ServiceDefinitions) validates manifests and rendered resources against platform invariants (default-deny baseline present, no plaintext secrets, required labels/egress-class, schema conformance). The same policies also run in-cluster as an admission controller, rejecting any non-conforming resource at reconcile time (defense in depth — a resource that somehow bypassed CI still cannot be admitted). Enforcement never replaces behavior tests: a policy admitting a manifest is not proof the runtime behaves (R-TEST-01), so L2/L3 Chainsaw tests remain mandatory. Policies are authored once and shared between the CLI gate and the admission controller. - Chainsaw — L1 rendering, L2 behavioral, and L3 fault-injection. Declarative steps
(apply/assert/error,
script/command,try/catch/finally), bounded polling with explicit timeouts, per-test namespace isolation, and automatic cleanup satisfy the reproducibility contract. Fault-injection (force the tunnel down, cordon/kill a node, inject a bad image, trigger a restore) is expressed as Chainsawscriptsteps, asserting fail-closed / no-data-loss via probe fixtures, withfinallyrestoring state.
Probes are ephemeral job pods; egress-IP checks use an external echo; tests carry their R-* IDs
(labels/annotations/naming) so the coverage report is generated automatically; both tools' output
feeds JUnit + the requirement-coverage report and the CI merge / phase-exit gates. Tool versions are
pinned for determinism.
Where each layer runs
- L0 (Kyverno CLI) and L1 (rendering assertions) run in CI without a cluster — pure manifest/ ServiceDefinition validation; fast merge-gate feedback.
- L2 behavioral and L3 fault-injection run as an in-cluster Chainsaw Job on the dev/test cluster.
CI triggers the Job (and waits for it); the Job — not CI — holds cluster access, so CI never
needs a broad kubeconfig. This keeps credentials in-cluster and least-privileged (R-SEC-02).
- The Job runs under a dedicated, scoped test ServiceAccount whose RBAC grants exactly what the behavior/fault tests need (create/delete test namespaces + fixtures + probes, apply/patch NetworkPolicies, scale the VPN sidecar/gateway, cordon/drain a node, inject a bad image tag, trigger a Velero restore). This is a privileged test identity — it MUST be scoped, named, and documented, MUST NOT be reused by workloads, and its own reach is itself an audit item.
- The Job writes JUnit + coverage artifacts to a known location (log stream and/or a results PVC/object) that CI collects to enforce the merge / phase-exit gates.
- Fixtures are hermetic and self-cleaning (
finallysteps); a leaked fixture fails the run.
Acceptance gates (for the testing capability itself)
- A single command runs the suite and emits requirement-tagged JUnit + coverage.
- Every
R-*requirement maps to at least one passing behavior test (coverage report proves it). - Safety-critical requirements each have a fault-injection test that fails closed / loses no data.
- Re-running the suite yields identical results (reproducibility) with no leaked fixtures.
Open questions
- Tool mix is resolved: Kyverno (L0) + Chainsaw (L1/L2/L3), ADR-0011.
- Harness location is resolved: L2/L3 run as an in-cluster Chainsaw Job triggered by CI; CI holds no cluster credentials.
- Kyverno-as-admission-enforcer is resolved: yes — the L0 policies also enforce at admission in-cluster (defense in depth), in addition to the CI gate.
- Delegated to WS-TEST at implementation time (not human decisions): the exact RBAC scope of the test ServiceAccount (author it least-privilege for precisely the fault-injection actions it performs, 07) and the results-artifact mechanism (log stream vs results PVC/object store). Both are implementer's discretion within the stated constraints.