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: