Wire L1 render conformance tests to the renderer (R-NET-01, R-NET-05/06/07)
Implements the two L1 rendering tests that were failing scaffolds: - egress-class-render (R-NET-05/06/07): renders vpn + internet fixtures, diffs each against a committed golden, asserts the vpn steering label is present on vpn and absent on internet (fail-closed), and that flipping the class changes ONLY the egress path (R-NET-07). This fixes the --requirements R-NET-05 merge gate. - default-deny-baseline-render (R-NET-01): asserts every render carries the namespaced default-deny baseline, shaped deny-all + both directions. Each test carries a pure-offline render-check.sh (renderer + golden diff, no cluster). run.sh runs L1 render checks offline and emits a REAL pass/fail JUnit (docs/14: L1 rendering assertions run in CI without a cluster) instead of skipping L1 with the cluster-gated layers; the chainsaw-test.yaml wrappers invoke the same render-check.sh so an in-cluster run executes the identical assertion. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
6b2c04ed5c
commit
4a017c590e
12 files changed
+851
-26
No files matched your search
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the L1 *rendering* conformance checks OFFLINE (no cluster) and emit JUnit.
|
||||||
|
|
||||||
|
docs/14: "L1 rendering assertions run in CI without a cluster." L1 verifies the
|
||||||
|
ServiceDefinition -> rendered-resources mapping against golden expectations; unlike L2/L3
|
||||||
|
it needs no live cluster. Each l1-rendering test dir carries a `render-check.sh` (renderer
|
||||||
|
+ golden diff + structural asserts). run.sh invokes this for `--layer l1` so those tests
|
||||||
|
report a REAL pass/fail instead of being skipped with the cluster-gated layers.
|
||||||
|
|
||||||
|
Pure standard library (matches the harness's no-pip-dependency posture); the render checks
|
||||||
|
themselves shell out to catalog/render.py (the renderer's PyYAML dependency lives there).
|
||||||
|
|
||||||
|
Usage: run_l1_offline.py --junit <out.xml> [--label-prefix P] <test-dir> [<test-dir> ...]
|
||||||
|
Exit 0 iff every selected render check passes.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
from xml.dom import minidom
|
||||||
|
|
||||||
|
LABEL_LINE_RE_TMPL = r"^\s*{prefix}/(R-[A-Z0-9]+-[0-9]+)\s*:\s*[\"']?true"
|
||||||
|
|
||||||
|
|
||||||
|
def collect_meta(test_yaml: Path, prefix: str):
|
||||||
|
text = test_yaml.read_text() if test_yaml.exists() else ""
|
||||||
|
name_m = re.search(r"^\s*name:\s*(\S+)", text, re.M)
|
||||||
|
name = name_m.group(1) if name_m else test_yaml.parent.name
|
||||||
|
rx = re.compile(LABEL_LINE_RE_TMPL.format(prefix=re.escape(prefix)), re.I)
|
||||||
|
reqs = sorted({m.group(1).upper() for m in (rx.match(l) for l in text.splitlines()) if m})
|
||||||
|
return name, reqs
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--junit", type=Path, required=True)
|
||||||
|
ap.add_argument("--label-prefix", default="conformance.home.k3s")
|
||||||
|
ap.add_argument("dirs", nargs="+", type=Path)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
cases = [] # (name, reqs, dir, rc, output)
|
||||||
|
for d in args.dirs:
|
||||||
|
check = d / "render-check.sh"
|
||||||
|
name, reqs = collect_meta(d / "chainsaw-test.yaml", args.label_prefix)
|
||||||
|
if not check.exists():
|
||||||
|
cases.append((name, reqs, d, 3, f"no render-check.sh in {d}"))
|
||||||
|
continue
|
||||||
|
proc = subprocess.run(
|
||||||
|
["bash", str(check)], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
out = (proc.stdout + proc.stderr).strip()
|
||||||
|
cases.append((name, reqs, d, proc.returncode, out))
|
||||||
|
|
||||||
|
failures = sum(1 for c in cases if c[3] not in (0, 3))
|
||||||
|
skipped = sum(1 for c in cases if c[3] == 3)
|
||||||
|
|
||||||
|
suite = ET.Element(
|
||||||
|
"testsuite",
|
||||||
|
{
|
||||||
|
"name": "home-k3s-conformance L1 (offline render)",
|
||||||
|
"tests": str(len(cases)),
|
||||||
|
"failures": str(failures),
|
||||||
|
"errors": "0",
|
||||||
|
"skipped": str(skipped),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for name, reqs, d, rc, out in cases:
|
||||||
|
tc = ET.SubElement(
|
||||||
|
suite,
|
||||||
|
"testcase",
|
||||||
|
{"classname": f"L1:{','.join(reqs) if reqs else 'UNTAGGED'}", "name": name},
|
||||||
|
)
|
||||||
|
props = ET.SubElement(tc, "properties")
|
||||||
|
ET.SubElement(props, "property", {"name": "requirements", "value": ",".join(reqs)})
|
||||||
|
ET.SubElement(props, "property", {"name": "layer", "value": "L1"})
|
||||||
|
ET.SubElement(props, "property", {"name": "path", "value": str(d)})
|
||||||
|
if rc == 3:
|
||||||
|
ET.SubElement(tc, "skipped", {"message": out or "no render-check.sh"})
|
||||||
|
elif rc != 0:
|
||||||
|
fail = ET.SubElement(tc, "failure", {"message": f"render-check exit {rc}"})
|
||||||
|
fail.text = out
|
||||||
|
status = "PASS" if rc == 0 else ("SKIP" if rc == 3 else "FAIL")
|
||||||
|
print(f"[l1-offline] {status:4s} {name} ({','.join(reqs) or 'untagged'})",
|
||||||
|
file=sys.stderr)
|
||||||
|
if rc not in (0, 3):
|
||||||
|
print(out, file=sys.stderr)
|
||||||
|
|
||||||
|
root = ET.Element(
|
||||||
|
"testsuites",
|
||||||
|
{
|
||||||
|
"name": "home-k3s-conformance",
|
||||||
|
"tests": str(len(cases)),
|
||||||
|
"failures": str(failures),
|
||||||
|
"errors": "0",
|
||||||
|
"skipped": str(skipped),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
root.append(suite)
|
||||||
|
args.junit.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
xml = minidom.parseString(ET.tostring(root, "utf-8")).toprettyxml(indent=" ")
|
||||||
|
args.junit.write_text(xml)
|
||||||
|
print(f"[l1-offline] wrote JUnit ({len(cases)} case(s), {failures} failure(s)) -> "
|
||||||
|
f"{args.junit}", file=sys.stderr)
|
||||||
|
return 1 if failures else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+44
-2
@@ -169,6 +169,28 @@ run_chainsaw() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- L1 rendering: cluster-INDEPENDENT render checks (docs/14) --------------------------
|
||||||
|
# L1 verifies ServiceDefinition -> rendered-resources against golden expectations; it needs
|
||||||
|
# no live cluster. Each l1-rendering test dir carries a `render-check.sh` (renderer + golden
|
||||||
|
# diff + structural asserts). We run those directly and emit a REAL pass/fail JUnit, instead
|
||||||
|
# of skipping L1 with the cluster-gated layers (T-0.3). Honors the same --layer/--requirements
|
||||||
|
# selection as the rest of the suite (select_dirs), filtered to the l1-rendering subtree.
|
||||||
|
l1_selected_dirs() {
|
||||||
|
select_dirs | grep '/l1-rendering/' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
run_l1_offline() {
|
||||||
|
local -a l1dirs
|
||||||
|
mapfile -t l1dirs < <(l1_selected_dirs)
|
||||||
|
if [ "${#l1dirs[@]}" -eq 0 ]; then
|
||||||
|
log "No L1 render tests match the selection (layer=${LAYER} reqs='${REQS}')."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "Running ${#l1dirs[@]} L1 render check(s) OFFLINE (no cluster; docs/14)."
|
||||||
|
"${PYTHON}" "${CONF_ROOT}/lib/run_l1_offline.py" \
|
||||||
|
--junit "${JUNIT_OUT}" --label-prefix "${CONF_LABEL_PREFIX}" "${l1dirs[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Main
|
# Main
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -187,9 +209,29 @@ case "${MODE}" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# run mode: need a cluster for L1/L2/L3. Without one (Phase 0), emit skipped JUnit.
|
# L1 rendering is cluster-independent (docs/14): run the selected L1 render checks offline
|
||||||
|
# and treat their result as authoritative REGARDLESS of cluster availability. A render-check
|
||||||
|
# failure is a real failure (nothing cluster-gated about it), so it always blocks.
|
||||||
|
L1_RC=0
|
||||||
|
run_l1_offline || L1_RC=$?
|
||||||
|
if [ "${LAYER}" = l1 ]; then
|
||||||
|
# L1-only run (the ci/l1.sh merge-gate path): the offline render result IS the result.
|
||||||
|
if [ "${L1_RC}" -eq 0 ]; then
|
||||||
|
log "Done (L1 render). JUnit: ${JUNIT_OUT} | Coverage: ${COVERAGE_OUT}/coverage.md"
|
||||||
|
else
|
||||||
|
log "L1 render checks FAILED (rc=${L1_RC}). JUnit: ${JUNIT_OUT}"
|
||||||
|
fi
|
||||||
|
exit "${L1_RC}"
|
||||||
|
fi
|
||||||
|
if [ "${L1_RC}" -ne 0 ]; then
|
||||||
|
log "L1 render checks FAILED (rc=${L1_RC}) — blocking (render failure is not cluster-gated)."
|
||||||
|
exit "${L1_RC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# run mode: need a cluster for L2/L3. Without one (Phase 0), emit skipped JUnit for the
|
||||||
|
# cluster-gated layers. (L1 already ran offline above and passed.)
|
||||||
if [ "${FORCE_NO_CLUSTER}" = 1 ] || ! conf_cluster_reachable; then
|
if [ "${FORCE_NO_CLUSTER}" = 1 ] || ! conf_cluster_reachable; then
|
||||||
log "No reachable cluster — emitting SKIPPED JUnit scaffold (Phase 0 behavior)."
|
log "No reachable cluster — emitting SKIPPED JUnit scaffold for L2/L3 (Phase 0 behavior)."
|
||||||
log "Also running structural lint so scaffolds stay valid."
|
log "Also running structural lint so scaffolds stay valid."
|
||||||
lint_tests || { log "lint failed"; exit 1; }
|
lint_tests || { log "lint failed"; exit 1; }
|
||||||
"${PYTHON}" "${CONF_ROOT}/lib/emit_skipped_junit.py" \
|
"${PYTHON}" "${CONF_ROOT}/lib/emit_skipped_junit.py" \
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
# the L0 Kyverno gate (T-0.5). Rendering the baseline is necessary but NOT
|
# the L0 Kyverno gate (T-0.5). Rendering the baseline is necessary but NOT
|
||||||
# sufficient — enforcement is proven behaviorally at L2 (R-TEST-01).
|
# sufficient — enforcement is proven behaviorally at L2 (R-TEST-01).
|
||||||
#
|
#
|
||||||
# STATUS: scaffold — needs T-0.3 (renderer) + T-2.2 (baseline definition).
|
# STATUS: IMPLEMENTED (T-0.3). render-check.sh renders a service and asserts the
|
||||||
|
# namespaced default-deny baseline is present + shaped (deny-all, both directions) —
|
||||||
|
# a pure offline check (renderer + golden diff, no cluster). conformance/run.sh --layer
|
||||||
|
# l1 and ci/l1.sh run it directly; this Chainsaw Test wraps the SAME script so an
|
||||||
|
# in-cluster run executes the identical assertion. Rendering != enforcement (proven L2).
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||||
kind: Test
|
kind: Test
|
||||||
@@ -27,11 +31,10 @@ metadata:
|
|||||||
Rendering any service's namespace yields a default-deny baseline policy.
|
Rendering any service's namespace yields a default-deny baseline policy.
|
||||||
spec:
|
spec:
|
||||||
steps:
|
steps:
|
||||||
- name: given-a-rendered-namespace
|
- name: render-and-assert-default-deny
|
||||||
|
description: >-
|
||||||
|
Render a service and assert the namespaced default-deny baseline is present and
|
||||||
|
shaped (deny-all ingress, both Ingress+Egress), matching the golden.
|
||||||
try:
|
try:
|
||||||
- script:
|
- script:
|
||||||
content: 'echo "TODO(T-0.3/T-2.2): render a service; capture namespaced policies"'
|
content: bash ./render-check.sh
|
||||||
- name: then-render-includes-default-deny
|
|
||||||
try:
|
|
||||||
- assert:
|
|
||||||
file: golden/default-deny-baseline.expected.yaml # TODO: author golden
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# L1 fixture: a minimal ServiceDefinition. Any onboarded service — regardless of its
|
||||||
|
# dependencies — must render the namespaced default-deny baseline (R-NET-01). This fixture
|
||||||
|
# declares NO dependencies, so its render must still carry the deny-all + DNS-only baseline.
|
||||||
|
# Validates against schemas/servicedefinition.schema.json.
|
||||||
|
name: baseline-demo
|
||||||
|
kind: container
|
||||||
|
source:
|
||||||
|
image: nginx:1.27
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
# Namespace (trust zone; default-deny + policies apply)
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: baseline-demo
|
||||||
|
labels:
|
||||||
|
homelab.keithjackson.dev/app-namespace: 'true'
|
||||||
|
---
|
||||||
|
# Default-deny baseline (R-NET-01): deny all, allow only DNS
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny
|
||||||
|
namespace: baseline-demo
|
||||||
|
labels:
|
||||||
|
homelab.keithjackson.dev/baseline: default-deny
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
ingress: []
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- protocol: TCP
|
||||||
|
port: 53
|
||||||
|
---
|
||||||
|
# Deployment (R-PLAT-02)
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: baseline-demo
|
||||||
|
namespace: baseline-demo
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: baseline-demo
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: baseline-demo
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: baseline-demo
|
||||||
|
homelab.keithjackson.dev/egress-class: internet
|
||||||
|
homelab.keithjackson.dev/auth-mode: none
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: baseline-demo
|
||||||
|
image: nginx:1.27
|
||||||
|
---
|
||||||
|
# ServiceMonitor (R-OBS-01)
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: baseline-demo
|
||||||
|
namespace: baseline-demo
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
app.kubernetes.io/name: baseline-demo
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: baseline-demo
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- baseline-demo
|
||||||
|
endpoints:
|
||||||
|
- port: metrics
|
||||||
|
interval: 30s
|
||||||
|
---
|
||||||
|
# Flux ImageRepository (R-UPD)
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: ImageRepository
|
||||||
|
metadata:
|
||||||
|
name: baseline-demo
|
||||||
|
namespace: baseline-demo
|
||||||
|
spec:
|
||||||
|
image: nginx
|
||||||
|
interval: 1h0m0s
|
||||||
|
---
|
||||||
|
# Flux ImagePolicy (R-UPD)
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: ImagePolicy
|
||||||
|
metadata:
|
||||||
|
name: baseline-demo
|
||||||
|
namespace: baseline-demo
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/update-channel: stable
|
||||||
|
homelab.keithjackson.dev/auto-rollback: 'true'
|
||||||
|
spec:
|
||||||
|
imageRepositoryRef:
|
||||||
|
name: baseline-demo
|
||||||
|
policy:
|
||||||
|
semver:
|
||||||
|
range: '>=0.0.0'
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# L1 render check (offline, no cluster) — default-deny-baseline-render
|
||||||
|
# Serves: R-NET-01 (every namespace renders a default-deny baseline). Run by
|
||||||
|
# conformance/run.sh --layer l1 and ci/l1.sh. Pure bash + catalog/render.py.
|
||||||
|
#
|
||||||
|
# GIVEN any onboarded service (this fixture declares NO dependencies),
|
||||||
|
# WHEN its resources are rendered,
|
||||||
|
# THEN (1) the render matches the committed golden byte-for-byte, and
|
||||||
|
# (2) it includes a NetworkPolicy labelled baseline=default-deny that denies
|
||||||
|
# all ingress (ingress: []) and covers BOTH Ingress and Egress — the
|
||||||
|
# render-time partner of the Kyverno L0 gate and the L2 behavioral test.
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT="$(cd "${DIR}/../../../.." && pwd)" # repo root
|
||||||
|
RENDER="${ROOT}/catalog/render.py"
|
||||||
|
PYTHON="${PYTHON:-python3}"
|
||||||
|
LBL="homelab.keithjackson.dev"
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${tmp}"' EXIT
|
||||||
|
|
||||||
|
fail() { echo "RENDER-CHECK FAIL (default-deny-baseline-render): $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
"${PYTHON}" "${RENDER}" "${DIR}/fixtures/svcdef.yaml" > "${tmp}/out.yaml"
|
||||||
|
|
||||||
|
# (1) golden match.
|
||||||
|
diff -u "${DIR}/golden/expected.yaml" "${tmp}/out.yaml" || fail "render drifted from golden"
|
||||||
|
|
||||||
|
# (2) the baseline is present and shaped as deny-all + both directions.
|
||||||
|
grep -q "${LBL}/baseline: default-deny" "${tmp}/out.yaml" \
|
||||||
|
|| fail "render is missing the default-deny baseline NetworkPolicy (R-NET-01)"
|
||||||
|
|
||||||
|
# Structural assertion via the renderer's own parse (deterministic, no cluster):
|
||||||
|
# find the default-deny NetworkPolicy and assert its shape.
|
||||||
|
"${PYTHON}" - "${tmp}/out.yaml" <<'PY' || fail "default-deny baseline is malformed (R-NET-01)"
|
||||||
|
import sys, yaml
|
||||||
|
docs = [d for d in yaml.safe_load_all(open(sys.argv[1])) if d]
|
||||||
|
np = [d for d in docs
|
||||||
|
if d.get("kind") == "NetworkPolicy"
|
||||||
|
and d.get("metadata", {}).get("labels", {}).get(
|
||||||
|
"homelab.keithjackson.dev/baseline") == "default-deny"]
|
||||||
|
assert len(np) == 1, f"expected exactly one default-deny baseline, got {len(np)}"
|
||||||
|
spec = np[0]["spec"]
|
||||||
|
assert spec.get("ingress") == [], "baseline must deny ALL ingress (ingress: [])"
|
||||||
|
assert set(spec.get("policyTypes", [])) == {"Ingress", "Egress"}, \
|
||||||
|
"baseline must cover BOTH Ingress and Egress"
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "render-check OK: default-deny-baseline-render (R-NET-01)"
|
||||||
@@ -13,9 +13,12 @@
|
|||||||
# (and `egress: internet` does NOT), and flipping the class only changes the
|
# (and `egress: internet` does NOT), and flipping the class only changes the
|
||||||
# egress path (R-NET-07), not the workload spec.
|
# 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).
|
# STATUS: IMPLEMENTED (T-0.3). The render assertion is a pure offline check
|
||||||
# Rendering assertions can run without live behavior, but Chainsaw applies to a
|
# (render-check.sh: renderer + golden diff, no cluster). conformance/run.sh --layer l1
|
||||||
# cluster; until Phase 1 this is skipped by run.sh like every other layer.
|
# and ci/l1.sh run render-check.sh directly WITHOUT a cluster (docs/14: "L1 rendering
|
||||||
|
# assertions run in CI without a cluster"). This Chainsaw Test wraps the SAME
|
||||||
|
# render-check.sh in a script step so an in-cluster Chainsaw run executes the identical
|
||||||
|
# assertion — rendering the policy, NOT enforcing it (enforcement is proven at L2/L3).
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||||
kind: Test
|
kind: Test
|
||||||
@@ -33,19 +36,11 @@ metadata:
|
|||||||
egress: vpn renders the egress-gateway + kill-switch path; internet does not.
|
egress: vpn renders the egress-gateway + kill-switch path; internet does not.
|
||||||
spec:
|
spec:
|
||||||
steps:
|
steps:
|
||||||
- name: given-vpn-servicedefinition-render
|
- name: render-and-assert-egress-class
|
||||||
|
description: >-
|
||||||
|
Render the vpn + internet fixtures and assert: each matches its golden; the vpn
|
||||||
|
render carries the egress-class=vpn steering label and internet does not; and the
|
||||||
|
two renders differ ONLY in the egress-class value (R-NET-07).
|
||||||
try:
|
try:
|
||||||
- script:
|
- script:
|
||||||
# TODO(T-0.3): invoke the renderer on a fixture ServiceDefinition
|
content: bash ./render-check.sh
|
||||||
# (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"'
|
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# L1 fixture: an internet-class ServiceDefinition. BYTE-IDENTICAL to svcdef-egress-vpn.yaml
|
||||||
|
# EXCEPT `egress: internet`. The two renders must differ ONLY in the egress path (R-NET-07):
|
||||||
|
# the internet render carries NO vpn steering label. Validates against the schema.
|
||||||
|
name: sonarr
|
||||||
|
kind: container
|
||||||
|
source:
|
||||||
|
image: linuxserver/sonarr
|
||||||
|
exposure: internal
|
||||||
|
egress: internet
|
||||||
|
storage:
|
||||||
|
- name: config
|
||||||
|
size: 2Gi
|
||||||
|
class: longhorn
|
||||||
|
ingressRoutes:
|
||||||
|
- host: sonarr.home.keithjackson.dev
|
||||||
|
port: 8989
|
||||||
|
auth:
|
||||||
|
mode: forward-auth
|
||||||
|
groups:
|
||||||
|
- media-admins
|
||||||
|
update:
|
||||||
|
channel: stable
|
||||||
|
cadence: "0 4 * * *"
|
||||||
|
autoRollbackOnFailedHealth: true
|
||||||
|
observability:
|
||||||
|
metrics: true
|
||||||
|
slis:
|
||||||
|
- up
|
||||||
|
backup:
|
||||||
|
policy: daily-offcluster
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# L1 fixture: a vpn-class ServiceDefinition. Identical to svcdef-egress-internet.yaml
|
||||||
|
# EXCEPT `egress: vpn`, so the render diff isolates the egress path (R-NET-05/07).
|
||||||
|
# sonarr is in the normative vpn-class set (docs/03/13). Validates against
|
||||||
|
# schemas/servicedefinition.schema.json.
|
||||||
|
name: sonarr
|
||||||
|
kind: container
|
||||||
|
source:
|
||||||
|
image: linuxserver/sonarr
|
||||||
|
exposure: internal
|
||||||
|
egress: vpn
|
||||||
|
storage:
|
||||||
|
- name: config
|
||||||
|
size: 2Gi
|
||||||
|
class: longhorn
|
||||||
|
ingressRoutes:
|
||||||
|
- host: sonarr.home.keithjackson.dev
|
||||||
|
port: 8989
|
||||||
|
auth:
|
||||||
|
mode: forward-auth
|
||||||
|
groups:
|
||||||
|
- media-admins
|
||||||
|
update:
|
||||||
|
channel: stable
|
||||||
|
cadence: "0 4 * * *"
|
||||||
|
autoRollbackOnFailedHealth: true
|
||||||
|
observability:
|
||||||
|
metrics: true
|
||||||
|
slis:
|
||||||
|
- up
|
||||||
|
backup:
|
||||||
|
policy: daily-offcluster
|
||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
# Namespace (trust zone; default-deny + policies apply)
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
labels:
|
||||||
|
homelab.keithjackson.dev/app-namespace: 'true'
|
||||||
|
gateway-access: shared
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/backup-policy: daily-offcluster
|
||||||
|
---
|
||||||
|
# Default-deny baseline (R-NET-01): deny all, allow only DNS
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
homelab.keithjackson.dev/baseline: default-deny
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
ingress: []
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- protocol: TCP
|
||||||
|
port: 53
|
||||||
|
---
|
||||||
|
# PVC sonarr-config (R-STATE)
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: sonarr-config
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 2Gi
|
||||||
|
---
|
||||||
|
# Deployment (R-PLAT-02)
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
homelab.keithjackson.dev/egress-class: internet
|
||||||
|
homelab.keithjackson.dev/auth-mode: forward-auth
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/auth-mode: forward-auth
|
||||||
|
homelab.keithjackson.dev/auth-groups: media-admins
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: sonarr
|
||||||
|
image: linuxserver/sonarr
|
||||||
|
ports:
|
||||||
|
- containerPort: 8989
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /data/config
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: sonarr-config
|
||||||
|
---
|
||||||
|
# Service
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
ports:
|
||||||
|
- name: port-8989
|
||||||
|
port: 8989
|
||||||
|
targetPort: 8989
|
||||||
|
---
|
||||||
|
# HTTPRoute sonarr-8989 (R-TLS/R-DNS)
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: HTTPRoute
|
||||||
|
metadata:
|
||||||
|
name: sonarr-8989
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/auth-mode: forward-auth
|
||||||
|
homelab.keithjackson.dev/auth-groups: media-admins
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: shared
|
||||||
|
namespace: gateway
|
||||||
|
sectionName: https
|
||||||
|
hostnames:
|
||||||
|
- sonarr.home.keithjackson.dev
|
||||||
|
rules:
|
||||||
|
- matches:
|
||||||
|
- path:
|
||||||
|
type: PathPrefix
|
||||||
|
value: /
|
||||||
|
backendRefs:
|
||||||
|
- name: sonarr
|
||||||
|
port: 8989
|
||||||
|
---
|
||||||
|
# ServiceMonitor (R-OBS-01)
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- sonarr
|
||||||
|
endpoints:
|
||||||
|
- port: metrics
|
||||||
|
interval: 30s
|
||||||
|
---
|
||||||
|
# PrometheusRule SLI alerts (R-OBS-01)
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: PrometheusRule
|
||||||
|
metadata:
|
||||||
|
name: sonarr-slis
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
groups:
|
||||||
|
- name: sonarr.slis
|
||||||
|
rules:
|
||||||
|
- alert: sonarr-down
|
||||||
|
expr: up{job="sonarr"} == 0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
service: sonarr
|
||||||
|
annotations:
|
||||||
|
summary: sonarr is down
|
||||||
|
description: 'sonarr has been unreachable for 5m (SLI: up).'
|
||||||
|
---
|
||||||
|
# Flux ImageRepository (R-UPD)
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: ImageRepository
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
spec:
|
||||||
|
image: linuxserver/sonarr
|
||||||
|
interval: 1h0m0s
|
||||||
|
---
|
||||||
|
# Flux ImagePolicy (R-UPD)
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: ImagePolicy
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/update-channel: stable
|
||||||
|
homelab.keithjackson.dev/auto-rollback: 'true'
|
||||||
|
spec:
|
||||||
|
imageRepositoryRef:
|
||||||
|
name: sonarr
|
||||||
|
policy:
|
||||||
|
semver:
|
||||||
|
range: '>=0.0.0'
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
# Namespace (trust zone; default-deny + policies apply)
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
labels:
|
||||||
|
homelab.keithjackson.dev/app-namespace: 'true'
|
||||||
|
gateway-access: shared
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/backup-policy: daily-offcluster
|
||||||
|
---
|
||||||
|
# Default-deny baseline (R-NET-01): deny all, allow only DNS
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
homelab.keithjackson.dev/baseline: default-deny
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
ingress: []
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- protocol: TCP
|
||||||
|
port: 53
|
||||||
|
---
|
||||||
|
# PVC sonarr-config (R-STATE)
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: sonarr-config
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 2Gi
|
||||||
|
---
|
||||||
|
# Deployment (R-PLAT-02)
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
homelab.keithjackson.dev/egress-class: vpn
|
||||||
|
homelab.keithjackson.dev/auth-mode: forward-auth
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/auth-mode: forward-auth
|
||||||
|
homelab.keithjackson.dev/auth-groups: media-admins
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: sonarr
|
||||||
|
image: linuxserver/sonarr
|
||||||
|
ports:
|
||||||
|
- containerPort: 8989
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /data/config
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: sonarr-config
|
||||||
|
---
|
||||||
|
# Service
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
ports:
|
||||||
|
- name: port-8989
|
||||||
|
port: 8989
|
||||||
|
targetPort: 8989
|
||||||
|
---
|
||||||
|
# HTTPRoute sonarr-8989 (R-TLS/R-DNS)
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: HTTPRoute
|
||||||
|
metadata:
|
||||||
|
name: sonarr-8989
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/auth-mode: forward-auth
|
||||||
|
homelab.keithjackson.dev/auth-groups: media-admins
|
||||||
|
spec:
|
||||||
|
parentRefs:
|
||||||
|
- name: shared
|
||||||
|
namespace: gateway
|
||||||
|
sectionName: https
|
||||||
|
hostnames:
|
||||||
|
- sonarr.home.keithjackson.dev
|
||||||
|
rules:
|
||||||
|
- matches:
|
||||||
|
- path:
|
||||||
|
type: PathPrefix
|
||||||
|
value: /
|
||||||
|
backendRefs:
|
||||||
|
- name: sonarr
|
||||||
|
port: 8989
|
||||||
|
---
|
||||||
|
# ServiceMonitor (R-OBS-01)
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- sonarr
|
||||||
|
endpoints:
|
||||||
|
- port: metrics
|
||||||
|
interval: 30s
|
||||||
|
---
|
||||||
|
# PrometheusRule SLI alerts (R-OBS-01)
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: PrometheusRule
|
||||||
|
metadata:
|
||||||
|
name: sonarr-slis
|
||||||
|
namespace: sonarr
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
app.kubernetes.io/name: sonarr
|
||||||
|
spec:
|
||||||
|
groups:
|
||||||
|
- name: sonarr.slis
|
||||||
|
rules:
|
||||||
|
- alert: sonarr-down
|
||||||
|
expr: up{job="sonarr"} == 0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
service: sonarr
|
||||||
|
annotations:
|
||||||
|
summary: sonarr is down
|
||||||
|
description: 'sonarr has been unreachable for 5m (SLI: up).'
|
||||||
|
---
|
||||||
|
# Flux ImageRepository (R-UPD)
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: ImageRepository
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
spec:
|
||||||
|
image: linuxserver/sonarr
|
||||||
|
interval: 1h0m0s
|
||||||
|
---
|
||||||
|
# Flux ImagePolicy (R-UPD)
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: ImagePolicy
|
||||||
|
metadata:
|
||||||
|
name: sonarr
|
||||||
|
namespace: sonarr
|
||||||
|
annotations:
|
||||||
|
homelab.keithjackson.dev/update-channel: stable
|
||||||
|
homelab.keithjackson.dev/auto-rollback: 'true'
|
||||||
|
spec:
|
||||||
|
imageRepositoryRef:
|
||||||
|
name: sonarr
|
||||||
|
policy:
|
||||||
|
semver:
|
||||||
|
range: '>=0.0.0'
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# L1 render check (offline, no cluster) — egress-class-render
|
||||||
|
# Serves: R-NET-05 (egress is a declared property), R-NET-06/07 (vpn steering /
|
||||||
|
# flip only changes the egress path). Run by conformance/run.sh --layer l1 and by
|
||||||
|
# ci/l1.sh. Pure bash + the renderer (catalog/render.py); no cluster, no chainsaw.
|
||||||
|
#
|
||||||
|
# GIVEN two ServiceDefinitions identical except `egress: vpn` vs `egress: internet`,
|
||||||
|
# WHEN the renderer produces resources,
|
||||||
|
# THEN (1) each matches its committed golden byte-for-byte,
|
||||||
|
# (2) the vpn render carries the egress-class=vpn steering label and the
|
||||||
|
# internet render does NOT (fail-closed: no silent vpn path on internet),
|
||||||
|
# (3) the two renders differ ONLY in the egress-class value (R-NET-07):
|
||||||
|
# flipping the class changes the egress path, not the workload spec.
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT="$(cd "${DIR}/../../../.." && pwd)" # repo root
|
||||||
|
RENDER="${ROOT}/catalog/render.py"
|
||||||
|
PYTHON="${PYTHON:-python3}"
|
||||||
|
LBL="homelab.keithjackson.dev"
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${tmp}"' EXIT
|
||||||
|
|
||||||
|
fail() { echo "RENDER-CHECK FAIL (egress-class-render): $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# (1) golden match for both classes.
|
||||||
|
"${PYTHON}" "${RENDER}" "${DIR}/fixtures/svcdef-egress-vpn.yaml" > "${tmp}/vpn.yaml"
|
||||||
|
"${PYTHON}" "${RENDER}" "${DIR}/fixtures/svcdef-egress-internet.yaml" > "${tmp}/internet.yaml"
|
||||||
|
diff -u "${DIR}/golden/egress-vpn.expected.yaml" "${tmp}/vpn.yaml" || fail "vpn render drifted from golden"
|
||||||
|
diff -u "${DIR}/golden/egress-internet.expected.yaml" "${tmp}/internet.yaml" || fail "internet render drifted from golden"
|
||||||
|
|
||||||
|
# (2) vpn steering label present on vpn, absent on internet (R-NET-05, fail-closed).
|
||||||
|
grep -q "${LBL}/egress-class: vpn" "${tmp}/vpn.yaml" \
|
||||||
|
|| fail "vpn render is missing the egress-class=vpn steering label"
|
||||||
|
if grep -q "${LBL}/egress-class: vpn" "${tmp}/internet.yaml"; then
|
||||||
|
fail "internet render must NOT carry the vpn steering label (fail-open leak)"
|
||||||
|
fi
|
||||||
|
grep -q "${LBL}/egress-class: internet" "${tmp}/internet.yaml" \
|
||||||
|
|| fail "internet render is missing the egress-class=internet label"
|
||||||
|
|
||||||
|
# (3) R-NET-07: the only difference is the egress-class value. Neutralize the class
|
||||||
|
# value in each render; the rest MUST be byte-identical.
|
||||||
|
sed 's#egress-class: vpn#egress-class: CLASS#' "${tmp}/vpn.yaml" > "${tmp}/vpn.norm"
|
||||||
|
sed 's#egress-class: internet#egress-class: CLASS#' "${tmp}/internet.yaml" > "${tmp}/internet.norm"
|
||||||
|
diff -u "${tmp}/vpn.norm" "${tmp}/internet.norm" \
|
||||||
|
|| fail "flipping egress class changed more than the egress path (R-NET-07 violated)"
|
||||||
|
|
||||||
|
echo "render-check OK: egress-class-render (R-NET-05/06/07)"
|
||||||
Reference in new issue
Block a user