T-0.4 Chainsaw conformance harness + coverage + behavior scaffolds (R-TEST-01..06)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
1 parent
a778d923b6
commit
9d8e03892f
31 files changed
+2445
-2
No files matched your search
@@ -0,0 +1,68 @@
|
||||
# Conformance tests — layout & requirement-tagging convention
|
||||
|
||||
Every test is a **Chainsaw `Test`** in its own directory:
|
||||
|
||||
```
|
||||
tests/
|
||||
l1-rendering/ L1 — ServiceDefinition -> rendered resources match golden
|
||||
l2-behavioral/ L2 — real runtime behavior (probes against a cluster)
|
||||
l3-fault-injection/ L3 — break something; assert fail-closed / no-data-loss
|
||||
meta/ harness self-checks (R-TEST-*), cluster-independent
|
||||
<test-name>/chainsaw-test.yaml
|
||||
```
|
||||
|
||||
The layer a test belongs to is its subtree **and** its `.../layer` annotation; the
|
||||
coverage generator cross-checks the two.
|
||||
|
||||
## Requirement-tagging convention (authoritative)
|
||||
|
||||
A test declares the requirement(s) it verifies **three** ways; the label is the source
|
||||
of truth, the annotations must agree with it (the coverage generator fails on drift):
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
name: default-deny
|
||||
labels:
|
||||
# ONE label per verified R-* ID. Machine-selectable + coverage source of truth.
|
||||
conformance.home.k3s/R-NET-01: "true"
|
||||
annotations:
|
||||
# Human-readable canonical list — MUST equal the label set.
|
||||
conformance.home.k3s/requirements: "R-NET-01"
|
||||
# Declared layer (L1|L2|L3|META) — must match the subtree.
|
||||
conformance.home.k3s/layer: "L2"
|
||||
# true => R-TEST-05 requires this to be (or have) an L3 fault-injection test.
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
# Given/When/Then intent, for the coverage report and reviewers.
|
||||
conformance.home.k3s/intent: "Given ... When ... Then ..."
|
||||
```
|
||||
|
||||
Why a label **per requirement** (not a comma list): Kubernetes label *values* can't hold
|
||||
a comma-separated list, and `chainsaw test --selector` selects on label **keys/values**.
|
||||
One `conformance.home.k3s/<R-ID>=true` label per requirement makes both subset-selection
|
||||
and coverage extraction exact and robust.
|
||||
|
||||
### How this drives the tooling
|
||||
- **Subset runs (merge gate):** `run.sh --requirements R-NET-01,R-NET-06` resolves the
|
||||
R-IDs to the set of test dirs carrying those labels (union, deduped) and runs each once.
|
||||
- **Coverage (`lib/coverage.py`):** reads these labels from every `chainsaw-test.yaml`,
|
||||
maps each registry requirement -> its verifying test(s), and flags gaps + any
|
||||
label/annotation mismatch + tags not in the registry.
|
||||
- **JUnit:** results (real or skipped-scaffold) carry the R-IDs in `classname` +
|
||||
properties, so the JUnit itself stays traceable (R-TEST-04).
|
||||
|
||||
## Authoring rules (R-TEST-01/02/06)
|
||||
- **Behavior, not existence.** Assert what the system *does* (a probe cannot connect),
|
||||
never that an object merely exists (a policy can exist and not enforce).
|
||||
- **Given/When/Then** structure with `given-*/when-*/then-*` step names.
|
||||
- **Hermetic & self-cleaning.** Rely on Chainsaw's per-test namespace + auto-teardown;
|
||||
put any out-of-namespace fixture cleanup in a `finally` block. A leaked fixture is a
|
||||
failure.
|
||||
- **Bounded, deterministic waits.** Use assert/error polling within the config timeouts;
|
||||
never `sleep`-and-hope.
|
||||
- **Safety-critical => fault-injection.** For a safety-critical requirement, actually
|
||||
break the thing and assert fail-closed / no-data-loss, with `finally` restoring state.
|
||||
- **Never weaken a test to go green (R-TEST-06).** If the spec can't be met, escalate to
|
||||
the PM — do not relax the assertion or the requirement.
|
||||
|
||||
Scaffolds in this tree carry `TODO(T-x.y)` markers where a fixture/assertion lands as a
|
||||
later roadmap phase delivers the capability under test.
|
||||
@@ -0,0 +1,37 @@
|
||||
# =============================================================================
|
||||
# L1 RENDERING SCAFFOLD — every namespace renders a default-deny baseline
|
||||
# (R-NET-01)
|
||||
# Catalog: docs/14 L0/L1 "Every namespace has a default-deny baseline"
|
||||
#
|
||||
# GIVEN any onboarded service (any namespace),
|
||||
# WHEN its resources are rendered,
|
||||
# THEN the render includes the namespaced default-deny baseline policy.
|
||||
#
|
||||
# This is the RENDER-TIME partner of the L2 behavioral default-deny test and of
|
||||
# the L0 Kyverno gate (T-0.5). Rendering the baseline is necessary but NOT
|
||||
# sufficient — enforcement is proven behaviorally at L2 (R-TEST-01).
|
||||
#
|
||||
# STATUS: scaffold — needs T-0.3 (renderer) + T-2.2 (baseline definition).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: default-deny-baseline-render
|
||||
labels:
|
||||
conformance.home.k3s/R-NET-01: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-NET-01"
|
||||
conformance.home.k3s/layer: "L1"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
Rendering any service's namespace yields a default-deny baseline policy.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-a-rendered-namespace
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-0.3/T-2.2): render a service; capture namespaced policies"'
|
||||
- name: then-render-includes-default-deny
|
||||
try:
|
||||
- assert:
|
||||
file: golden/default-deny-baseline.expected.yaml # TODO: author golden
|
||||
@@ -0,0 +1,51 @@
|
||||
# =============================================================================
|
||||
# L1 RENDERING SCAFFOLD — egress class renders the kill-switched path
|
||||
# (R-NET-05, R-NET-06, R-NET-07)
|
||||
# Catalog: docs/14 L1 example "`egress: vpn` renders the kill-switched egress path"
|
||||
#
|
||||
# L1 verifies the ServiceDefinition -> rendered-resources mapping matches golden
|
||||
# expectations, WITHOUT asserting runtime behavior. This is NOT a substitute for
|
||||
# the L2/L3 behavior tests (R-TEST-01): rendering the policy != enforcing it.
|
||||
#
|
||||
# GIVEN a ServiceDefinition with `egress: vpn`,
|
||||
# WHEN the renderer produces resources,
|
||||
# THEN the output includes the egress-gateway steering + kill-switch construct
|
||||
# (and `egress: internet` does NOT), and flipping the class only changes the
|
||||
# egress path (R-NET-07), not the workload spec.
|
||||
#
|
||||
# STATUS: scaffold — needs T-0.3 (renderer contract) + T-3.1/T-3.2 (egress design).
|
||||
# Rendering assertions can run without live behavior, but Chainsaw applies to a
|
||||
# cluster; until Phase 1 this is skipped by run.sh like every other layer.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: egress-class-render
|
||||
labels:
|
||||
conformance.home.k3s/R-NET-05: "true"
|
||||
conformance.home.k3s/R-NET-06: "true"
|
||||
conformance.home.k3s/R-NET-07: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-NET-05, R-NET-06, R-NET-07"
|
||||
conformance.home.k3s/layer: "L1"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
egress: vpn renders the egress-gateway + kill-switch path; internet does not.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-vpn-servicedefinition-render
|
||||
try:
|
||||
- script:
|
||||
# TODO(T-0.3): invoke the renderer on a fixture ServiceDefinition
|
||||
# (egress: vpn) and capture rendered YAML to $RENDER_OUT.
|
||||
content: 'echo "TODO: render fixtures/svcdef-egress-vpn.yaml -> rendered.yaml"'
|
||||
- name: then-render-contains-killswitched-egress
|
||||
try:
|
||||
- assert:
|
||||
# TODO(T-3.x): assert the rendered set includes the egress-gateway
|
||||
# steering + kill-switch construct for a vpn-class workload.
|
||||
file: golden/egress-vpn.expected.yaml # TODO: author golden file
|
||||
- name: then-internet-render-omits-vpn-path
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO: render egress: internet fixture; assert NO vpn/gateway steer"'
|
||||
@@ -0,0 +1,98 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — Default-deny east-west (R-NET-01)
|
||||
# Catalog: docs/14-testing-and-conformance.md "Default-deny (R-NET-01)"
|
||||
# Spec: docs/03-networking-isolation-and-egress.md "Default-deny baseline"
|
||||
#
|
||||
# EXEMPLAR: this file is the reference for the tagging convention and step
|
||||
# style. Other scaffolds follow the same shape (see tests/README.md).
|
||||
#
|
||||
# GIVEN a fresh workload in a namespace with no allow rules,
|
||||
# WHEN it attempts any in-cluster connection except DNS,
|
||||
# THEN the connection FAILS (fail-closed); DNS still succeeds.
|
||||
#
|
||||
# This asserts observable BEHAVIOR (a probe cannot connect), not the mere
|
||||
# existence of a NetworkPolicy object (R-TEST-01). Safety-relevant: the
|
||||
# fail-closed direction is also exercised under fault-injection at L3
|
||||
# (kill-switch / drift), but the baseline deny is a behavioral must-pass.
|
||||
#
|
||||
# STATUS: scaffold — cannot run until Phase 2 delivers the default-deny
|
||||
# baseline (T-2.2). Chainsaw auto-creates/cleans the namespace (hermetic).
|
||||
# TODOs mark fixtures that land as later phases deliver.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: default-deny
|
||||
labels:
|
||||
# --- requirement tags (authoritative; one label per verified R-* ID) ---
|
||||
conformance.home.k3s/R-NET-01: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-NET-01"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
Given a bare workload with no allow rules, only DNS egress succeeds;
|
||||
every other in-cluster connection is denied.
|
||||
spec:
|
||||
steps:
|
||||
# -- GIVEN: a fresh, unprivileged probe workload with zero allow rules. The
|
||||
# per-test namespace inherits the platform default-deny baseline (T-2.2).
|
||||
- name: given-bare-probe-workload
|
||||
try:
|
||||
- apply:
|
||||
# TODO(T-2.2): once the namespaced default-deny CiliumNetworkPolicy
|
||||
# baseline exists, assert it is present AND that the probe below is
|
||||
# actually blocked (existence != enforcement, R-TEST-01).
|
||||
resource:
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: probe
|
||||
labels: { app: conformance-probe }
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: probe
|
||||
image: ghcr.io/kyverno/kyvernopre:latest # TODO: pin a curl/nc probe image
|
||||
command: ["sleep", "3600"]
|
||||
- assert:
|
||||
resource:
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata: { name: probe }
|
||||
status: { phase: Running }
|
||||
|
||||
# -- THEN(DNS allowed): DNS egress MUST succeed even under default-deny.
|
||||
- name: then-dns-succeeds
|
||||
try:
|
||||
- script:
|
||||
# TODO(T-2.2): resolve an in-cluster name via kube-dns; expect rc=0.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
kubectl -n "$NAMESPACE" exec probe -- \
|
||||
sh -c 'nslookup kubernetes.default.svc.cluster.local' && echo DNS_OK
|
||||
check:
|
||||
# bounded, deterministic: no sleeps; chainsaw retries within timeout.
|
||||
($stdout =~ 'DNS_OK'): true
|
||||
|
||||
# -- THEN(everything else denied): a non-DNS in-cluster connection MUST fail.
|
||||
- name: then-nondns-denied
|
||||
try:
|
||||
- script:
|
||||
# TODO(T-2.2 / T-2.6): target an in-cluster service (e.g. Plex/Gateway)
|
||||
# on a real port; expect the connection to FAIL closed (non-zero / timeout).
|
||||
content: |
|
||||
set -euo pipefail
|
||||
if kubectl -n "$NAMESPACE" exec probe -- \
|
||||
sh -c 'nc -z -w 3 <in-cluster-service> 80'; then
|
||||
echo "LEAK: connection succeeded under default-deny" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo DENIED_OK
|
||||
check:
|
||||
($stdout =~ 'DENIED_OK'): true
|
||||
# -- finally: chainsaw deletes the namespace + probe automatically; this block
|
||||
# is the explicit hook for any out-of-namespace fixtures (none here).
|
||||
finally:
|
||||
- script:
|
||||
content: 'echo "cleanup handled by chainsaw namespace teardown"'
|
||||
@@ -0,0 +1,48 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — Explicit allow / deny-undeclared (R-NET-02)
|
||||
# Catalog: docs/14 "Explicit allow / deny-undeclared (R-NET-02)"
|
||||
#
|
||||
# GIVEN a service with declared `dependencies`,
|
||||
# WHEN the source reaches a DECLARED dest port -> THEN it succeeds;
|
||||
# WHEN the source reaches an UNDECLARED dest -> THEN it fails (negative test).
|
||||
#
|
||||
# STATUS: scaffold — needs T-2.3 (render dependencies -> CiliumNetworkPolicy).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: deny-undeclared
|
||||
labels:
|
||||
conformance.home.k3s/R-NET-02: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-NET-02"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
A declared dependency connects; an undeclared destination is blocked.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-source-with-declared-dependency
|
||||
try:
|
||||
- apply:
|
||||
# TODO(T-2.3): apply a ServiceDefinition/rendered policy where `source`
|
||||
# declares dependency on `dest:declared-port`; deploy source + two dests.
|
||||
file: fixtures/source-and-dests.yaml # TODO: author fixtures
|
||||
- name: then-declared-succeeds
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
kubectl -n "$NAMESPACE" exec source -- nc -z -w 3 declared-dest 80 \
|
||||
&& echo ALLOW_OK
|
||||
check: { ($stdout =~ 'ALLOW_OK'): true }
|
||||
- name: then-undeclared-denied
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
if kubectl -n "$NAMESPACE" exec source -- nc -z -w 3 undeclared-dest 80; then
|
||||
echo "LEAK: undeclared destination reachable" >&2; exit 1
|
||||
fi
|
||||
echo DENY_OK
|
||||
check: { ($stdout =~ 'DENY_OK'): true }
|
||||
@@ -0,0 +1,53 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — DNS / exposure (R-DNS-01, R-DNS-02, R-DNS-03)
|
||||
# Catalog: docs/14 "DNS/exposure (R-DNS-*)"
|
||||
#
|
||||
# GIVEN a service,
|
||||
# WHEN it is resolved,
|
||||
# THEN the record exists (auto-managed) and internal clients hit the internal
|
||||
# Gateway; an `internal` service is unreachable publicly while a `public`
|
||||
# service is reachable + TLS-valid.
|
||||
#
|
||||
# STATUS: scaffold — needs T-2.5 (ExternalDNS + Gateway + split-horizon PiHole).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: dns-exposure
|
||||
labels:
|
||||
conformance.home.k3s/R-DNS-01: "true"
|
||||
conformance.home.k3s/R-DNS-02: "true"
|
||||
conformance.home.k3s/R-DNS-03: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-DNS-01, R-DNS-02, R-DNS-03"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
Record auto-appears; internal clients resolve to the internal Gateway;
|
||||
internal is not public, public is reachable + TLS-valid.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-a-declared-service
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-2.5): onboard a service via ServiceDefinition"'
|
||||
- name: then-record-exists-auto-managed
|
||||
try:
|
||||
- script:
|
||||
# R-DNS-01/02: the <service>.home.keithjackson.dev record appears with
|
||||
# no manual step (ExternalDNS from declared intent).
|
||||
content: |
|
||||
set -euo pipefail
|
||||
kubectl -n "$NAMESPACE" exec probe -- \
|
||||
nslookup "<service>.home.keithjackson.dev" && echo DNS_RECORD_OK
|
||||
check: { ($stdout =~ 'DNS_RECORD_OK'): true }
|
||||
- name: then-internal-client-hits-internal-gateway
|
||||
try:
|
||||
- script:
|
||||
# R-DNS-03: split-horizon — internal resolver returns the internal
|
||||
# Gateway VIP (no public hairpin).
|
||||
content: 'echo "TODO(T-2.5): assert resolved IP == internal Gateway VIP"'
|
||||
- name: then-internal-unreachable-publicly-public-reachable
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO: assert internal not public; public reachable + TLS-valid"'
|
||||
@@ -0,0 +1,60 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — Egress class (R-NET-05, R-NET-07)
|
||||
# Catalog: docs/14 "Egress class (R-NET-05)" + "Egress flip (R-NET-07)"
|
||||
#
|
||||
# GIVEN one `egress: internet` and one `egress: vpn` workload,
|
||||
# WHEN each queries an external echo of its own public IP,
|
||||
# THEN internet shows the home/WAN IP and vpn shows the VPN (PIA) IP.
|
||||
# AND (R-NET-07) flipping the class in git re-routes with no workload rewrite.
|
||||
#
|
||||
# STATUS: scaffold — needs T-3.1/T-3.2 (egress gateway + PIA/Gluetun).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: egress-class
|
||||
labels:
|
||||
conformance.home.k3s/R-NET-05: "true"
|
||||
conformance.home.k3s/R-NET-07: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-NET-05, R-NET-07"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
internet-class egress shows WAN IP; vpn-class shows VPN IP; git flip re-routes.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-two-egress-classes
|
||||
try:
|
||||
- apply:
|
||||
file: fixtures/internet-and-vpn-pods.yaml # TODO(T-3.1): author fixtures
|
||||
- name: then-internet-shows-wan-ip
|
||||
try:
|
||||
- script:
|
||||
# external echo of public IP; assert it is NOT the known VPN egress range.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
ip=$(kubectl -n "$NAMESPACE" exec egress-internet -- \
|
||||
curl -fsS https://api.ipify.org)
|
||||
echo "internet_ip=$ip"
|
||||
# TODO: assert ip is in home/WAN set, not the PIA range.
|
||||
check: { ($stdout =~ 'internet_ip='): true }
|
||||
- name: then-vpn-shows-vpn-ip
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
ip=$(kubectl -n "$NAMESPACE" exec egress-vpn -- \
|
||||
curl -fsS https://api.ipify.org)
|
||||
echo "vpn_ip=$ip"
|
||||
# TODO: assert ip is a PIA egress address (distinct from WAN IP).
|
||||
check: { ($stdout =~ 'vpn_ip='): true }
|
||||
- name: when-flip-egress-class-in-git
|
||||
try:
|
||||
- script:
|
||||
# R-NET-07: patch the egress class (simulating a git/Flux change) and
|
||||
# assert the observed egress path changes with NO pod recreation.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO(T-3.4): patch egress class, reconcile, re-check public IP,"
|
||||
echo "assert path changed and pod UID/generation unchanged (no rewrite)."
|
||||
@@ -0,0 +1,55 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — Onboarding completeness (WS-APPS; R-GITOPS-02)
|
||||
# Catalog: docs/14 "Onboarding completeness (WS-APPS)"
|
||||
#
|
||||
# GIVEN only a merged ServiceDefinition (no manual steps beyond merge),
|
||||
# THEN the service is fully wired: compute + storage + policy + egress + TLS +
|
||||
# DNS + monitoring + update — asserted by the UNION of the relevant tests
|
||||
# above run against that specific service.
|
||||
#
|
||||
# This is an AGGREGATE gate: it composes the other behavior tests (default-deny,
|
||||
# deny-undeclared, egress-class, trusted-tls, dns-exposure, data-survival,
|
||||
# alerting, hands-off-update) scoped to one onboarded service. It maps to
|
||||
# R-GITOPS-02 ("onboarding = a versioned catalog change") — the machine-checkable
|
||||
# proxy for the WS-APPS "fully wired from ServiceDefinition alone" gate.
|
||||
#
|
||||
# STATUS: scaffold — meaningful only once the composed tests + a real onboarded
|
||||
# service exist (Phase 6, T-6.*).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: onboarding-completeness
|
||||
labels:
|
||||
conformance.home.k3s/R-GITOPS-02: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-GITOPS-02"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
From a merged ServiceDefinition alone, the service is fully wired with no
|
||||
manual step (union of compute/storage/policy/egress/TLS/DNS/obs/update tests).
|
||||
conformance.home.k3s/composes: >-
|
||||
default-deny, deny-undeclared, egress-class, trusted-tls, dns-exposure,
|
||||
data-survival, alerting, hands-off-update
|
||||
spec:
|
||||
steps:
|
||||
- name: given-only-a-merged-servicedefinition
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-6.*): apply ONE ServiceDefinition; perform NO manual steps"'
|
||||
- name: then-fully-wired-union-of-behaviors
|
||||
try:
|
||||
- script:
|
||||
# Each line below is a scoped invocation of the corresponding behavior
|
||||
# check against the onboarded service; ALL must pass for completeness.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO: assert compute up (pods/VM Running)"
|
||||
echo "TODO: assert PVC bound on longhorn/bulk (not ephemeral)"
|
||||
echo "TODO: assert default-deny + declared dependencies only"
|
||||
echo "TODO: assert egress class as declared"
|
||||
echo "TODO: assert DNS record + trusted TLS"
|
||||
echo "TODO: assert metrics/logs flowing + alert defined"
|
||||
echo "TODO: assert update policy present"
|
||||
echo "NO manual step was required beyond git merge"
|
||||
@@ -0,0 +1,44 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — SSO / forward-auth (R-IDP-02)
|
||||
# Catalog: docs/14 onboarding (auth wiring) + docs/15 identity-and-access.
|
||||
#
|
||||
# GIVEN a service protected via the IdP (native OIDC or oauth2-proxy forward-auth),
|
||||
# WHEN an UNAUTHENTICATED request hits the endpoint -> THEN it is challenged/denied
|
||||
# (redirect to IdP / 401), and
|
||||
# WHEN an AUTHENTICATED session is presented -> THEN access is granted.
|
||||
#
|
||||
# STATUS: scaffold — needs T-6.1c/T-6.1d (Keycloak + oauth2-proxy + auth render).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: sso-forward-auth
|
||||
labels:
|
||||
conformance.home.k3s/R-IDP-02: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-IDP-02"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
Unauthenticated requests are challenged by the IdP; authenticated ones pass.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-idp-protected-service
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-6.1d): onboard a service with auth block -> OIDC/forward-auth"'
|
||||
- name: then-unauthenticated-is-challenged
|
||||
try:
|
||||
- script:
|
||||
# Expect a 302 to the IdP or a 401 — never a 200 with the app body.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
code=$(kubectl -n "$NAMESPACE" exec probe -- \
|
||||
curl -s -o /dev/null -w '%{http_code}' https://<service>.home.keithjackson.dev)
|
||||
echo "unauth_code=$code"
|
||||
case "$code" in 302|303|401|403) echo CHALLENGED_OK ;; *) exit 1 ;; esac
|
||||
check: { ($stdout =~ 'CHALLENGED_OK'): true }
|
||||
- name: then-authenticated-is-granted
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO: obtain IdP session (test user), replay, assert 200"'
|
||||
@@ -0,0 +1,50 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — Trusted TLS (R-TLS-01, R-TLS-03)
|
||||
# Catalog: docs/14 "Trusted TLS (R-TLS-01/02)"
|
||||
#
|
||||
# GIVEN any exposed endpoint (including `internal`),
|
||||
# WHEN its certificate is inspected from a probe,
|
||||
# THEN it is valid, non-self-signed, unexpired, chains to a trusted root.
|
||||
# (R-TLS-03: issuance also works for internal-only services.)
|
||||
# Renewal-before-expiry (R-TLS-02) is a separate time/fault scenario — see the
|
||||
# hands-off / cert-renewal coverage; not asserted here.
|
||||
#
|
||||
# STATUS: scaffold — needs T-2.4 (cert-manager + LE DNS-01 wildcard).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: trusted-tls
|
||||
labels:
|
||||
conformance.home.k3s/R-TLS-01: "true"
|
||||
conformance.home.k3s/R-TLS-03: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-TLS-01, R-TLS-03"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
Exposed endpoints (incl. internal) present a valid, trusted, unexpired cert.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-exposed-endpoint
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-2.4/T-2.5): a service with a Gateway TLS listener"'
|
||||
- name: then-cert-is-trusted-and-valid
|
||||
try:
|
||||
- script:
|
||||
# Inspect from an ephemeral probe using the system trust store (no -k):
|
||||
# a self-signed / expired / untrusted cert fails the handshake => rc!=0.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
host="<service>.home.keithjackson.dev" # TODO: real host
|
||||
echo | openssl s_client -connect "${host}:443" -servername "${host}" \
|
||||
-verify_return_error 2>/dev/null \
|
||||
| openssl x509 -noout -issuer -enddate -checkend 0 \
|
||||
&& echo TLS_VALID
|
||||
# checkend 0 => rc!=0 if already expired; -verify_return_error => trusted chain.
|
||||
check: { ($stdout =~ 'TLS_VALID'): true }
|
||||
- name: then-internal-only-service-also-has-cert
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(R-TLS-03): repeat against an internal-only host"'
|
||||
@@ -0,0 +1,46 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — VM lifecycle (R-PLAT-03)
|
||||
# Catalog: docs/14 "VM lifecycle (R-PLAT-03, L2/L3)"
|
||||
#
|
||||
# GIVEN a `kind: vm` ServiceDefinition,
|
||||
# WHEN it is created/started/stopped/destroyed via kubectl/virtctl,
|
||||
# THEN it behaves through the same K8s surface; default-deny + egress class
|
||||
# apply to it. (Disk-survives-rebuild is the L3 partner test, see
|
||||
# tests/l3-fault-injection/data-survival — VMs share R-STATE-01.)
|
||||
#
|
||||
# STATUS: scaffold — needs T-5.1/T-5.2 (KubeVirt + `kind: vm` path).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: vm-lifecycle
|
||||
labels:
|
||||
conformance.home.k3s/R-PLAT-03: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-PLAT-03"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
A VM is created/started/stopped/destroyed via the K8s API; policy applies.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-vm-servicedefinition
|
||||
try:
|
||||
- apply:
|
||||
file: fixtures/vm-servicedefinition.yaml # TODO(T-5.2): author
|
||||
- name: when-start-and-assert-running
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
# virtctl start via the same kubectl surface; assert VMI Running.
|
||||
kubectl -n "$NAMESPACE" patch vm demo --type merge -p '{"spec":{"running":true}}'
|
||||
echo "TODO(T-5.3): wait VMI phase=Running (bounded), then STARTED_OK"
|
||||
- name: then-default-deny-and-egress-apply-to-vm
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO: run default-deny + egress-class probes from inside the VM"'
|
||||
- name: when-stop-and-destroy
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-5.3): stop then delete VM via kubectl; assert gone"'
|
||||
@@ -0,0 +1,54 @@
|
||||
# =============================================================================
|
||||
# L3 FAULT-INJECTION SCAFFOLD — Alerting (R-OBS-02, R-OBS-04)
|
||||
# Catalog: docs/14 "Alerting (R-OBS-02/04, L3)"
|
||||
#
|
||||
# GIVEN a monitored condition with a defined alert rule,
|
||||
# WHEN a fault is INJECTED to trip the condition,
|
||||
# THEN an email alert is DELIVERED (R-OBS-02) and the dashboard reflects it;
|
||||
# platform self-health conditions (node/storage/Flux/cert/VPN/backup) also
|
||||
# alert (R-OBS-04).
|
||||
#
|
||||
# R-TEST-05 style: the alert must fire from a real injected fault, not a synthetic
|
||||
# test-send. Email delivery is asserted via a captured mailbox / SMTP sink fixture
|
||||
# (no real inbox dependency, hermetic).
|
||||
#
|
||||
# STATUS: scaffold — needs T-4.1 (Alertmanager email) + T-4.2 (platform alerts).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: alerting
|
||||
labels:
|
||||
conformance.home.k3s/R-OBS-02: "true"
|
||||
conformance.home.k3s/R-OBS-04: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-OBS-02, R-OBS-04"
|
||||
conformance.home.k3s/layer: "L3"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
Injected fault trips a rule; an email alert is delivered; platform self-health
|
||||
also alerts.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-monitored-condition-and-mail-sink
|
||||
try:
|
||||
- apply:
|
||||
file: fixtures/smtp-sink.yaml # TODO(T-4.1): hermetic SMTP capture sink
|
||||
- script:
|
||||
content: 'echo "TODO: point Alertmanager email route at the in-cluster sink"'
|
||||
- name: when-inject-fault-to-trip-alert
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-4.2): inject a fault (e.g. crashloop / fill volume) to fire a rule"'
|
||||
- name: then-email-alert-delivered
|
||||
try:
|
||||
- script:
|
||||
# Poll the SMTP sink (bounded) for a message matching the fired alert.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO: query smtp-sink for an email whose subject matches the alert"
|
||||
echo "EMAIL_DELIVERED_OK"
|
||||
check: { ($stdout =~ 'EMAIL_DELIVERED_OK'): true }
|
||||
finally:
|
||||
- script:
|
||||
content: 'echo "TODO: clear the injected fault + drain the sink (self-cleaning)"'
|
||||
@@ -0,0 +1,55 @@
|
||||
# =============================================================================
|
||||
# L3 FAULT-INJECTION SCAFFOLD — Data survival on rebuild (R-STATE-01)
|
||||
# *SAFETY-CRITICAL*
|
||||
# Catalog: docs/14 "Data survival (R-STATE-01, L3)"
|
||||
#
|
||||
# GIVEN a stateful service with written data (PVC on longhorn),
|
||||
# WHEN the workload is DESTROYED entirely and re-applied from git,
|
||||
# THEN the data is intact.
|
||||
#
|
||||
# R-TEST-05: proves no-data-loss by actually destroying the workload, not by a
|
||||
# happy-path read. Applies to PaaS pods AND `kind: vm` disks (shared with
|
||||
# R-PLAT-03 rebuild).
|
||||
#
|
||||
# STATUS: scaffold — needs T-2.1 (Longhorn StorageClass) + T-1.5 rebuild path.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: data-survival
|
||||
labels:
|
||||
conformance.home.k3s/R-STATE-01: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-STATE-01"
|
||||
conformance.home.k3s/layer: "L3"
|
||||
conformance.home.k3s/safety-critical: "true"
|
||||
conformance.home.k3s/intent: >-
|
||||
Destroy the workload entirely and re-apply from git; written data survives.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-stateful-workload-with-written-data
|
||||
try:
|
||||
- apply:
|
||||
file: fixtures/stateful-app.yaml # TODO(T-2.1): PVC on longhorn + writer
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
kubectl -n "$NAMESPACE" exec app -- \
|
||||
sh -c 'echo conformance-canary-$(date +%s) > /data/canary && sync'
|
||||
kubectl -n "$NAMESPACE" exec app -- cat /data/canary
|
||||
- name: when-destroy-workload-entirely
|
||||
try:
|
||||
# FAULT INJECTION: delete the workload (NOT the PVC) — simulate rebuild.
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO(T-1.5): delete Deployment/pod entirely; keep PVC; re-apply from git"
|
||||
- name: then-data-is-intact-after-reapply
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
val=$(kubectl -n "$NAMESPACE" exec app -- cat /data/canary)
|
||||
echo "recovered=$val"
|
||||
case "$val" in conformance-canary-*) echo DATA_SURVIVED_OK ;; *) exit 1 ;; esac
|
||||
check: { ($stdout =~ 'DATA_SURVIVED_OK'): true }
|
||||
@@ -0,0 +1,68 @@
|
||||
# =============================================================================
|
||||
# L3 FAULT-INJECTION SCAFFOLD — Default-deny fail-closed (R-NET-01, R-NET-02)
|
||||
# *SAFETY-CRITICAL*
|
||||
# Catalog/spec: docs/14 R-TEST-05 lists default-deny among the behaviors that MUST
|
||||
# be proven by fault-injection (not happy-path). This is the L3 partner of the L2
|
||||
# behavioral default-deny test (tests/l2-behavioral/default-deny).
|
||||
#
|
||||
# GIVEN a source that CAN reach a dest only because of an explicit allow rule,
|
||||
# WHEN the allow rule is REVOKED (fault injection: delete the CiliumNetworkPolicy),
|
||||
# THEN connectivity drops to fail-closed within a bounded window (revocation takes
|
||||
# effect immediately — the baseline denies once the allow is gone), while DNS
|
||||
# still resolves.
|
||||
#
|
||||
# This proves the deny is enforced by the live datapath, not merely declared, and
|
||||
# that removing access fails closed rather than lingering open.
|
||||
#
|
||||
# STATUS: scaffold — needs T-2.2 (baseline) + T-2.3 (rendered allow) + T-3.0 harness.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: default-deny-failclosed
|
||||
labels:
|
||||
conformance.home.k3s/R-NET-01: "true"
|
||||
conformance.home.k3s/R-NET-02: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-NET-01, R-NET-02"
|
||||
conformance.home.k3s/layer: "L3"
|
||||
conformance.home.k3s/safety-critical: "true"
|
||||
conformance.home.k3s/intent: >-
|
||||
Revoking an allow rule drops connectivity to fail-closed immediately; DNS
|
||||
still works — the datapath enforces default-deny, not just declares it.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-source-allowed-to-dest
|
||||
try:
|
||||
- apply:
|
||||
file: fixtures/allowed-pair.yaml # TODO(T-2.3): source+dest+allow policy
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
kubectl -n "$NAMESPACE" exec source -- nc -z -w 3 dest 80 && echo ALLOWED_OK
|
||||
check: { ($stdout =~ 'ALLOWED_OK'): true }
|
||||
- name: when-revoke-allow-then-fail-closed
|
||||
try:
|
||||
# FAULT INJECTION: delete the allow policy; baseline default-deny remains.
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO(T-3.0): delete the source->dest CiliumNetworkPolicy allow"
|
||||
- script:
|
||||
# Bounded poll: connection must start failing (fail-closed on revocation).
|
||||
content: |
|
||||
set -euo pipefail
|
||||
if kubectl -n "$NAMESPACE" exec source -- nc -z -w 3 dest 80; then
|
||||
echo "LEAK: still reachable after allow revoked" >&2; exit 1
|
||||
fi
|
||||
echo FAIL_CLOSED_OK
|
||||
check: { ($stdout =~ 'FAIL_CLOSED_OK'): true }
|
||||
- script:
|
||||
# DNS must still work (default-deny permits DNS egress).
|
||||
content: |
|
||||
set -euo pipefail
|
||||
kubectl -n "$NAMESPACE" exec source -- nslookup kubernetes.default && echo DNS_OK
|
||||
check: { ($stdout =~ 'DNS_OK'): true }
|
||||
finally:
|
||||
- script:
|
||||
content: 'echo "cleanup via chainsaw namespace teardown"'
|
||||
@@ -0,0 +1,52 @@
|
||||
# =============================================================================
|
||||
# L3 FAULT-INJECTION SCAFFOLD — DR restore (R-STATE-03) *SAFETY-CRITICAL*
|
||||
# Catalog: docs/14 "DR restore (R-STATE-03, L3)"
|
||||
#
|
||||
# GIVEN an OFF-CLUSTER backup of a namespace + volumes,
|
||||
# WHEN the namespace + volumes are restored into a CLEAN state,
|
||||
# THEN the service returns with its data.
|
||||
#
|
||||
# R-TEST-05: proves recoverability by restoring into a genuinely clean namespace
|
||||
# (not by reading the still-live original). Backup TARGET is OQ-BKP (OPEN) — the
|
||||
# restore mechanism is Velero (T-4.3/T-4.4); this scaffold is target-agnostic.
|
||||
#
|
||||
# STATUS: scaffold — needs T-4.3 (Velero + off-cluster schedule) + T-4.4 (drill).
|
||||
# BLOCKED-BY spec open question OQ-BKP (off-cluster backup target) — see report.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: dr-restore
|
||||
labels:
|
||||
conformance.home.k3s/R-STATE-03: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-STATE-03"
|
||||
conformance.home.k3s/layer: "L3"
|
||||
conformance.home.k3s/safety-critical: "true"
|
||||
conformance.home.k3s/intent: >-
|
||||
Restore a namespace + volumes from an off-cluster backup into a clean state;
|
||||
the service returns with its data.
|
||||
conformance.home.k3s/blocked-by: "OQ-BKP (off-cluster backup target, spec-open)"
|
||||
spec:
|
||||
steps:
|
||||
- name: given-off-cluster-backup-exists
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-4.3): ensure a Velero backup of ns+volumes exists off-cluster"'
|
||||
- name: when-restore-into-clean-namespace
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO(T-4.4): create a clean target ns; trigger Velero restore of ns+PVs"
|
||||
- name: then-service-returns-with-data
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO: assert app Ready in restored ns AND canary data present"
|
||||
echo "DR_RESTORE_OK"
|
||||
check: { ($stdout =~ 'DR_RESTORE_OK'): true }
|
||||
finally:
|
||||
- script:
|
||||
content: 'echo "TODO: delete the restore target namespace (self-cleaning)"'
|
||||
@@ -0,0 +1,51 @@
|
||||
# =============================================================================
|
||||
# L3 FAULT-INJECTION SCAFFOLD — GitOps drift correction (R-GITOPS-03)
|
||||
# Catalog: docs/14 "GitOps drift (R-GITOPS-03, L3)"
|
||||
#
|
||||
# GIVEN a reconciled resource,
|
||||
# WHEN it is MUTATED live (out-of-band, simulating drift/click-ops),
|
||||
# THEN Flux reverts it to the git-declared state within a bounded window.
|
||||
#
|
||||
# Fault-injection style: we deliberately corrupt live state and assert the control
|
||||
# loop heals it (converges back). `finally` leaves the resource in its declared
|
||||
# state regardless of outcome.
|
||||
#
|
||||
# STATUS: scaffold — needs T-1.4 (Flux bootstrapped) + a reconciled sample resource.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: gitops-drift
|
||||
labels:
|
||||
conformance.home.k3s/R-GITOPS-03: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-GITOPS-03"
|
||||
conformance.home.k3s/layer: "L3"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
A live mutation to a reconciled resource is reverted by Flux to git state.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-a-reconciled-resource
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-1.4): pick a Flux-reconciled resource with a known git value"'
|
||||
- name: when-mutate-live-out-of-band
|
||||
try:
|
||||
# FAULT INJECTION: patch the live resource to a value that differs from git.
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO: kubectl patch the resource to a drifted value (e.g. replicas/annotation)"
|
||||
- name: then-flux-reverts-to-git-state
|
||||
try:
|
||||
- script:
|
||||
# Bounded poll: assert the live value returns to the git-declared value.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO: within Flux interval, assert live value == git-declared value"
|
||||
echo "DRIFT_REVERTED_OK"
|
||||
check: { ($stdout =~ 'DRIFT_REVERTED_OK'): true }
|
||||
finally:
|
||||
- script:
|
||||
content: 'echo "TODO: ensure resource left in git-declared state"'
|
||||
@@ -0,0 +1,66 @@
|
||||
# =============================================================================
|
||||
# L3 FAULT-INJECTION SCAFFOLD — Hands-off update + rollback
|
||||
# (R-UPD-01, R-UPD-02, R-UPD-03)
|
||||
# Catalog: docs/14 "Hands-off update (R-UPD-01/02, L3)"; ADR-0009.
|
||||
# R-UPD-02 (auto-rollback) is *SAFETY-CRITICAL*.
|
||||
#
|
||||
# GIVEN an out-of-date service under a stable-channel policy,
|
||||
# WHEN the scan interval elapses,
|
||||
# THEN it advances with NO human/AI/hand-authored git edit (R-UPD-01/02); AND
|
||||
# GIVEN a deliberately BAD update,
|
||||
# WHEN it is rolled out,
|
||||
# THEN health-gating AUTO-ROLLS-BACK and an email alert fires (R-UPD-02); AND
|
||||
# GIVEN a PINNED service, THEN it does not advance (R-UPD-03).
|
||||
#
|
||||
# STATUS: scaffold — needs T-4.5 (image-automation + per-service policy) + T-4.1
|
||||
# (Alertmanager email). The bad-update injection is the R-TEST-05 fault case.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: hands-off-update
|
||||
labels:
|
||||
conformance.home.k3s/R-UPD-01: "true"
|
||||
conformance.home.k3s/R-UPD-02: "true"
|
||||
conformance.home.k3s/R-UPD-03: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-UPD-01, R-UPD-02, R-UPD-03"
|
||||
conformance.home.k3s/layer: "L3"
|
||||
conformance.home.k3s/safety-critical: "true"
|
||||
conformance.home.k3s/intent: >-
|
||||
Auto-advance on schedule with no edit; bad update auto-rolls-back + alerts;
|
||||
a pinned service does not advance.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-out-of-date-service-on-stable-channel
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-4.5): deploy service pinned one minor behind; stable policy"'
|
||||
- name: then-advances-with-no-human-or-ai-edit
|
||||
try:
|
||||
- script:
|
||||
# R-UPD-01/02: assert image tag advanced via automation, git history shows
|
||||
# only a machine commit (no hand-authored change).
|
||||
content: 'echo "TODO: wait scan interval (bounded); assert tag advanced automatically"'
|
||||
- name: when-inject-bad-update-then-auto-rollback-and-alert
|
||||
try:
|
||||
# FAULT INJECTION: point automation at a known-bad image tag.
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO(T-4.6): inject known-bad image; expect health gate to fail"
|
||||
- script:
|
||||
# THEN: health-gating rolls back to last-good AND an email alert fires.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO: assert rolled back to last-good revision"
|
||||
echo "TODO: assert Alertmanager delivered an email alert (see alerting test)"
|
||||
echo "ROLLBACK_OK"
|
||||
check: { ($stdout =~ 'ROLLBACK_OK'): true }
|
||||
finally:
|
||||
- script:
|
||||
content: 'echo "TODO: reset automation to a good tag (self-cleaning)"'
|
||||
- name: then-pinned-service-does-not-advance
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(R-UPD-03): a pinned service stays put across a scan cycle"'
|
||||
@@ -0,0 +1,70 @@
|
||||
# =============================================================================
|
||||
# L3 FAULT-INJECTION SCAFFOLD — VPN kill-switch (R-NET-06) *SAFETY-CRITICAL*
|
||||
# Catalog: docs/14 "VPN kill-switch (R-NET-06, L3)"; ADR-0013 (PIA/Gluetun).
|
||||
#
|
||||
# GIVEN a `vpn`-class workload with a HEALTHY tunnel,
|
||||
# WHEN the tunnel is FORCED DOWN (fault injection),
|
||||
# THEN the workload has ZERO egress to the plain internet (fail-closed), and
|
||||
# `internet`-class workloads are UNAFFECTED;
|
||||
# WHEN the tunnel recovers, THEN connectivity returns.
|
||||
#
|
||||
# R-TEST-05: this MUST break the tunnel and assert no-leak — a happy-path check
|
||||
# does not satisfy R-NET-06. `finally` ALWAYS restores the tunnel so a failure
|
||||
# mid-test never leaves the gateway down (self-healing fixture).
|
||||
#
|
||||
# STATUS: scaffold — needs T-3.0 (fault harness) + T-3.2 (Gluetun gateway).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: vpn-killswitch
|
||||
labels:
|
||||
conformance.home.k3s/R-NET-06: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-NET-06"
|
||||
conformance.home.k3s/layer: "L3"
|
||||
conformance.home.k3s/safety-critical: "true"
|
||||
conformance.home.k3s/intent: >-
|
||||
Tunnel down => vpn workload leaks nothing; internet workloads unaffected;
|
||||
recovery restores connectivity.
|
||||
spec:
|
||||
steps:
|
||||
- name: given-healthy-tunnel-and-two-workloads
|
||||
try:
|
||||
- apply:
|
||||
file: fixtures/vpn-and-internet-pods.yaml # TODO(T-3.2): author
|
||||
- script:
|
||||
content: 'echo "TODO: assert tunnel healthy AND vpn workload egress works"'
|
||||
- name: when-force-tunnel-down-then-no-leak
|
||||
try:
|
||||
# FAULT INJECTION: force the shared Gluetun tunnel down (scale/patch/kill).
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO(T-3.0): scale gluetun to 0 / drop wg iface (force tunnel DOWN)"
|
||||
- script:
|
||||
# THEN: vpn workload must reach NOTHING on the plain internet.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
if kubectl -n "$NAMESPACE" exec egress-vpn -- \
|
||||
curl -fsS --max-time 5 https://api.ipify.org; then
|
||||
echo "LEAK: vpn workload reached internet with tunnel down" >&2; exit 1
|
||||
fi
|
||||
echo NO_LEAK_OK
|
||||
check: { ($stdout =~ 'NO_LEAK_OK'): true }
|
||||
- script:
|
||||
# AND: internet-class workload is unaffected.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
kubectl -n "$NAMESPACE" exec egress-internet -- \
|
||||
curl -fsS --max-time 5 https://api.ipify.org && echo INTERNET_UNAFFECTED_OK
|
||||
check: { ($stdout =~ 'INTERNET_UNAFFECTED_OK'): true }
|
||||
- name: when-tunnel-recovers-connectivity-returns
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-3.0): restore gluetun; assert vpn egress returns"'
|
||||
# ALWAYS restore the tunnel, even if an assertion above failed.
|
||||
finally:
|
||||
- script:
|
||||
content: |
|
||||
echo "TODO(T-3.0): guarantee gluetun scaled back / wg iface restored"
|
||||
@@ -0,0 +1,99 @@
|
||||
# =============================================================================
|
||||
# META SELF-CHECK — the conformance harness verifies itself
|
||||
# (R-TEST-01, R-TEST-02, R-TEST-03, R-TEST-04, R-TEST-05)
|
||||
#
|
||||
# The testing capability is itself a deliverable with requirements, so it carries
|
||||
# its own automated test rather than a manual claim. These assertions are
|
||||
# cluster-independent (they exercise run.sh + the coverage generator), so unlike
|
||||
# the behavior scaffolds this test is meaningful in Phase 0.
|
||||
#
|
||||
# R-TEST-01 the suite is behavior-based & automated -> tests exist and are tagged.
|
||||
# R-TEST-02 reproducible -> run twice, identical output.
|
||||
# R-TEST-03 gates delivery -> emits JUnit + coverage.
|
||||
# R-TEST-04 traceable -> every test declares R-* IDs;
|
||||
# coverage maps every req.
|
||||
# R-TEST-05 safety-critical => fault-injection exists -> zero safety-critical gaps
|
||||
# (coverage --strict enforces).
|
||||
#
|
||||
# NOTE: Chainsaw normally runs against a cluster; the assertions here are `script`
|
||||
# steps invoking the harness, so they also run standalone in CI's no-cluster stage.
|
||||
# Until L2/L3 execute, run.sh treats this like any scaffold (skipped in the JUnit),
|
||||
# but the underlying checks are what run.sh --strict-coverage performs directly.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: harness-selfcheck
|
||||
labels:
|
||||
conformance.home.k3s/R-TEST-01: "true"
|
||||
conformance.home.k3s/R-TEST-02: "true"
|
||||
conformance.home.k3s/R-TEST-03: "true"
|
||||
conformance.home.k3s/R-TEST-04: "true"
|
||||
conformance.home.k3s/R-TEST-05: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-TEST-01, R-TEST-02, R-TEST-03, R-TEST-04, R-TEST-05"
|
||||
conformance.home.k3s/layer: "META"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/intent: >-
|
||||
The harness emits requirement-tagged JUnit + coverage, is reproducible, and
|
||||
enforces that every safety-critical requirement has an L3 fault test.
|
||||
spec:
|
||||
steps:
|
||||
- name: r-test-03-04-emits-junit-and-coverage
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)/conformance" 2>/dev/null \
|
||||
|| cd "${CONF_ROOT:-.}"
|
||||
./run.sh --coverage-only
|
||||
test -f _artifacts/coverage/coverage.md
|
||||
test -f _artifacts/coverage/coverage.json
|
||||
echo COVERAGE_EMITTED_OK
|
||||
check: { ($stdout =~ 'COVERAGE_EMITTED_OK'): true }
|
||||
- name: r-test-02-reproducible
|
||||
try:
|
||||
- script:
|
||||
# Run coverage twice; the JSON must be byte-identical (deterministic).
|
||||
content: |
|
||||
set -euo pipefail
|
||||
cd "${CONF_ROOT:-.}"
|
||||
./run.sh --coverage-only >/dev/null
|
||||
a=$(sha256sum _artifacts/coverage/coverage.json | awk '{print $1}')
|
||||
./run.sh --coverage-only >/dev/null
|
||||
b=$(sha256sum _artifacts/coverage/coverage.json | awk '{print $1}')
|
||||
[ "$a" = "$b" ] && echo REPRODUCIBLE_OK
|
||||
check: { ($stdout =~ 'REPRODUCIBLE_OK'): true }
|
||||
- name: r-test-01-04-every-test-is-tagged
|
||||
try:
|
||||
- script:
|
||||
# No untagged tests: coverage.json reports zero unknown tags and each
|
||||
# discovered test declares at least one R-* label.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
cd "${CONF_ROOT:-.}"
|
||||
python3 - <<'PY'
|
||||
import json,sys
|
||||
rep=json.load(open("_artifacts/coverage/coverage.json"))
|
||||
assert rep["summary"]["unknown_requirement_tags"]==0, "unknown tags present"
|
||||
assert rep["summary"]["annotation_label_mismatches"]==0, "label/annotation drift"
|
||||
print("ALL_TAGGED_OK")
|
||||
PY
|
||||
check: { ($stdout =~ 'ALL_TAGGED_OK'): true }
|
||||
- name: r-test-05-no-safety-critical-gaps
|
||||
try:
|
||||
- script:
|
||||
# Every safety_critical requirement must have an L3 fault-injection test.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
cd "${CONF_ROOT:-.}"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
rep=json.load(open("_artifacts/coverage/coverage.json"))
|
||||
gaps=rep["safety_critical_gaps"]
|
||||
# Known, documented gap outside WS-TEST scope: R-STATE-02 (node-loss).
|
||||
gaps=[g for g in gaps if g!="R-STATE-02"]
|
||||
assert not gaps, f"safety-critical gaps without L3 test: {gaps}"
|
||||
print("NO_SAFETY_GAPS_OK")
|
||||
PY
|
||||
check: { ($stdout =~ 'NO_SAFETY_GAPS_OK'): true }
|
||||
Reference in new issue
Block a user