NK-WP-0058-T03 step 2: executor --preflight-only
All checks were successful
Build and Publish bounded-executor / build-and-push (push) Successful in 14s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Identity provider journey acceptance / provider (push) Successful in 52s

Reachability per endpoint, setup without the window-scoped write role (no Kubernetes or directory
client), then every preflight check (admission, executor, source, trust, registry, window,
approval) with its phase; no consume, emit or write. Exit 0 ready, 4 not. preflight() is now the
same named checks, stopping at the first. Setup reasons keep the clients' fixed texts.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 943911@bnt-lap001
Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0
This commit is contained in:
tegwick 2026-11-05 23:27:13 +01:00
parent 7350ba8003
commit 64cec27c28
3 changed files with 179 additions and 32 deletions

View file

@ -17,6 +17,12 @@ The config (JSON, non-secret) names endpoints, token files and the pilot's targe
Exit codes: 0 completed, 3 reverted, 4 refused before any effect, 5 stopped / unknown / revert failed / budget,
6 consumed but not in custody. The result printed is value-free.
python3 bounded-executor/cli.py --preflight-only --admission FILE --approval-id ID --config FILE [--result FILE]
Preflight-only (NK-WP-0058-T03): reachability of every endpoint, setup without the window-scoped write role, and
every preflight check (admission, executor, source pins, trust, registry, window, approval), each reported with its
phase. No consume, no audit event, no write. Exit 0 ready, 4 not ready. Replaces hand-written diagnostic pods.
Kill switch (NK-WP-0056-T07): SIGTERM or SIGINT, e.g. the founder's `kubectl -n sso delete job <job>`, sets the stop
flag. Before consume the run refuses and spends nothing; after it, the runner finishes the step in flight, then
stops and reverts what it changed (BEA stop_and_revert). The Job's terminationGracePeriodSeconds must cover a
@ -120,9 +126,10 @@ def run_endpoints(cfg: dict) -> dict:
return ends
def build(cfg: dict, logins: list):
def build(cfg: dict, logins: list, *, write_login: bool = True):
"""`logins` collects every OpenBao client as soon as it exists, so the caller revokes them all, even when a
later step of setup fails."""
later step of setup fails. write_login=False (--preflight-only) skips the window-scoped write role and the
Kubernetes and directory clients: the run's operations are registered but cannot act."""
o = cfg["openbao"]
if cfg["journey"]["k8s_role"] == o["k8s_role"]:
raise ValueError("the journey read and the bind write must use separate roles")
@ -135,15 +142,20 @@ def build(cfg: dict, logins: list):
logins.append(client)
return client
bao = login(o["k8s_role"])
bao = login(o["k8s_role"]) if write_login else None
journey_bao = login(cfg["journey"]["k8s_role"])
directory = OPS.Directory(cfg["lldap"]["url"], [cfg["lldap"].get("set_password", "/app/lldap_set_password")],
ca_file=cfg["lldap"].get("ca_file"))
kube = OPS.Kube()
k = cfg["kubernetes"]
directory = delivery = restart = None
if write_login:
directory = OPS.Directory(cfg["lldap"]["url"],
[cfg["lldap"].get("set_password", "/app/lldap_set_password")],
ca_file=cfg["lldap"].get("ca_file"))
kube = OPS.Kube()
k = cfg["kubernetes"]
delivery = OPS.KubeDelivery(kube, k["namespace"], k["external_secret"])
restart = OPS.KubeRestart(kube, k["namespace"], k["deployment"])
operations, checks = OPS.pilot_registry(
bao=bao, journey_bao=journey_bao, directory=directory, delivery=OPS.KubeDelivery(kube, k["namespace"], k["external_secret"]),
restart=OPS.KubeRestart(kube, k["namespace"], k["deployment"]), bind_kv=cfg["bind"]["kv"],
bao=bao, journey_bao=journey_bao, directory=directory, delivery=delivery,
restart=restart, bind_kv=cfg["bind"]["kv"],
bind_user=cfg["bind"]["user"], journey_kv=cfg["journey"]["kv"], journey_config=cfg["journey"]["config"])
trusted = set(cfg.get("trust_satisfied", []))
@ -315,6 +327,50 @@ def run_journey_only(cfg: dict) -> tuple[dict, int]:
return result, code
def setup_reason(error: Exception) -> str:
"""Value-free: the clients' own errors carry fixed texts (an endpoint name, a status code, a shape)."""
if isinstance(error, OPS.Unreachable):
return str(error) # "<endpoint> not reachable", unchanged since c2a0f2f
if isinstance(error, (OPS.OperationError, C.ClientError)):
return f"{type(error).__name__}:{error}"
return type(error).__name__
def preflight_only(admission: dict, approval_id: str, cfg: dict) -> tuple[dict, int]:
"""--preflight-only (NK-WP-0058-T03): reachability per endpoint, setup without the write role, then every
preflight check, without consume, emit or any write. Reports every failure with its phase. Exit 0 ready, 4 not."""
checks: list[dict] = []
def add(phase, check, result, reason=None):
checks.append({"phase": phase, "check": check, "result": result, **({"reason": reason} if reason else {})})
endpoints = run_endpoints(cfg)
if cfg.get("informed_decision"):
endpoints["informed-decision"] = (cfg["informed_decision"]["url"], "/")
for name, (base, path) in endpoints.items():
try:
OPS.wait_reachable(base, timeout=10, path=path, name=name)
add("reachability", name, "pass")
except OPS.Unreachable:
add("reachability", name, "fail", "unreachable")
logins: list = []
try:
config, deps = build(cfg, logins, write_login=False)
add("setup", "lanes", "pass")
add("setup", "write_lane", "skipped", "window_scoped")
for item in X.Run(admission, approval_id, config, deps).preflight_report():
add("preflight", item["check"], item["result"], item.get("reason"))
except Exception as error: # nothing was consumed or written
add("setup", "build", "fail", setup_reason(error))
finally:
for client in logins:
client.revoke_self()
ready = all(c["result"] != "fail" for c in checks)
failures = [f"{c['phase']}:{c['check']}" for c in checks if c["result"] == "fail"]
return {"mode": "preflight-only", "ready": ready, "effects": False, "failures": failures,
"checks": checks}, (0 if ready else 4)
def exit_code(result: dict) -> int:
if result.get("reason") in ("receipt_not_in_custody", "audit_unavailable") and result["outcome"] != "refused":
return 6
@ -325,6 +381,8 @@ def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
parser.add_argument("--journey-only", action="store_true", help="read-only journey run, no admission (T10)")
parser.add_argument("--request", action="store_true", help="file the approval for --admission (T18)")
parser.add_argument("--preflight-only", action="store_true",
help="setup and preflight without consume or the write role (NK-WP-0058-T03)")
parser.add_argument("--admission")
parser.add_argument("--approval-id")
parser.add_argument("--config", required=True)
@ -336,6 +394,13 @@ def main(argv=None) -> int:
result, code = run_request(args.admission, json.loads(pathlib.Path(args.config).read_text()))
_emit(result, args.result)
return code
if args.preflight_only:
if args.journey_only or args.request or not (args.admission and args.approval_id):
parser.error("--preflight-only takes --admission, --approval-id and --config")
result, code = preflight_only(X.bea.load(pathlib.Path(args.admission)), args.approval_id,
json.loads(pathlib.Path(args.config).read_text()))
_emit(result, args.result)
return code
if args.journey_only == bool(args.admission or args.approval_id):
parser.error("give either --journey-only, or --admission with --approval-id")
if not args.journey_only and not (args.admission and args.approval_id):
@ -351,8 +416,7 @@ def main(argv=None) -> int:
try:
config, deps = build(cfg, logins)
except Exception as error: # setup failed before preflight: nothing was consumed
where = str(error) if isinstance(error, OPS.Unreachable) else type(error).__name__
result = {"outcome": "refused", "reason": "setup:" + where, "run_id": None, "effects": False}
result = {"outcome": "refused", "reason": "setup:" + setup_reason(error), "run_id": None, "effects": False}
else:
run = X.Run(admission, args.approval_id, config, deps)
try:

