NK-WP-0058-T03 step 2: executor --preflight-only
All checks were successful
All checks were successful
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:
parent
7350ba8003
commit
64cec27c28
3 changed files with 179 additions and 32 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue