feat: add bounded Kubernetes runtime verification
This commit is contained in:
@@ -35,6 +35,7 @@ from .bundle import (
|
||||
service_names,
|
||||
write_env,
|
||||
)
|
||||
from .cluster_evidence import collect_kubernetes_evidence
|
||||
from .model import (
|
||||
ComponentConfig,
|
||||
DEFAULT_GARAGE_IMAGE,
|
||||
@@ -141,6 +142,37 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
help="Output JSON path; defaults to <directory>/kubernetes.json.",
|
||||
)
|
||||
|
||||
verify_kubernetes = subparsers.add_parser(
|
||||
"verify-kubernetes",
|
||||
help="Collect sanitized readiness evidence from a live multi-host profile.",
|
||||
)
|
||||
_directory_argument(verify_kubernetes)
|
||||
verify_kubernetes.add_argument("--namespace", default="govoplan")
|
||||
verify_kubernetes.add_argument(
|
||||
"--ops-url",
|
||||
help="Ops status URL; defaults to <public-url>/api/v1/ops/status.",
|
||||
)
|
||||
verify_kubernetes.add_argument(
|
||||
"--api-key-env",
|
||||
default="GOVOPLAN_OPS_API_KEY",
|
||||
help="Environment variable containing an API key with Ops read scope.",
|
||||
)
|
||||
verify_kubernetes.add_argument(
|
||||
"--exercise-api-pod-loss",
|
||||
action="store_true",
|
||||
help="Delete one ready API pod and prove health-aware replacement.",
|
||||
)
|
||||
verify_kubernetes.add_argument(
|
||||
"--timeout-seconds",
|
||||
type=float,
|
||||
default=180.0,
|
||||
)
|
||||
verify_kubernetes.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
help="Private evidence output path.",
|
||||
)
|
||||
|
||||
operations = subparsers.add_parser(
|
||||
"operations",
|
||||
help="List durable deployment and recovery operations.",
|
||||
@@ -282,6 +314,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
return _status(args)
|
||||
if args.command == "render-kubernetes":
|
||||
return _render_kubernetes(args)
|
||||
if args.command == "verify-kubernetes":
|
||||
return _verify_kubernetes(args)
|
||||
if args.command == "operations":
|
||||
return _operations(args)
|
||||
if args.command == "recover":
|
||||
@@ -598,9 +632,7 @@ def _render_kubernetes(args: argparse.Namespace) -> int:
|
||||
output = (args.output or (paths.root / "kubernetes.json")).expanduser().resolve()
|
||||
atomic_write(output, canonical_json(manifest), mode=0o600)
|
||||
print(f"Wrote stateless Kubernetes runtime manifest to {output}")
|
||||
print(
|
||||
"Required Secret keys: " + ", ".join(kubernetes_secret_contract())
|
||||
)
|
||||
print("Required Secret keys: " + ", ".join(kubernetes_secret_contract()))
|
||||
print(
|
||||
write_secret_creation_hint(
|
||||
paths.env,
|
||||
@@ -611,6 +643,41 @@ def _render_kubernetes(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _verify_kubernetes(args: argparse.Namespace) -> int:
|
||||
paths = bundle_paths(args.directory)
|
||||
spec = load_spec(paths.spec)
|
||||
api_key = str(os.environ.get(args.api_key_env) or "").strip()
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
f"{args.api_key_env} must contain an API key with Ops read scope"
|
||||
)
|
||||
ops_url = args.ops_url or (spec.public_url.rstrip("/") + "/api/v1/ops/status")
|
||||
evidence = collect_kubernetes_evidence(
|
||||
installation_id=spec.installation_id,
|
||||
namespace=args.namespace,
|
||||
ops_url=ops_url,
|
||||
api_key=api_key,
|
||||
exercise_api_pod_loss=args.exercise_api_pod_loss,
|
||||
timeout_seconds=args.timeout_seconds,
|
||||
)
|
||||
timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
||||
output = (
|
||||
(args.output or paths.root / "evidence" / f"kubernetes-{timestamp}.json")
|
||||
.expanduser()
|
||||
.resolve()
|
||||
)
|
||||
ensure_private_directory(output.parent)
|
||||
atomic_write(output, canonical_json(evidence), mode=0o600)
|
||||
print(f"Wrote Kubernetes evidence to {output}")
|
||||
if evidence["result"]["state"] != "passed":
|
||||
print(
|
||||
"Failed checks: " + ", ".join(evidence["result"]["failed_checks"]),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _operations(args: argparse.Namespace) -> int:
|
||||
paths = bundle_paths(args.directory)
|
||||
payload = list_operations(paths)
|
||||
|
||||
Reference in New Issue
Block a user