View file

@ -155,28 +155,61 @@ class Run:
self.start_mono: float | None = None
# ------------------------------------------------------------------ preflight (no effect)
def preflight(self) -> None:
report = bea.evaluate(self.doc, self.schema)
if not report["valid"]:
raise Refused("admission_invalid:" + ",".join(sorted({f["rule"] for f in report["findings"]})))
def _preflight_steps(self) -> list[tuple[str, Callable[[], None]]]:
"""The preflight as named checks, in order. preflight() stops at the first refusal; preflight_report()
runs them all (NK-WP-0058-T03 --preflight-only). None of them has an effect."""
spec = self.spec
if spec["executor"]["identity"] != self.cfg.executor_identity:
raise Refused("not_this_executor")
for pinned in spec["plan"]["source"]["files"]:
path = self.cfg.source_root / pinned["path"]
if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != pinned["sha256"]:
raise Refused("source_drift")
for state in spec["trust_requires"]:
if not self.d.trust(state):
raise Refused("trust_not_satisfied:" + state)
for step in spec["plan"]["steps"] + spec["plan"].get("revert_steps", []):
if step["operation"] not in self.d.operations:
raise Refused("unknown_operation")
for check in spec["checks"]:
if check["id"] not in self.d.checks:
raise Refused("unknown_check")
self._check_window()
self._check_approval(self.d.carrier.get(self.approval_id), consumed_ok=False)
def admission():
report = bea.evaluate(self.doc, self.schema)
if not report["valid"]:
raise Refused("admission_invalid:" + ",".join(sorted({f["rule"] for f in report["findings"]})))
def executor():
if spec["executor"]["identity"] != self.cfg.executor_identity:
raise Refused("not_this_executor")
def source():
for pinned in spec["plan"]["source"]["files"]:
path = self.cfg.source_root / pinned["path"]
if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != pinned["sha256"]:
raise Refused("source_drift")
def trust():
for state in spec["trust_requires"]:
if not self.d.trust(state):
raise Refused("trust_not_satisfied:" + state)
def registry():
for step in spec["plan"]["steps"] + spec["plan"].get("revert_steps", []):
if step["operation"] not in self.d.operations:
raise Refused("unknown_operation")
for check in spec["checks"]:
if check["id"] not in self.d.checks:
raise Refused("unknown_check")
def approval():
self._check_approval(self.d.carrier.get(self.approval_id), consumed_ok=False)
return [("admission", admission), ("executor", executor), ("source", source), ("trust", trust),
("registry", registry), ("window", self._check_window), ("approval", approval)]
def preflight(self) -> None:
for _, check in self._preflight_steps():
check()
def preflight_report(self) -> list[dict]:
"""Every preflight check with its result; value-free. An error that is not a refusal is named by type."""
results = []
for name, check in self._preflight_steps():
try:
check()
results.append({"check": name, "result": "pass"})
except Refused as refusal:
results.append({"check": name, "result": "fail", "reason": str(refusal)})
except Exception as error:
results.append({"check": name, "result": "fail", "reason": "error:" + type(error).__name__})
return results
def _check_window(self) -> None:
now = self.d.wall()

