feat: add bounded Kubernetes runtime verification
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 8s

This commit is contained in:
2026-08-02 05:30:26 +02:00
parent 50e9607e72
commit ed31409034
6 changed files with 1094 additions and 11 deletions
+70 -3
View File
@@ -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)