100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
|
|
"""ops-warden routing contract.
|
||
|
|
|
||
|
|
ops-warden routes non-SSH credential needs here. It must NOT vend secret values.
|
||
|
|
A route result is a pointer: catalog id, readiness, decision status, and the safe
|
||
|
|
next command. This module computes that pointer for a lane. No value is read.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from dataclasses import dataclass, asdict
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from secrets_engine.catalog import CatalogEntry
|
||
|
|
from secrets_engine.decisions import Decision, resolve_decision
|
||
|
|
from secrets_engine.errors import DecisionError
|
||
|
|
from secrets_engine.openbao import OpenBaoClient
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class RouteResult:
|
||
|
|
catalog_id: str
|
||
|
|
owner: str
|
||
|
|
stage: str
|
||
|
|
decision_status: str
|
||
|
|
decision_ref: str
|
||
|
|
review_url: str
|
||
|
|
metadata_applied: bool
|
||
|
|
value_present: bool
|
||
|
|
ready: bool
|
||
|
|
next_command: str
|
||
|
|
missing: str
|
||
|
|
|
||
|
|
def to_json(self) -> dict[str, Any]:
|
||
|
|
return asdict(self)
|
||
|
|
|
||
|
|
|
||
|
|
def route_lane(
|
||
|
|
entry: CatalogEntry,
|
||
|
|
*,
|
||
|
|
hub_url: str,
|
||
|
|
repo_root: Path,
|
||
|
|
client: OpenBaoClient | None = None,
|
||
|
|
) -> RouteResult:
|
||
|
|
"""Build the front-door routing pointer for a lane. Never reads the value."""
|
||
|
|
decision_status = "n/a (bootstrap-only)"
|
||
|
|
decision_ref = entry.approval.get("decision_ref", "")
|
||
|
|
review_url = ""
|
||
|
|
decision: Decision | None = None
|
||
|
|
if entry.approval_required():
|
||
|
|
try:
|
||
|
|
decision = resolve_decision(
|
||
|
|
hub_url=hub_url, repo_root=repo_root, decision_ref=decision_ref
|
||
|
|
)
|
||
|
|
decision_status = decision.status
|
||
|
|
review_url = decision.review_url
|
||
|
|
except DecisionError:
|
||
|
|
decision_status = "missing"
|
||
|
|
|
||
|
|
metadata_applied = False
|
||
|
|
value_present = False
|
||
|
|
if client is not None and client.is_reachable():
|
||
|
|
metadata_applied = client.read_policy(entry.policy_name) is not None
|
||
|
|
# Presence check uses the engine's own token; reports boolean only.
|
||
|
|
field = entry.fields[0] if entry.fields else ""
|
||
|
|
if field:
|
||
|
|
value_present = client.kv_field_present(entry.mount, entry.path, field)
|
||
|
|
|
||
|
|
approved = decision is None or decision.is_approved()
|
||
|
|
ready = approved and metadata_applied and value_present
|
||
|
|
|
||
|
|
if not approved:
|
||
|
|
missing = f"approved decision for '{decision_ref}'"
|
||
|
|
next_command = f"secrets-engine decision inspect {decision_ref or entry.id}"
|
||
|
|
elif not metadata_applied:
|
||
|
|
missing = "OpenBao policy/role apply"
|
||
|
|
next_command = f"secrets-engine apply {decision_ref or entry.id} --stage {entry.stage}"
|
||
|
|
elif not value_present:
|
||
|
|
missing = "provisioned secret value"
|
||
|
|
next_command = (
|
||
|
|
f"secrets-engine provision {entry.id} --stage {entry.stage} "
|
||
|
|
f"--field {entry.fields[0]} --from-file <path>"
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
missing = ""
|
||
|
|
next_command = f"secrets-engine exec --catalog {entry.id} -- <command...>"
|
||
|
|
|
||
|
|
return RouteResult(
|
||
|
|
catalog_id=entry.id,
|
||
|
|
owner=entry.owner,
|
||
|
|
stage=entry.stage,
|
||
|
|
decision_status=decision_status,
|
||
|
|
decision_ref=decision_ref,
|
||
|
|
review_url=review_url,
|
||
|
|
metadata_applied=metadata_applied,
|
||
|
|
value_present=value_present,
|
||
|
|
ready=ready,
|
||
|
|
next_command=next_command,
|
||
|
|
missing=missing,
|
||
|
|
)
|