#!/usr/bin/env python3 """CUST-WP-0071-T01: join live cluster allocation with owner/service/tenant.""" from __future__ import annotations import argparse import importlib.util import json import sys from datetime import UTC, datetime from pathlib import Path from typing import Any import yaml ROOT = Path(__file__).resolve().parents[1] CLUSTER_OBS = Path.home() / "railiance-cluster" / "tools" / "observe_cluster_resources.py" HEADROOM = Path.home() / "state-hub" / "scripts" / "release_headroom_preflight.py" OWNERS = ROOT / "docs" / "evidence" / "namespace-ownership.yaml" def load_module(path: Path, name: str): spec = importlib.util.spec_from_file_location(name, path) if spec is None or spec.loader is None: raise RuntimeError(f"cannot load {path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def join(observation: dict[str, Any], owners: dict[str, Any]) -> dict[str, Any]: mapping = owners.get("namespaces") or {} rows = [] unknown = [] zero_request = [] for workload in observation.get("utilization", {}).get("workloads") or []: ns = workload["namespace"] meta = mapping.get(ns, {"owner": "unknown", "service": "unknown", "tenant": "unknown"}) row = { "namespace": ns, "workload": workload["workload"], "pods": workload["pods"], "cpu_request_m": workload["requests"]["cpu_millicores"], "memory_request_bytes": workload["requests"]["memory_bytes"], "owner": meta.get("owner", "unknown"), "service": meta.get("service", "unknown"), "tenant": meta.get("tenant", "unknown"), } rows.append(row) if ns not in mapping: unknown.append(ns) if workload["requests"]["cpu_millicores"] == 0: zero_request.append(f"{ns}/{workload['workload']}") pending = observation.get("allocation", {}).get("pending_unscheduled") or {} alloc = observation.get("allocation") or {} capacity = observation.get("capacity", {}).get("totals") or {} residual = (alloc.get("residual_capacity") or {}).get("cpu_millicores", 0) requested = (alloc.get("denominators") or {}).get("cpu_requested_millicores", 0) return { "schema": "custodian.allocation-reconcile.v1", "workplan_id": "CUST-WP-0071-T01", "captured_at": observation.get("captured_at"), "cluster_observation_schema": observation.get("schema_version"), "count_host_and_cluster_cpus_once": True, "host": owners.get("host"), "capacity_cpu_m": capacity.get("cpu_millicores"), "scheduled_request_cpu_m": requested, "pending_unscheduled_cpu_m": pending.get("cpu_millicores", 0), "residual_cpu_m": residual, "not_a_scheduling_guarantee": True, "workloads": sorted(rows, key=lambda r: (-r["cpu_request_m"], r["namespace"], r["workload"])), "unknown_namespaces": sorted(set(unknown)), "zero_request_workloads": sorted(zero_request), "pending_unscheduled": pending.get("pods") or [], "measurement_gaps": observation.get("measurement_gaps") or [], "state_hub_preflight_observation": observation.get("state_hub_preflight_observation"), "signal_notes": [ "Host resource:hosteurope:railiance01 and cluster resource:railiance:reef-railiance:k3s are the same 4 vCPU; do not add them.", "Prometheus namespace_cpu:kube_pod_container_resource_requests:sum omitted Forgejo 100m on 2026-09-11; Kubernetes API is the scheduler-effective source.", "node-exporter was disabled; host CPU usage is not a reservation and is not treated as spare capacity.", "Zero-request pods are demand, not zero. Missing metrics are listed as gaps, not zeros.", "STATE-WP-0091 preflight consumes state_hub_preflight_observation; residual millicores are not admission.", ], } def render_markdown(report: dict[str, Any], preflight: dict[str, Any] | None) -> str: lines = [ f"# Railiance allocation reconcile — {report['captured_at']}", "", "CUST-WP-0071-T01. Scheduler-effective requests from the Kubernetes API", "via `railiance-cluster` observe (RCLUSTER-WP-0014 / RAIL-BS-WP-0014).", "Host and cluster are the same 4 vCPU and are counted once.", "", f"- Capacity: {report['capacity_cpu_m']}m", f"- Scheduled requests: {report['scheduled_request_cpu_m']}m", f"- Pending unscheduled: {report['pending_unscheduled_cpu_m']}m", f"- Residual (not a guarantee): {report['residual_cpu_m']}m", f"- Unknown namespaces: {', '.join(report['unknown_namespaces']) or 'none'}", f"- Zero-request workloads: {len(report['zero_request_workloads'])}", "", "| Namespace | Workload | Owner | Tenant | CPU request (m) | Pods |", "|---|---|---|---|---:|---:|", ] for row in report["workloads"]: lines.append( f"| {row['namespace']} | {row['workload']} | {row['owner']} | {row['tenant']} | {row['cpu_request_m']} | {row['pods']} |" ) lines += ["", "## STATE-WP-0091 preflight", ""] if preflight: lines.append(f"- ok: `{preflight.get('ok')}`") lines.append(f"- remaining_cpu_m: {preflight.get('remaining_cpu_m')}") for reason in preflight.get("reasons") or []: lines.append(f"- refuse: {reason}") for note in preflight.get("notes") or []: lines.append(f"- note: {note}") lines += ["", "## Signal notes", ""] for note in report["signal_notes"]: lines.append(f"- {note}") lines.append("") return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--observation", type=Path, help="Existing cluster observation JSON") parser.add_argument("--kubectl", default="kubectl") parser.add_argument("--output-dir", type=Path, default=ROOT / "docs" / "evidence") args = parser.parse_args() cluster = load_module(CLUSTER_OBS, "cluster_resources") if args.observation: observation = json.loads(args.observation.read_text()) else: observation = cluster.collect(args.kubectl) owners = yaml.safe_load(OWNERS.read_text()) report = join(observation, owners) preflight = None if observation.get("state_hub_preflight_observation"): headroom = load_module(HEADROOM, "release_headroom") preflight = headroom.evaluate(observation["state_hub_preflight_observation"]) report["state_hub_preflight"] = preflight stamp = datetime.now(UTC).strftime("%Y-%m-%d") out_json = args.output_dir / f"{stamp}-allocation-reconcile.json" out_md = args.output_dir / f"{stamp}-allocation-reconcile.md" args.output_dir.mkdir(parents=True, exist_ok=True) out_json.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") out_md.write_text(render_markdown(report, preflight), encoding="utf-8") print(out_json) print(out_md) return 0 if __name__ == "__main__": raise SystemExit(main())