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:
parent
ded49d4d90
commit
eda153c7a1
2 changed files with 118 additions and 4 deletions
|
|
@ -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")
|
||||
|
|
|
|||
91
tests/test_weekly_release_owner.py
Normal file
91
tests/test_weekly_release_owner.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue