Compare commits
5
Commits
19fc8bcb27
...
fa4deac877
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa4deac877 | ||
|
|
a317493bca | ||
|
|
c66a7e29e9 | ||
|
|
155df8ee3f | ||
|
|
9d0814ae61 |
No files matched your search
@@ -467,7 +467,14 @@ def git_sha(repo: Path) -> str:
|
||||
capture_output=True, text=True, check=True).stdout.strip()
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
return "unknown"
|
||||
dirty = subprocess.run(["git", "-C", str(repo), "diff", "--quiet", "HEAD", "--"],
|
||||
# EXCLUDE conformance/runs/. It is TRACKED, so `--record` writing ledger.jsonl dirtied the
|
||||
# tree and every LATER run in the same session was stamped `-dirty` — recording one test
|
||||
# poisoned the next, and check.sh refuses to honour a dirty proven row. That cost a second
|
||||
# live fault-injection run on prod to work around. The docstring above already states the
|
||||
# intent for the untracked runs/<id>/ half of this same artifact; the ledger is the tracked
|
||||
# half. Mirrors status.py's IMPL_EXCLUDE_PREFIXES, which lists this path for the same reason.
|
||||
dirty = subprocess.run(["git", "-C", str(repo), "diff", "--quiet", "HEAD", "--",
|
||||
".", ":(exclude)conformance/runs/"],
|
||||
capture_output=True).returncode != 0
|
||||
return f"{sha}-dirty" if dirty else sha
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" ?>
|
||||
<testsuites name="home-k3s-conformance" tests="1" failures="0" errors="0" skipped="0">
|
||||
<testsuite name="home-k3s-conformance L0/L1 (offline)" tests="1" failures="0" errors="0" skipped="0">
|
||||
<testcase classname="L1:R-NET-05,R-NET-06,R-NET-07" name="egress-class-render">
|
||||
<properties>
|
||||
<property name="requirements" value="R-NET-05,R-NET-06,R-NET-07"/>
|
||||
<property name="layer" value="L1"/>
|
||||
<property name="path" value="/work/.claude/conf-clone/conformance/tests/l1-rendering/egress-class-render"/>
|
||||
</properties>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -0,0 +1,7 @@
|
||||
[1m[run.sh][0m Coverage report first (always)
|
||||
[coverage] 49/51 requirements have a scaffolded test (96.1%); 2 gap(s), 0 safety-critical gap(s).
|
||||
[coverage] wrote /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.md and /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.json
|
||||
[1m[run.sh][0m Running 1 offline L0/L1 check(s) (no cluster; docs/14).
|
||||
[offline] L1 PASS egress-class-render (R-NET-05,R-NET-06,R-NET-07)
|
||||
[offline] wrote JUnit (1 case(s), 0 failure(s)) -> /work/.claude/conf-clone/conformance/_artifacts/report.junit.xml
|
||||
[1m[run.sh][0m Done (offline l1). JUnit: /work/.claude/conf-clone/conformance/_artifacts/report.junit.xml | Coverage: /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.md
|
||||
@@ -0,0 +1,4 @@
|
||||
<testsuites name="chainsaw-report" tests="1" failures="0" errors="0" time="0.000000"><testsuite name="/work/.claude/conf-clone/conformance/tests/l3-fault-injection/vpn-killswitch" tests="1" failures="0" errors="0" id="0" time="">
|
||||
<testcase name="vpn-killswitch" classname="" time="174.632779" />
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -0,0 +1,237 @@
|
||||
[1m[run.sh][0m Coverage report first (always)
|
||||
[coverage] 49/51 requirements have a scaffolded test (96.1%); 2 gap(s), 0 safety-critical gap(s).
|
||||
[coverage] wrote /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.md and /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.json
|
||||
[1m[run.sh][0m No L0/L1 offline checks match the selection (layer=l3 reqs='R-NET-06').
|
||||
[1m[run.sh][0m LIGHT pass: 1 test(s), parallel=2 (pinned v0.2.12)
|
||||
Version: 0.2.12
|
||||
Loading config (/work/.claude/conf-clone/conformance/config/chainsaw-config.yaml)...
|
||||
- Using test file: chainsaw-test
|
||||
- TestDirs [/work/.claude/conf-clone/conformance/tests/l3-fault-injection/vpn-killswitch]
|
||||
- SkipDelete false
|
||||
- FailFast false
|
||||
- ReportFormat 'JUNIT-TEST'
|
||||
- ReportName 'chainsaw-report-light'
|
||||
- ReportPath '/work/.claude/conf-clone/conformance/_artifacts'
|
||||
- Namespace ''
|
||||
- FullName false
|
||||
- IncludeTestRegex ''
|
||||
- ExcludeTestRegex ''
|
||||
- ApplyTimeout 1m30s
|
||||
- AssertTimeout 3m0s
|
||||
- CleanupTimeout 2m0s
|
||||
- DeleteTimeout 2m0s
|
||||
- ErrorTimeout 1m0s
|
||||
- ExecTimeout 3m0s
|
||||
- DeletionPropagationPolicy Background
|
||||
- Parallel 2
|
||||
- Template true
|
||||
- NoCluster false
|
||||
- PauseOnFailure false
|
||||
Loading tests...
|
||||
- vpn-killswitch (/work/.claude/conf-clone/conformance/tests/l3-fault-injection/vpn-killswitch)
|
||||
Loading values...
|
||||
Running tests...
|
||||
=== RUN chainsaw
|
||||
=== PAUSE chainsaw
|
||||
=== CONT chainsaw
|
||||
=== RUN chainsaw/vpn-killswitch
|
||||
=== PAUSE chainsaw/vpn-killswitch
|
||||
=== CONT chainsaw/vpn-killswitch
|
||||
l.go:52: | 01:44:38 | [32;1mvpn-killswitch[0;22m | [32;1m@chainsaw [0;22m | [32;1mCREATE[0;22m | [32;1mOK[0;22m | v1/Namespace @ chainsaw-credible-bird
|
||||
l.go:52: | 01:44:38 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mTRY[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:44:38 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
kubectl label namespace "$NAMESPACE" \
|
||||
pod-security.kubernetes.io/enforce=privileged \
|
||||
pod-security.kubernetes.io/warn=privileged \
|
||||
pod-security.kubernetes.io/audit=privileged \
|
||||
--overwrite
|
||||
echo "NS_PRIVILEGED_OK"
|
||||
l.go:52: | 01:44:39 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
namespace/chainsaw-credible-bird labeled
|
||||
NS_PRIVILEGED_OK
|
||||
l.go:52: | 01:44:39 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:44:39 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mAPPLY[0;22m | [36;1mRUN[0;22m | v1/Pod @ chainsaw-credible-bird/egress-internet
|
||||
l.go:52: | 01:44:39 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mCREATE[0;22m | [32;1mOK[0;22m | v1/Pod @ chainsaw-credible-bird/egress-internet
|
||||
l.go:52: | 01:44:39 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mAPPLY[0;22m | [32;1mDONE[0;22m | v1/Pod @ chainsaw-credible-bird/egress-internet
|
||||
l.go:52: | 01:44:39 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mAPPLY[0;22m | [36;1mRUN[0;22m | v1/Pod @ chainsaw-credible-bird/egress-vpn
|
||||
l.go:52: | 01:44:39 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mCREATE[0;22m | [32;1mOK[0;22m | v1/Pod @ chainsaw-credible-bird/egress-vpn
|
||||
l.go:52: | 01:44:39 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mAPPLY[0;22m | [32;1mDONE[0;22m | v1/Pod @ chainsaw-credible-bird/egress-vpn
|
||||
l.go:52: | 01:44:39 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mASSERT[0;22m | [36;1mRUN[0;22m | v1/Pod @ chainsaw-credible-bird/egress-internet
|
||||
l.go:52: | 01:44:41 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mASSERT[0;22m | [32;1mDONE[0;22m | v1/Pod @ chainsaw-credible-bird/egress-internet
|
||||
l.go:52: | 01:44:41 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mASSERT[0;22m | [36;1mRUN[0;22m | v1/Pod @ chainsaw-credible-bird/egress-vpn
|
||||
l.go:52: | 01:44:41 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mASSERT[0;22m | [32;1mDONE[0;22m | v1/Pod @ chainsaw-credible-bird/egress-vpn
|
||||
l.go:52: | 01:44:41 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
ns="$NAMESPACE"
|
||||
get_ip() {
|
||||
kubectl -n "$ns" exec "$1" -c curl -- \
|
||||
curl -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true
|
||||
}
|
||||
wan="" vpn=""
|
||||
for i in $(seq 1 12); do
|
||||
[ -n "$wan" ] || wan="$(get_ip egress-internet)"
|
||||
[ -n "$vpn" ] || vpn="$(get_ip egress-vpn)"
|
||||
if [ -n "$wan" ] && [ -n "$vpn" ] && [ "$wan" != "$vpn" ]; then break; fi
|
||||
[ "$wan" = "$vpn" ] && vpn="" # not steered yet; re-poll
|
||||
sleep 5
|
||||
done
|
||||
echo "wan=$wan vpn=$vpn"
|
||||
[ -n "$wan" ] || { echo "FAIL: no WAN IP from internet-class pod" >&2; exit 1; }
|
||||
[ -n "$vpn" ] || { echo "FAIL: vpn-class egress not working at baseline" >&2; exit 1; }
|
||||
[ "$wan" != "$vpn" ] || { echo "FAIL: vpn-class == WAN at baseline (not tunneled)" >&2; exit 1; }
|
||||
echo "TUNNEL_HEALTHY_OK"
|
||||
l.go:52: | 01:44:43 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
wan=136.51.115.252 vpn=158.173.241.11
|
||||
TUNNEL_HEALTHY_OK
|
||||
l.go:52: | 01:44:43 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:44:43 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mTRY[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:44:43 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mTRY[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:44:43 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mAPPLY[0;22m | [36;1mRUN[0;22m | batch/v1/Job @ chainsaw-credible-bird/tunnel-stop
|
||||
l.go:52: | 01:44:43 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mCREATE[0;22m | [32;1mOK[0;22m | batch/v1/Job @ chainsaw-credible-bird/tunnel-stop
|
||||
l.go:52: | 01:44:43 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mAPPLY[0;22m | [32;1mDONE[0;22m | batch/v1/Job @ chainsaw-credible-bird/tunnel-stop
|
||||
l.go:52: | 01:44:43 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mASSERT[0;22m | [36;1mRUN[0;22m | batch/v1/Job @ chainsaw-credible-bird/tunnel-stop
|
||||
l.go:52: | 01:44:48 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mASSERT[0;22m | [32;1mDONE[0;22m | batch/v1/Job @ chainsaw-credible-bird/tunnel-stop
|
||||
l.go:52: | 01:44:48 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -u
|
||||
ns="$NAMESPACE"
|
||||
wan="$(kubectl -n "$ns" exec egress-internet -c curl -- \
|
||||
curl -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true)"
|
||||
for i in $(seq 1 8); do
|
||||
out="$(kubectl -n "$ns" exec egress-vpn -c curl -- \
|
||||
curl -fsS --max-time 8 https://api.ipify.org 2>/dev/null || true)"
|
||||
if [ -n "$out" ]; then
|
||||
if [ -n "$wan" ] && [ "$out" = "$wan" ]; then
|
||||
echo "LEAK: vpn-class reached the WAN IP ($out) with the tunnel DOWN" >&2; exit 1
|
||||
fi
|
||||
echo "LEAK: vpn-class reached the internet ($out) with the tunnel DOWN" >&2; exit 1
|
||||
fi
|
||||
echo "attempt $i: vpn-class blackholed (no response), good"
|
||||
sleep 3
|
||||
done
|
||||
echo "NO_LEAK_OK"
|
||||
l.go:52: | 01:46:18 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
attempt 1: vpn-class blackholed (no response), good
|
||||
attempt 2: vpn-class blackholed (no response), good
|
||||
attempt 3: vpn-class blackholed (no response), good
|
||||
attempt 4: vpn-class blackholed (no response), good
|
||||
attempt 5: vpn-class blackholed (no response), good
|
||||
attempt 6: vpn-class blackholed (no response), good
|
||||
attempt 7: vpn-class blackholed (no response), good
|
||||
attempt 8: vpn-class blackholed (no response), good
|
||||
NO_LEAK_OK
|
||||
l.go:52: | 01:46:18 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:46:18 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
ns="$NAMESPACE"
|
||||
out="$(kubectl -n "$ns" exec egress-internet -c curl -- \
|
||||
curl -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true)"
|
||||
echo "internet_ip=$out"
|
||||
[ -n "$out" ] || { echo "FAIL: internet-class egress broke while tunnel down (must be unaffected)" >&2; exit 1; }
|
||||
echo "INTERNET_UNAFFECTED_OK"
|
||||
l.go:52: | 01:46:18 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
internet_ip=136.51.115.252
|
||||
INTERNET_UNAFFECTED_OK
|
||||
l.go:52: | 01:46:18 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:46:18 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mTRY[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:46:18 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mFINALLY[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:46:18 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
ns="$NAMESPACE"
|
||||
# Distinct Job name (tunnel-restart) so it never collides with the
|
||||
# tunnel-stop Job. Left-aligned manifest so the block scalar yields
|
||||
# valid YAML at column 0 at runtime.
|
||||
cat <<'JOB' | kubectl -n "$ns" apply -f -
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: tunnel-restart
|
||||
labels: { app: vpn-killswitch-fault }
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 90
|
||||
ttlSecondsAfterFinished: 600
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: vpn-killswitch-fault }
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
hostPID: true
|
||||
nodeSelector:
|
||||
homelab.keithjackson.dev/egress-gateway: "true"
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
containers:
|
||||
- name: nsenter
|
||||
image: busybox:1.37.0
|
||||
securityContext:
|
||||
privileged: true
|
||||
command: ["nsenter","-t","1","-m","-u","-i","-n","-p","--","systemctl","start","vpn-egress-tunnel"]
|
||||
JOB
|
||||
# Block teardown until the restart actually completed on the host — a
|
||||
# leaked-down tunnel is unacceptable. `wait` exits non-zero on failure
|
||||
# or timeout, which (set -e) fails the step and surfaces the problem.
|
||||
kubectl -n "$ns" wait --for=condition=complete --timeout=110s job/tunnel-restart
|
||||
echo "TUNNEL_SELF_HEAL_OK"
|
||||
l.go:52: | 01:46:21 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
job.batch/tunnel-restart created
|
||||
job.batch/tunnel-restart condition met
|
||||
TUNNEL_SELF_HEAL_OK
|
||||
l.go:52: | 01:46:21 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:46:21 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mFINALLY[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:46:21 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [36;1mTRY[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:46:21 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
ns="$NAMESPACE"
|
||||
wan="$(kubectl -n "$ns" exec egress-internet -c curl -- \
|
||||
curl -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true)"
|
||||
vpn=""
|
||||
for i in $(seq 1 24); do
|
||||
vpn="$(kubectl -n "$ns" exec egress-vpn -c curl -- \
|
||||
curl -fsS --max-time 8 https://api.ipify.org 2>/dev/null || true)"
|
||||
if [ -n "$vpn" ] && [ "$vpn" != "$wan" ]; then break; fi
|
||||
vpn=""
|
||||
sleep 5
|
||||
done
|
||||
echo "recovered_vpn=$vpn wan=$wan"
|
||||
[ -n "$vpn" ] || { echo "FAIL: vpn-class egress did not recover after tunnel restart" >&2; exit 1; }
|
||||
[ "$vpn" != "$wan" ] || { echo "FAIL: vpn-class egress recovered onto the WAN (not tunneled)" >&2; exit 1; }
|
||||
echo "TUNNEL_RECOVERED_OK"
|
||||
l.go:52: | 01:46:25 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
recovered_vpn=158.173.241.11 wan=136.51.115.252
|
||||
TUNNEL_RECOVERED_OK
|
||||
l.go:52: | 01:46:25 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:46:25 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [36;1mTRY[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:46:25 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCLEANUP[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:46:25 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mDELETE[0;22m | [32;1mOK[0;22m | batch/v1/Job @ chainsaw-credible-bird/tunnel-stop
|
||||
l.go:52: | 01:46:25 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCLEANUP[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:46:25 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mCLEANUP[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:46:25 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mDELETE[0;22m | [32;1mOK[0;22m | v1/Pod @ chainsaw-credible-bird/egress-vpn
|
||||
l.go:52: | 01:46:56 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mDELETE[0;22m | [32;1mOK[0;22m | v1/Pod @ chainsaw-credible-bird/egress-internet
|
||||
l.go:52: | 01:47:26 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mCLEANUP[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:47:26 | [36;1mvpn-killswitch[0;22m | [36;1m@chainsaw [0;22m | [36;1mCLEANUP[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:47:26 | [32;1mvpn-killswitch[0;22m | [32;1m@chainsaw [0;22m | [32;1mDELETE[0;22m | [32;1mOK[0;22m | v1/Namespace @ chainsaw-credible-bird
|
||||
l.go:52: | 01:47:33 | [36;1mvpn-killswitch[0;22m | [36;1m@chainsaw [0;22m | [36;1mCLEANUP[0;22m | [36;1mEND[0;22m |
|
||||
--- PASS: chainsaw (0.00s)
|
||||
--- PASS: chainsaw/vpn-killswitch (174.63s)
|
||||
PASS
|
||||
Tests Summary...
|
||||
- Passed tests 1
|
||||
- Failed tests 0
|
||||
- Skipped tests 0
|
||||
Done.
|
||||
[1m[run.sh][0m HEAVY pass: no heavy tests selected.
|
||||
merge_junit: merged 1 report(s) -> /work/.claude/conf-clone/conformance/_artifacts/report.junit.xml (tests=1 failures=0 errors=0 skipped=0)
|
||||
[1m[run.sh][0m Done. JUnit: /work/.claude/conf-clone/conformance/_artifacts/report.junit.xml | Coverage: /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.md
|
||||
@@ -0,0 +1,4 @@
|
||||
<testsuites name="chainsaw-report" tests="1" failures="0" errors="0" time="0.000000"><testsuite name="/work/.claude/conf-clone/conformance/tests/l3-fault-injection/vpn-killswitch" tests="1" failures="0" errors="0" id="0" time="">
|
||||
<testcase name="vpn-killswitch" classname="" time="173.473422" />
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -0,0 +1,237 @@
|
||||
[1m[run.sh][0m Coverage report first (always)
|
||||
[coverage] 49/51 requirements have a scaffolded test (96.1%); 2 gap(s), 0 safety-critical gap(s).
|
||||
[coverage] wrote /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.md and /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.json
|
||||
[1m[run.sh][0m No L0/L1 offline checks match the selection (layer=l3 reqs='R-NET-06').
|
||||
[1m[run.sh][0m LIGHT pass: 1 test(s), parallel=2 (pinned v0.2.12)
|
||||
Version: 0.2.12
|
||||
Loading config (/work/.claude/conf-clone/conformance/config/chainsaw-config.yaml)...
|
||||
- Using test file: chainsaw-test
|
||||
- TestDirs [/work/.claude/conf-clone/conformance/tests/l3-fault-injection/vpn-killswitch]
|
||||
- SkipDelete false
|
||||
- FailFast false
|
||||
- ReportFormat 'JUNIT-TEST'
|
||||
- ReportName 'chainsaw-report-light'
|
||||
- ReportPath '/work/.claude/conf-clone/conformance/_artifacts'
|
||||
- Namespace ''
|
||||
- FullName false
|
||||
- IncludeTestRegex ''
|
||||
- ExcludeTestRegex ''
|
||||
- ApplyTimeout 1m30s
|
||||
- AssertTimeout 3m0s
|
||||
- CleanupTimeout 2m0s
|
||||
- DeleteTimeout 2m0s
|
||||
- ErrorTimeout 1m0s
|
||||
- ExecTimeout 3m0s
|
||||
- DeletionPropagationPolicy Background
|
||||
- Parallel 2
|
||||
- Template true
|
||||
- NoCluster false
|
||||
- PauseOnFailure false
|
||||
Loading tests...
|
||||
- vpn-killswitch (/work/.claude/conf-clone/conformance/tests/l3-fault-injection/vpn-killswitch)
|
||||
Loading values...
|
||||
Running tests...
|
||||
=== RUN chainsaw
|
||||
=== PAUSE chainsaw
|
||||
=== CONT chainsaw
|
||||
=== RUN chainsaw/vpn-killswitch
|
||||
=== PAUSE chainsaw/vpn-killswitch
|
||||
=== CONT chainsaw/vpn-killswitch
|
||||
l.go:52: | 01:49:24 | [32;1mvpn-killswitch[0;22m | [32;1m@chainsaw [0;22m | [32;1mCREATE[0;22m | [32;1mOK[0;22m | v1/Namespace @ chainsaw-beloved-toad
|
||||
l.go:52: | 01:49:24 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mTRY[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:49:24 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
kubectl label namespace "$NAMESPACE" \
|
||||
pod-security.kubernetes.io/enforce=privileged \
|
||||
pod-security.kubernetes.io/warn=privileged \
|
||||
pod-security.kubernetes.io/audit=privileged \
|
||||
--overwrite
|
||||
echo "NS_PRIVILEGED_OK"
|
||||
l.go:52: | 01:49:24 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
namespace/chainsaw-beloved-toad labeled
|
||||
NS_PRIVILEGED_OK
|
||||
l.go:52: | 01:49:24 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:49:24 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mAPPLY[0;22m | [36;1mRUN[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-internet
|
||||
l.go:52: | 01:49:24 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mCREATE[0;22m | [32;1mOK[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-internet
|
||||
l.go:52: | 01:49:24 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mAPPLY[0;22m | [32;1mDONE[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-internet
|
||||
l.go:52: | 01:49:24 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mAPPLY[0;22m | [36;1mRUN[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-vpn
|
||||
l.go:52: | 01:49:25 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mCREATE[0;22m | [32;1mOK[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-vpn
|
||||
l.go:52: | 01:49:25 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mAPPLY[0;22m | [32;1mDONE[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-vpn
|
||||
l.go:52: | 01:49:25 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mASSERT[0;22m | [36;1mRUN[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-internet
|
||||
l.go:52: | 01:49:26 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mASSERT[0;22m | [32;1mDONE[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-internet
|
||||
l.go:52: | 01:49:26 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mASSERT[0;22m | [36;1mRUN[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-vpn
|
||||
l.go:52: | 01:49:26 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mASSERT[0;22m | [32;1mDONE[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-vpn
|
||||
l.go:52: | 01:49:26 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
ns="$NAMESPACE"
|
||||
get_ip() {
|
||||
kubectl -n "$ns" exec "$1" -c curl -- \
|
||||
curl -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true
|
||||
}
|
||||
wan="" vpn=""
|
||||
for i in $(seq 1 12); do
|
||||
[ -n "$wan" ] || wan="$(get_ip egress-internet)"
|
||||
[ -n "$vpn" ] || vpn="$(get_ip egress-vpn)"
|
||||
if [ -n "$wan" ] && [ -n "$vpn" ] && [ "$wan" != "$vpn" ]; then break; fi
|
||||
[ "$wan" = "$vpn" ] && vpn="" # not steered yet; re-poll
|
||||
sleep 5
|
||||
done
|
||||
echo "wan=$wan vpn=$vpn"
|
||||
[ -n "$wan" ] || { echo "FAIL: no WAN IP from internet-class pod" >&2; exit 1; }
|
||||
[ -n "$vpn" ] || { echo "FAIL: vpn-class egress not working at baseline" >&2; exit 1; }
|
||||
[ "$wan" != "$vpn" ] || { echo "FAIL: vpn-class == WAN at baseline (not tunneled)" >&2; exit 1; }
|
||||
echo "TUNNEL_HEALTHY_OK"
|
||||
l.go:52: | 01:49:27 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
wan=136.51.115.252 vpn=158.173.241.11
|
||||
TUNNEL_HEALTHY_OK
|
||||
l.go:52: | 01:49:27 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:49:27 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mTRY[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:49:27 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mTRY[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:49:27 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mAPPLY[0;22m | [36;1mRUN[0;22m | batch/v1/Job @ chainsaw-beloved-toad/tunnel-stop
|
||||
l.go:52: | 01:49:27 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mCREATE[0;22m | [32;1mOK[0;22m | batch/v1/Job @ chainsaw-beloved-toad/tunnel-stop
|
||||
l.go:52: | 01:49:27 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mAPPLY[0;22m | [32;1mDONE[0;22m | batch/v1/Job @ chainsaw-beloved-toad/tunnel-stop
|
||||
l.go:52: | 01:49:27 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mASSERT[0;22m | [36;1mRUN[0;22m | batch/v1/Job @ chainsaw-beloved-toad/tunnel-stop
|
||||
l.go:52: | 01:49:31 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mASSERT[0;22m | [32;1mDONE[0;22m | batch/v1/Job @ chainsaw-beloved-toad/tunnel-stop
|
||||
l.go:52: | 01:49:31 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -u
|
||||
ns="$NAMESPACE"
|
||||
wan="$(kubectl -n "$ns" exec egress-internet -c curl -- \
|
||||
curl -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true)"
|
||||
for i in $(seq 1 8); do
|
||||
out="$(kubectl -n "$ns" exec egress-vpn -c curl -- \
|
||||
curl -fsS --max-time 8 https://api.ipify.org 2>/dev/null || true)"
|
||||
if [ -n "$out" ]; then
|
||||
if [ -n "$wan" ] && [ "$out" = "$wan" ]; then
|
||||
echo "LEAK: vpn-class reached the WAN IP ($out) with the tunnel DOWN" >&2; exit 1
|
||||
fi
|
||||
echo "LEAK: vpn-class reached the internet ($out) with the tunnel DOWN" >&2; exit 1
|
||||
fi
|
||||
echo "attempt $i: vpn-class blackholed (no response), good"
|
||||
sleep 3
|
||||
done
|
||||
echo "NO_LEAK_OK"
|
||||
l.go:52: | 01:51:01 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
attempt 1: vpn-class blackholed (no response), good
|
||||
attempt 2: vpn-class blackholed (no response), good
|
||||
attempt 3: vpn-class blackholed (no response), good
|
||||
attempt 4: vpn-class blackholed (no response), good
|
||||
attempt 5: vpn-class blackholed (no response), good
|
||||
attempt 6: vpn-class blackholed (no response), good
|
||||
attempt 7: vpn-class blackholed (no response), good
|
||||
attempt 8: vpn-class blackholed (no response), good
|
||||
NO_LEAK_OK
|
||||
l.go:52: | 01:51:01 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:51:01 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
ns="$NAMESPACE"
|
||||
out="$(kubectl -n "$ns" exec egress-internet -c curl -- \
|
||||
curl -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true)"
|
||||
echo "internet_ip=$out"
|
||||
[ -n "$out" ] || { echo "FAIL: internet-class egress broke while tunnel down (must be unaffected)" >&2; exit 1; }
|
||||
echo "INTERNET_UNAFFECTED_OK"
|
||||
l.go:52: | 01:51:02 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
internet_ip=136.51.115.252
|
||||
INTERNET_UNAFFECTED_OK
|
||||
l.go:52: | 01:51:02 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:51:02 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mTRY[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:51:02 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mFINALLY[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:51:02 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
ns="$NAMESPACE"
|
||||
# Distinct Job name (tunnel-restart) so it never collides with the
|
||||
# tunnel-stop Job. Left-aligned manifest so the block scalar yields
|
||||
# valid YAML at column 0 at runtime.
|
||||
cat <<'JOB' | kubectl -n "$ns" apply -f -
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: tunnel-restart
|
||||
labels: { app: vpn-killswitch-fault }
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 90
|
||||
ttlSecondsAfterFinished: 600
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: vpn-killswitch-fault }
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
hostPID: true
|
||||
nodeSelector:
|
||||
homelab.keithjackson.dev/egress-gateway: "true"
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
containers:
|
||||
- name: nsenter
|
||||
image: busybox:1.37.0
|
||||
securityContext:
|
||||
privileged: true
|
||||
command: ["nsenter","-t","1","-m","-u","-i","-n","-p","--","systemctl","start","vpn-egress-tunnel"]
|
||||
JOB
|
||||
# Block teardown until the restart actually completed on the host — a
|
||||
# leaked-down tunnel is unacceptable. `wait` exits non-zero on failure
|
||||
# or timeout, which (set -e) fails the step and surfaces the problem.
|
||||
kubectl -n "$ns" wait --for=condition=complete --timeout=110s job/tunnel-restart
|
||||
echo "TUNNEL_SELF_HEAL_OK"
|
||||
l.go:52: | 01:51:05 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
job.batch/tunnel-restart created
|
||||
job.batch/tunnel-restart condition met
|
||||
TUNNEL_SELF_HEAL_OK
|
||||
l.go:52: | 01:51:05 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:51:05 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mFINALLY[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:51:05 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [36;1mTRY[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:51:05 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [36;1mCMD[0;22m | [36;1mRUN[0;22m |
|
||||
=== COMMAND
|
||||
/usr/bin/sh -c set -eu
|
||||
ns="$NAMESPACE"
|
||||
wan="$(kubectl -n "$ns" exec egress-internet -c curl -- \
|
||||
curl -fsS --max-time 10 https://api.ipify.org 2>/dev/null || true)"
|
||||
vpn=""
|
||||
for i in $(seq 1 24); do
|
||||
vpn="$(kubectl -n "$ns" exec egress-vpn -c curl -- \
|
||||
curl -fsS --max-time 8 https://api.ipify.org 2>/dev/null || true)"
|
||||
if [ -n "$vpn" ] && [ "$vpn" != "$wan" ]; then break; fi
|
||||
vpn=""
|
||||
sleep 5
|
||||
done
|
||||
echo "recovered_vpn=$vpn wan=$wan"
|
||||
[ -n "$vpn" ] || { echo "FAIL: vpn-class egress did not recover after tunnel restart" >&2; exit 1; }
|
||||
[ "$vpn" != "$wan" ] || { echo "FAIL: vpn-class egress recovered onto the WAN (not tunneled)" >&2; exit 1; }
|
||||
echo "TUNNEL_RECOVERED_OK"
|
||||
l.go:52: | 01:51:08 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [36;1mSCRIPT[0;22m | [36;1mLOG[0;22m |
|
||||
=== STDOUT
|
||||
recovered_vpn=158.173.241.11 wan=136.51.115.252
|
||||
TUNNEL_RECOVERED_OK
|
||||
l.go:52: | 01:51:08 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [32;1mSCRIPT[0;22m | [32;1mDONE[0;22m |
|
||||
l.go:52: | 01:51:08 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-tunnel-recovers-connectivity-returns[0;22m | [36;1mTRY[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:51:08 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCLEANUP[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:51:08 | [32;1mvpn-killswitch[0;22m | [32;1mwhen-force-tunnel-down-then-no-leak [0;22m | [32;1mDELETE[0;22m | [32;1mOK[0;22m | batch/v1/Job @ chainsaw-beloved-toad/tunnel-stop
|
||||
l.go:52: | 01:51:08 | [36;1mvpn-killswitch[0;22m | [36;1mwhen-force-tunnel-down-then-no-leak [0;22m | [36;1mCLEANUP[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:51:08 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mCLEANUP[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:51:08 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mDELETE[0;22m | [32;1mOK[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-vpn
|
||||
l.go:52: | 01:51:39 | [32;1mvpn-killswitch[0;22m | [32;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [32;1mDELETE[0;22m | [32;1mOK[0;22m | v1/Pod @ chainsaw-beloved-toad/egress-internet
|
||||
l.go:52: | 01:52:10 | [36;1mvpn-killswitch[0;22m | [36;1mgiven-healthy-tunnel-and-two-workloads [0;22m | [36;1mCLEANUP[0;22m | [36;1mEND[0;22m |
|
||||
l.go:52: | 01:52:10 | [36;1mvpn-killswitch[0;22m | [36;1m@chainsaw [0;22m | [36;1mCLEANUP[0;22m | [36;1mBEGIN[0;22m |
|
||||
l.go:52: | 01:52:10 | [32;1mvpn-killswitch[0;22m | [32;1m@chainsaw [0;22m | [32;1mDELETE[0;22m | [32;1mOK[0;22m | v1/Namespace @ chainsaw-beloved-toad
|
||||
l.go:52: | 01:52:18 | [36;1mvpn-killswitch[0;22m | [36;1m@chainsaw [0;22m | [36;1mCLEANUP[0;22m | [36;1mEND[0;22m |
|
||||
--- PASS: chainsaw (0.00s)
|
||||
--- PASS: chainsaw/vpn-killswitch (173.47s)
|
||||
PASS
|
||||
Tests Summary...
|
||||
- Passed tests 1
|
||||
- Failed tests 0
|
||||
- Skipped tests 0
|
||||
Done.
|
||||
[1m[run.sh][0m HEAVY pass: no heavy tests selected.
|
||||
merge_junit: merged 1 report(s) -> /work/.claude/conf-clone/conformance/_artifacts/report.junit.xml (tests=1 failures=0 errors=0 skipped=0)
|
||||
[1m[run.sh][0m Done. JUnit: /work/.claude/conf-clone/conformance/_artifacts/report.junit.xml | Coverage: /work/.claude/conf-clone/conformance/_artifacts/coverage/coverage.md
|
||||
@@ -1,2 +1,5 @@
|
||||
{"chainsaw_version": "v0.2.12", "env": "dev", "git_sha": "57d4df8c4f992732a49f3e2daddcace866778ab0", "junit_result": "passed", "kube_version": "v1.36.4+k3s1", "layer": "L3", "note": "run log OUTPUT carries every declared pass token (NODE_LOSS_VOLUME_AVAILABLE_OK) and no skip token [log scoped to this testcase; tokens read from real-output sections only]", "proof": "proven", "requirements": ["R-STATE-02"], "run_id": "20260901T235117Z-e9a4b66", "sentinel": "NODE_LOSS_VOLUME_AVAILABLE_OK", "test": "node-loss", "ts_utc": "2026-09-01T23:51:17Z"}
|
||||
{"chainsaw_version": "v0.2.12", "env": "dev", "git_sha": "fdc3265585f4b775c0a6ae3fb33c837b0c643655", "junit_result": "passed", "kube_version": "v1.36.4+k3s1", "layer": "L3", "note": "run log OUTPUT carries every declared pass token (OFF_CLUSTER_TARGET_OK, BACKUPS_ARE_SCHEDULED_OK, DR_RESTORED_FROM_OFF_CLUSTER_BACKUP_OK) and no skip token [log scoped to this testcase; tokens read from real-output sections only]", "proof": "proven", "requirements": ["R-STATE-03"], "run_id": "20260902T001907Z-fdc3265", "sentinel": "DR_RESTORED_FROM_OFF_CLUSTER_BACKUP_OK", "test": "dr-restore", "ts_utc": "2026-09-02T00:19:07Z"}
|
||||
{"chainsaw_version": "v0.2.12", "env": "prod", "git_sha": "19fc8bcb27eab42a0007b28a831519d3e325c7b6", "junit_result": "passed", "kube_version": "v1.36.4+k3s1", "layer": "L1", "note": "L1 offline check exited 0; run_l1_offline.py renders its skips as JUnit <skipped/>, so a green JUnit is truthful without a sentinel", "proof": "proven", "requirements": ["R-NET-05", "R-NET-06", "R-NET-07"], "run_id": "20260913T014321Z-19fc8bc", "sentinel": null, "test": "egress-class-render", "ts_utc": "2026-09-13T01:43:21Z"}
|
||||
{"chainsaw_version": "v0.2.12", "env": "prod", "git_sha": "19fc8bcb27eab42a0007b28a831519d3e325c7b6-dirty", "junit_result": "passed", "kube_version": "v1.36.4+k3s1", "layer": "L3", "note": "run log OUTPUT carries every declared pass token (NS_PRIVILEGED_OK, TUNNEL_HEALTHY_OK, NO_LEAK_OK, INTERNET_UNAFFECTED_OK, TUNNEL_SELF_HEAL_OK, TUNNEL_RECOVERED_OK) and no skip token [log scoped to this testcase; tokens read from real-output sections only]", "proof": "proven", "requirements": ["R-NET-06"], "run_id": "20260913T014437Z-19fc8bc", "sentinel": "TUNNEL_RECOVERED_OK", "test": "vpn-killswitch", "ts_utc": "2026-09-13T01:44:37Z"}
|
||||
{"chainsaw_version": "v0.2.12", "env": "prod", "git_sha": "9d0814ae61e6a0ba64d3789a71f1cd9c985efbf0", "junit_result": "passed", "kube_version": "v1.36.4+k3s1", "layer": "L3", "note": "run log OUTPUT carries every declared pass token (NS_PRIVILEGED_OK, TUNNEL_HEALTHY_OK, NO_LEAK_OK, INTERNET_UNAFFECTED_OK, TUNNEL_SELF_HEAL_OK, TUNNEL_RECOVERED_OK) and no skip token [log scoped to this testcase; tokens read from real-output sections only]", "proof": "proven", "requirements": ["R-NET-06"], "run_id": "20260913T014923Z-9d0814a", "sentinel": "TUNNEL_RECOVERED_OK", "test": "vpn-killswitch", "ts_utc": "2026-09-13T01:49:23Z"}
|
||||
@@ -250,6 +250,11 @@ try:
|
||||
sys.dont_write_bytecode = True
|
||||
import record_run as rr # noqa: E402
|
||||
|
||||
# status.py owns the latest-wins rule; import it rather than reimplementing it, so the two
|
||||
# cannot disagree about which rows are live evidence.
|
||||
import status as _st # noqa: E402
|
||||
rr_latest_by_test = _st.latest_by_test
|
||||
|
||||
smap = rr.sentinel_map(CONF / "tests")
|
||||
check(len(smap) >= 10,
|
||||
f"record_run.py's sentinel map contains {len(smap)} test(s) — the parser lost its "
|
||||
@@ -386,6 +391,15 @@ try:
|
||||
check(not bad,
|
||||
f"conformance/runs/ledger.jsonl has unparsable line(s) {bad} — the audit record "
|
||||
"is not machine-readable, so no proof can be derived from it")
|
||||
# Which rows are HONOURED: status.py resolves duplicates latest-wins (latest_by_test),
|
||||
# so a superseded row is not evidence of anything. The dirty-tree assertion below is
|
||||
# therefore scoped to honoured rows. Without that scoping, ONE historical dirty row reds
|
||||
# this gate permanently even after a clean re-run of the same test — and a dirty-then-
|
||||
# clean sequence is the normal shape of "noticed the tree was dirty, fixed it, re-ran".
|
||||
# The row is still kept in the ledger and its run.log stays committed: nothing is erased,
|
||||
# it simply stops being treated as a live claim. Structural assertions (provenance keys,
|
||||
# sentinel, test-still-exists) still apply to EVERY row.
|
||||
honoured_ids = {id(v) for v in rr_latest_by_test(rows).values()}
|
||||
for r in rows:
|
||||
for key in ("ts_utc", "git_sha", "test", "requirements", "junit_result", "proof",
|
||||
"run_id"):
|
||||
@@ -393,13 +407,22 @@ try:
|
||||
f"a ledger row for {r.get('test', '?')!r} lacks {key!r} — a row without "
|
||||
"provenance cannot be audited")
|
||||
if r.get("proof") == "proven":
|
||||
check(bool(r.get("sentinel")),
|
||||
f"a ledger row claims `proven` for {r.get('test')!r} with NO sentinel — "
|
||||
"the sentinel is the only thing separating a real pass from a self-skip")
|
||||
# L0/L1 rows are recorded proven WITHOUT a sentinel, deliberately, by
|
||||
# record_run.py's own SENTINEL_EXEMPT_LAYERS: run_l1_offline.py renders skips as
|
||||
# JUnit <skipped/>, so those layers have no pass token to attribute. This
|
||||
# assertion had NO layer exemption, so the two halves of the harness contradicted
|
||||
# each other — ANY `--record` run touching L0 or L1 wrote a row this gate then
|
||||
# rejected, which meant a requirement whose verifier set includes an L1 check
|
||||
# (R-NET-06 is one) could never be both PROVEN and L0-green. Honour the same
|
||||
# constant rather than restating the policy here, so the two cannot drift apart.
|
||||
if r.get("layer") not in rr.SENTINEL_EXEMPT_LAYERS:
|
||||
check(bool(r.get("sentinel")),
|
||||
f"a ledger row claims `proven` for {r.get('test')!r} with NO sentinel — "
|
||||
"the sentinel is the only thing separating a real pass from a self-skip")
|
||||
check(r.get("test") in smap,
|
||||
f"a ledger row claims `proven` for {r.get('test')!r}, which is not a "
|
||||
"test in the suite any more — a proof of something that no longer exists")
|
||||
check("-dirty" not in str(r.get("git_sha", "")),
|
||||
check(id(r) not in honoured_ids or "-dirty" not in str(r.get("git_sha", "")),
|
||||
f"a ledger row claims `proven` for {r.get('test')!r} from a DIRTY tree "
|
||||
f"({r.get('git_sha')}) — what ran is not what is committed")
|
||||
else:
|
||||
|
||||
Reference in new issue
Block a user