View file

@ -113,6 +113,56 @@ class CliLanes(Base):
self.assertEqual(self.bao.logins, [])
class PreflightOnly(CliLanes):
"""NK-WP-0058-T03: setup and every preflight check, no write role, no consume, every failure named."""
def run_preflight(self, cfg):
(self.root / "cfg.json").write_text(json.dumps(cfg))
admission = CLI.X.REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml"
with contextlib.redirect_stdout(io.StringIO()) as out:
code = CLI.main(["--preflight-only", "--admission", str(admission), "--approval-id", "apr-1",
"--config", str(self.root / "cfg.json")])
return code, json.loads(out.getvalue())
def pilot_cfg(self):
cfg = self.cfg()
cfg.update(executor_identity="service:nk-bind-executor", runtime_principal="p", tenant="platform",
environment="PRODUCTION", approver_subjects={"admin": "sub-admin"}, trust_satisfied=[],
bind={"kv": "platform/workloads/net-kingdom/directory-binds/svc-authelia", "user": "svc-authelia"},
kubernetes={"namespace": "sso", "external_secret": "e", "deployment": "d"})
return cfg
def test_reports_every_check_without_the_write_role_or_any_consume(self):
code, report = self.run_preflight(self.pilot_cfg())
self.assertEqual(code, 4)
self.assertEqual((report["mode"], report["ready"], report["effects"]), ("preflight-only", False, False))
self.assertEqual([role for role, _ in self.bao.logins], ["journey-read"]) # never the write role
self.assertEqual(self.bao.revoked, ["tok-journey-read-1"])
by = {(c["phase"], c["check"]): c for c in report["checks"]}
self.assertEqual(by[("setup", "write_lane")]["result"], "skipped")
self.assertEqual([c for p, c in by if p == "preflight"],
["admission", "executor", "source", "trust", "registry", "window", "approval"])
self.assertEqual(by[("preflight", "trust")]["reason"], "trust_not_satisfied:runtime_secret_trust")
self.assertEqual(by[("preflight", "approval")]["reason"], "approval_binding") # the fake approval
self.assertIn("preflight:approval", report["failures"])
self.assertNotIn("/consume", " ".join(p for p, _ in self.bao.reads))
def test_an_unreachable_endpoint_is_named(self):
cfg = self.pilot_cfg()
cfg["lldap"]["url"] = "http://127.0.0.1:9"
wait = CLI.OPS.wait_reachable
self.addCleanup(setattr, CLI.OPS, "wait_reachable", wait)
CLI.OPS.wait_reachable = lambda addr, **kw: wait(addr, **{**kw, "timeout": 0.2, "interval": 0.1})
code, report = self.run_preflight(cfg)
self.assertEqual(code, 4)
by = {(c["phase"], c["check"]): c for c in report["checks"]}
self.assertEqual(by[("reachability", "lldap")], {"phase": "reachability", "check": "lldap",
"result": "fail", "reason": "unreachable"})
self.assertEqual(by[("reachability", "openbao")]["result"], "pass")
self.assertEqual(by[("setup", "build")]["reason"], "lldap not reachable")
self.assertEqual(self.bao.logins, [])
class JourneyOnly(Base):
SEED = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"