Name the release unit and its owner on a weekly recommendation.

build_report calls the claim lookup. A workload with no claim, and a label with no claim, receive neither a unit id nor a declaration owner.
This commit is contained in:
codex 2026-10-22 00:56:09 +02:00
parent ded49d4d90
commit eda153c7a1
2 changed files with 118 additions and 4 deletions

View file

@ -10,6 +10,7 @@ import math
import os
from pathlib import Path
import re
import sys
import tempfile
SCHEMA = "custodian.weekly-allocation.v1"
@ -93,8 +94,19 @@ def recommendation_id(resource, namespace, workload):
return "allocation-" + digest([resource, namespace, workload, "sizing-review"])[:24]
def release_lookup(index, *, namespace, name, label=None):
"""Call the release-registry claim lookup. A missing index assigns nothing."""
if not index:
return {"unit_id": None, "owner": None}
registry = Path(__file__).resolve().parents[1].parent / "release-registry"
if str(registry) not in sys.path:
sys.path.insert(0, str(registry))
from release_registry.ownership import lookup_release
return lookup_release(index, namespace=namespace, name=name, label=label)
def build_report(cluster, allocation, telemetry, inventory, provenance, *, as_of,
previous=None, reviews=None, max_age_hours=48):
previous=None, reviews=None, max_age_hours=48, release_index=None):
now = timestamp(as_of)
if cluster.get("schema_version") != "railiance.cluster-resource-observation.v1":
raise ValueError("unsupported cluster schema")
@ -180,8 +192,14 @@ def build_report(cluster, allocation, telemetry, inventory, provenance, *, as_of
reasons.append("owner proposal retained; incomplete evidence prevents accepted sizing recommendation")
else:
reasons.append("no owner-reviewed sizing decision")
labels = workload.get("labels") or {}
identity = release_lookup(
release_index, namespace=key[0], name=key[1],
label=labels.get("railiance.io/release-unit"),
)
row = {
"namespace": key[0], "workload": key[1], "owner": owner.get("owner", "unknown"),
"release_unit": identity["unit_id"], "release_owner": identity["owner"],
"service": owner.get("service", "unknown"), "tenant": owner.get("tenant", "unknown"),
"pods": workload.get("pods"), "requests": workload.get("requests"),
"limits": workload.get("limits"), "instantaneous_observed": workload.get("observed"),
@ -218,8 +236,11 @@ def build_report(cluster, allocation, telemetry, inventory, provenance, *, as_of
row["comparison_reasons"][metric] = reason
rows.append(row)
prior_recommendation = next((r for r in (previous or {}).get("recommendations", []) if r["id"] == rid), {})
named_owner = identity["owner"] if identity["unit_id"] else row["owner"]
recommendations.append({
"id": rid, "decision": decision, "confidence": confidence, "owner": row["owner"],
"id": rid, "namespace": key[0], "workload": key[1],
"decision": decision, "confidence": confidence, "owner": named_owner,
"release_unit": identity["unit_id"], "release_owner": identity["owner"],
"reasons": reasons, "evidence_refs": evidence, "owner_proposal": review,
"receiving_task": review.get("receiving_task") if review else prior_recommendation.get("receiving_task"),
"previous_decision": prior_recommendation.get("decision"),
@ -287,6 +308,7 @@ def main():
parser.add_argument("--" + name, type=Path)
parser.add_argument("--previous", type=Path)
parser.add_argument("--reviews", type=Path)
parser.add_argument("--release-index", type=Path)
parser.add_argument("--as-of")
parser.add_argument("--max-age-hours", type=float, default=48)
parser.add_argument("--output", type=Path)
@ -298,7 +320,7 @@ def main():
raise ValueError("unsupported executor request schema")
if any(getattr(args, key) for key in ("cluster", "allocation", "telemetry", "inventory", "provenance", "previous", "reviews", "as_of", "output")):
raise ValueError("request cannot be mixed with input/output overrides")
for key in ("cluster", "allocation", "telemetry", "inventory", "provenance", "previous", "reviews", "output"):
for key in ("cluster", "allocation", "telemetry", "inventory", "provenance", "previous", "reviews", "release_index", "output"):
value = request.get(key)
setattr(args, key, args.request.parent / value if value else None)
args.as_of = request.get("as_of")
@ -309,7 +331,8 @@ def main():
inputs = {name: json.loads(getattr(args, name).read_text()) for name in ("cluster", "allocation", "telemetry", "inventory", "provenance")}
report = build_report(**inputs, as_of=args.as_of, max_age_hours=args.max_age_hours,
previous=json.loads(args.previous.read_text()) if args.previous else None,
reviews=json.loads(args.reviews.read_text()) if args.reviews else None)
reviews=json.loads(args.reviews.read_text()) if args.reviews else None,
release_index=json.loads(args.release_index.read_text()) if args.release_index else None)
retain(args.output, report)
except (OSError, ValueError, KeyError, TypeError) as exc:
parser.exit(2, f"weekly allocation report refused: {exc}\n")