railiance-platform/tests/test_audit_attended_kubeconfig.py
codex dad91e8c44 Route the sender lane's cluster writes through AUDIT_KUBECONFIG too.
Round-10 review (railiance-platform) found that ContainedOperator inherited
an ssh-only kubectl, so an E2 would write OpenBao and then fail its sync,
restart and rollback. Also: a bad AUDIT_KUBECONFIG now leaves window-error
evidence, and the receipt script validates bodies and lets a later
request-changes withdraw an approval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 236506@bnt-lap001
Assistant-Session: 09087018-91e1-48c5-ab2f-acfbb62c92e4
2026-11-03 15:09:59 +01:00

109 lines
4.7 KiB
Python

"""AUDIT_KUBECONFIG routes cluster reads and the port-forward locally.
Since RCLUSTER-WP-0019 the node account cannot read the k3s admin kubeconfig.
Attended runs name the operator's own kubeconfig for one command. Unset keeps
the earlier ssh path unchanged.
"""
import importlib.util
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
ROOT = Path(__file__).resolve().parents[1]
def load(name, rel):
sys.path.insert(0, str(ROOT / "scripts"))
spec = importlib.util.spec_from_file_location(name, ROOT / rel)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
PRE = load("pre_kc", "scripts/audit-core-recovery-preflight.py")
WIN = load("win_kc", "scripts/t02-attended-window.py")
class Done:
def __init__(self, rc=0, out="{}"):
self.returncode, self.stdout, self.stderr = rc, out, "secret-looking stderr"
def test_unset_keeps_ssh_path(monkeypatch):
monkeypatch.delenv("AUDIT_KUBECONFIG", raising=False)
remote = PRE.Remote("railiance01")
with patch.object(PRE.subprocess, "run", return_value=Done()) as run:
remote.kubectl(["get", "nodes"], label="read node state")
assert run.call_args.args[0][:3] == ["ssh", "-o", "BatchMode=yes"]
assert WIN.PortForward("railiance01", 18080).command()[0] == "ssh"
def test_set_runs_kubectl_locally_with_that_file(monkeypatch, tmp_path):
kc = tmp_path / "config"; kc.write_text("not read")
monkeypatch.setenv("AUDIT_KUBECONFIG", str(kc))
remote = PRE.Remote("railiance01")
with patch.object(PRE.subprocess, "run", return_value=Done()) as run:
remote.kubectl(["get", "nodes", "-o", "json"], label="read node state")
assert run.call_args.args[0] == ["kubectl", "--kubeconfig", str(kc), "get", "nodes", "-o", "json"]
# Host-level reads still use ssh to the node.
with patch.object(PRE.subprocess, "run", return_value=Done(out="12.0 1.0")) as run:
remote.run(["cat", "/proc/uptime"], label="read host uptime")
assert run.call_args.args[0][0] == "ssh"
command = WIN.PortForward("railiance01", 18080).command()
assert command[:5] == ["timeout", str(WIN.REMOTE_PF_TIMEOUT_SECONDS), "kubectl", "--kubeconfig", str(kc)]
assert "ssh" not in command and "127.0.0.1" in command
def test_failure_reports_only_the_step(monkeypatch, tmp_path):
kc = tmp_path / "config"; kc.write_text("")
monkeypatch.setenv("AUDIT_KUBECONFIG", str(kc))
with patch.object(PRE.subprocess, "run", return_value=Done(rc=1)):
with pytest.raises(PRE.PreflightError) as err:
PRE.Remote("railiance01").kubectl(["get", "nodes"], label="read node state")
assert str(err.value) == "read node state failed (exit 1)"
@pytest.mark.parametrize("value", ["relative/config", "/nonexistent/config"])
def test_bad_path_is_refused(monkeypatch, value):
monkeypatch.setenv("AUDIT_KUBECONFIG", value)
with pytest.raises(PRE.PreflightError):
PRE.Remote("railiance01")
LANE = load("lane_kc", "scripts/audit_core_synthetic_sender_lane.py")
def operator(monkeypatch):
monkeypatch.setattr(LANE.LEASE, "contained_bao_env", lambda: {"BAO_ADDR": "http://127.0.0.1:18200"})
return LANE.ContainedOperator("railiance01")
def test_lane_operator_writes_use_the_attended_kubeconfig(monkeypatch, tmp_path):
kc = tmp_path / "config"; kc.write_text("not read")
monkeypatch.setenv("AUDIT_KUBECONFIG", str(kc))
op = operator(monkeypatch)
with patch.object(LANE.CREDS.subprocess, "run", return_value=Done()) as run:
op.kubectl(["-n", "audit-core", "rollout", "restart", "deploy/audit-core"], label="restart audit-core")
assert run.call_args.args[0] == ["kubectl", "--kubeconfig", str(kc), "-n", "audit-core",
"rollout", "restart", "deploy/audit-core"]
def test_lane_operator_unset_keeps_ssh(monkeypatch):
monkeypatch.delenv("AUDIT_KUBECONFIG", raising=False)
op = operator(monkeypatch)
with patch.object(LANE.CREDS.subprocess, "run", return_value=Done()) as run:
op.kubectl(["get", "pods"], label="read pods")
assert run.call_args.args[0][:3] == ["ssh", "-o", "BatchMode=yes"]
def test_window_records_bad_kubeconfig_as_window_error(monkeypatch, tmp_path):
monkeypatch.setenv("AUDIT_KUBECONFIG", "relative/config")
evidence = tmp_path / "evidence"; evidence.mkdir(mode=0o700)
monkeypatch.setattr(WIN, "window", lambda args: WIN.PortForward("railiance01", 18080))
code = WIN.main(["--approval", str(tmp_path / "a.json"), "--load-driver", str(tmp_path / "d"),
"--bearer-file", str(tmp_path / "b"), "--evidence-dir", str(evidence),
"--confirm", "x"])
assert code == 1
assert [p.name.startswith("window-error-") for p in evidence.iterdir()] == [True]