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")

View file

@ -0,0 +1,91 @@
"""Release-unit names on the weekly allocation recommendation."""
import importlib.util
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("weekly", ROOT / "scripts/weekly_allocation_report.py")
weekly = importlib.util.module_from_spec(spec)
spec.loader.exec_module(weekly)
END = "2026-10-19T06:00:00+00:00"
RESOURCE = "resource:railiance:reef-railiance:k3s"
INDEX = {
"schema": "release-registry.index.v1",
"units": [
{
"unit_id": "flex-auth-secrets-engine",
"declaration": {
"unit_id": "flex-auth-secrets-engine",
"owner": "access-engine",
"workloads": [
{"kind": "Deployment", "namespace": "flex-auth", "name": "flex-auth-secrets-engine"}
],
},
}
],
}
def _workload(namespace, name, labels=None):
item = {
"namespace": namespace, "workload": name, "pods": 1,
"requests": {"cpu_millicores": 60, "memory_bytes": 256},
"limits": {"cpu_millicores": 1000, "memory_bytes": 1024},
}
if labels:
item["labels"] = labels
return item
class WeeklyReleaseOwnerTests(unittest.TestCase):
def report(self):
cluster = {
"schema_version": "railiance.cluster-resource-observation.v1",
"resource_id": RESOURCE, "captured_at": END,
"utilization": {"workloads": [
_workload("flex-auth", "flex-auth-secrets-engine"),
_workload("flex-auth", "flex-auth-tenant-engine"),
_workload("forgejo", "forge", {"railiance.io/release-unit": "flex-auth-secrets-engine"}),
]},
}
allocation = {
"schema": "custodian.allocation-reconcile.v1", "captured_at": END,
"host": {"cluster_resource_id": RESOURCE}, "count_host_and_cluster_cpus_once": True,
"workloads": [
{"namespace": "flex-auth", "workload": "flex-auth-secrets-engine", "owner": "namespace-map"},
{"namespace": "flex-auth", "workload": "flex-auth-tenant-engine", "owner": "namespace-map"},
{"namespace": "forgejo", "workload": "forge", "owner": "namespace-map"},
],
}
return weekly.build_report(
cluster, allocation,
{"captured_at": END, "window": "7d", "step": "5m", "queries": {}},
{"id": RESOURCE},
{"source_observation": END, "revisions": {"collector": "a" * 40}},
as_of=END, release_index=INDEX,
)
def by_name(self, report):
return {item["workload"]: item for item in report["recommendations"]}
def test_claim_label_and_gap_name_unit_and_owner(self):
first = self.by_name(self.report())
second = self.by_name(self.report())
claimed = first["flex-auth-secrets-engine"]
self.assertEqual(claimed["release_unit"], "flex-auth-secrets-engine")
self.assertEqual(claimed["owner"], "access-engine")
self.assertEqual(claimed["release_owner"], "access-engine")
self.assertEqual(second["flex-auth-secrets-engine"]["release_unit"], claimed["release_unit"])
self.assertEqual(second["flex-auth-secrets-engine"]["owner"], claimed["owner"])
unclaimed = first["flex-auth-tenant-engine"]
self.assertIsNone(unclaimed["release_unit"])
self.assertIsNone(unclaimed["release_owner"])
labeled = first["forge"]
self.assertIsNone(labeled["release_unit"])
self.assertIsNone(labeled["release_owner"])
self.assertNotEqual(labeled["owner"], "flex-auth-secrets-engine")
if __name__ == "__main__":
unittest.main()