Pass the composed release index into the weekly assessment.

The in-cluster runner and the request file both load config/release-index.json, so a claimed workload's recommendation names the unit id and the declaration owner.
This commit is contained in:
codex 2026-10-22 01:07:54 +02:00
parent c0227108d2
commit 82b2024727
5 changed files with 573 additions and 10 deletions

488
config/release-index.json Normal file
View file

@ -0,0 +1,488 @@
{
"schema": "release-registry.index.v1",
"units": [
{
"declaration": {
"composition": {
"member_repos": [
{
"deployables": [
"flex-auth-secrets-engine"
],
"repo": "access-engine",
"role": "application source"
}
],
"purpose": "Decides secrets-engine policy for one flex-auth consumer."
},
"contract_version": "1.0.0",
"criticality": "high",
"data_classification": "restricted",
"kind": "release-unit",
"owner": "access-engine",
"pins": {
"component": {
"name": "ocm.railiance.dev/flex-auth-secrets-engine",
"version": "0.2.0"
},
"resources": [
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd"
},
"name": "flex-auth",
"type": "ociImage",
"version": "0.2.0"
}
],
"sources": [
{
"digest": {
"hashAlgorithm": "SHA-1",
"normalisationAlgorithm": "gitCommit/v1",
"value": "0344b2a962e4ed977ea622b08cf20bf49d25b603"
},
"name": "access-engine",
"type": "git",
"version": "0.2.0"
}
]
},
"rollback_contract": {
"order": [
"helm-rollback"
]
},
"rollout_contract": {
"default_mode": "helm-upgrade-install"
},
"smoke_contract": {
"required": [
"http-ready"
]
},
"unit_id": "flex-auth-secrets-engine",
"workloads": [
{
"kind": "Deployment",
"name": "flex-auth-secrets-engine",
"namespace": "flex-auth"
}
]
},
"owner": "access-engine",
"path": "/home/worsch/access-engine/registry/release-units/flex-auth-secrets-engine.yaml",
"source": "dir:/home/worsch/access-engine",
"unit_id": "flex-auth-secrets-engine"
},
{
"declaration": {
"composition": {
"member_repos": [
{
"deployables": [
"forge",
"forgejo-runner"
],
"repo": "railiance-apps",
"role": "helm values and runner manifest"
},
{
"deployables": [
"forge",
"forgejo-runner"
],
"repo": "railiance-forge",
"role": "forge operating contracts"
}
],
"purpose": "Serves Forgejo git, packages, and the in-cluster Actions runner as one rollout."
},
"contract_version": "1.0.0",
"criticality": "critical",
"data_classification": "restricted",
"kind": "release-unit",
"owner": "railiance-apps",
"pins": {
"component": {
"name": "ocm.railiance.dev/forgejo",
"version": "11.0.3"
},
"resources": [
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba"
},
"name": "forgejo",
"type": "ociImage",
"version": "11.0.3"
},
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:5071e6832313bafe71577e05631bece88caff08fcfb372193e4a21941f4ed54b"
},
"name": "forgejo-runner",
"type": "ociImage",
"version": "11.0.3"
},
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:aa3df78ecf320f5fafdce71c659f1629e96e9de0968305fe1de670e0ca9176ce"
},
"name": "docker",
"type": "ociImage",
"version": "11.0.3"
}
],
"sources": [
{
"digest": {
"hashAlgorithm": "SHA-1",
"normalisationAlgorithm": "gitCommit/v1",
"value": "9ecb123451717a4d70ec50d490a228c30ea5ba88"
},
"name": "railiance-apps",
"type": "git",
"version": "11.0.3"
},
{
"digest": {
"hashAlgorithm": "SHA-1",
"normalisationAlgorithm": "gitCommit/v1",
"value": "2afdff68b1e22c48c4e7d5f20457f6cc420cb25c"
},
"name": "railiance-forge",
"type": "git",
"version": "11.0.3"
}
]
},
"rollback_contract": {
"order": [
"helm-rollback"
]
},
"rollout_contract": {
"default_mode": "helm-upgrade-install"
},
"smoke_contract": {
"required": [
"http-ready"
]
},
"unit_id": "forgejo",
"workloads": [
{
"kind": "Deployment",
"name": "forge",
"namespace": "forgejo"
},
{
"kind": "Deployment",
"name": "forgejo-runner",
"namespace": "forgejo"
}
]
},
"owner": "railiance-apps",
"path": "/home/worsch/railiance-apps/registry/release-units/forgejo.yaml",
"source": "dir:/home/worsch/railiance-apps",
"unit_id": "forgejo"
},
{
"declaration": {
"composition": {
"member_repos": [
{
"deployables": [
"knative"
],
"repo": "rail-knative",
"role": "substrate declaration"
},
{
"deployables": [
"knative"
],
"repo": "railiance-cluster",
"role": "cluster install"
}
],
"purpose": "Serves Knative workloads through the shared Serving and Kourier substrate.",
"upstream_components": [
{
"name": "knative-serving",
"source": "gcr.io/knative-releases",
"version": "1.22.0"
},
{
"name": "net-kourier",
"source": "gcr.io/knative-releases",
"version": "1.22.0"
}
]
},
"contract_version": "1.0.0",
"criticality": "critical",
"data_classification": "internal",
"kind": "release-unit",
"owner": "rail-knative",
"pins": {
"component": {
"name": "ocm.railiance.dev/knative",
"version": "1.22.0"
},
"resources": [
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:8f7e9df2642a8ba715ebc55b62abee17b0984b6b6af0c6ec00dabaadf5f8cccb"
},
"name": "activator",
"type": "ociImage",
"version": "1.22.0"
},
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:ec66d97244395b57a67923d4a8c5a95f0264805f1fdb2ee0cdb49ec0f3bd7855"
},
"name": "autoscaler",
"type": "ociImage",
"version": "1.22.0"
},
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:6833f2c83e9f356c274ddb2de40e5471ad2b1c08882c9bd11f5afa240e86d574"
},
"name": "controller",
"type": "ociImage",
"version": "1.22.0"
},
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:c505888f7ee34ccadc1b9a6877d6d49691f633ea28cf8cbe03fcfa6d3afcd584"
},
"name": "webhook",
"type": "ociImage",
"version": "1.22.0"
},
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:a2e8f568a4bf68ef6391bc3afea956d11a8698f6a22c4be50844b1da4f8bcc74"
},
"name": "net-kourier-controller",
"type": "ociImage",
"version": "1.22.0"
},
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:1c2b79776c6e3b38e8b0113b825e6a599f9bfc08d680c199d80bf8964856c529"
},
"name": "kourier-gateway",
"type": "ociImage",
"version": "1.22.0"
}
],
"sources": [
{
"digest": {
"hashAlgorithm": "SHA-1",
"normalisationAlgorithm": "gitCommit/v1",
"value": "affe67a4bc0fbdde9460edd8208cb8f895a23bbd"
},
"name": "rail-knative",
"type": "git",
"version": "1.22.0"
},
{
"digest": {
"hashAlgorithm": "SHA-1",
"normalisationAlgorithm": "gitCommit/v1",
"value": "a6742635824a7568cdae4aebdd743a6457c20e33"
},
"name": "railiance-cluster",
"type": "git",
"version": "1.22.0"
}
]
},
"rollback_contract": {
"order": [
"restore-previous-overlay"
]
},
"rollout_contract": {
"default_mode": "kustomize-overlay"
},
"smoke_contract": {
"required": [
"deployment-available"
]
},
"unit_id": "knative",
"workloads": [
{
"kind": "Deployment",
"name": "activator",
"namespace": "knative-serving"
},
{
"kind": "Deployment",
"name": "autoscaler",
"namespace": "knative-serving"
},
{
"kind": "Deployment",
"name": "controller",
"namespace": "knative-serving"
},
{
"kind": "Deployment",
"name": "webhook",
"namespace": "knative-serving"
},
{
"kind": "Deployment",
"name": "net-kourier-controller",
"namespace": "knative-serving"
},
{
"kind": "Deployment",
"name": "3scale-kourier-gateway",
"namespace": "kourier-system"
}
]
},
"owner": "rail-knative",
"path": "/home/worsch/rail-knative/registry/release-units/knative.yaml",
"source": "dir:/home/worsch/rail-knative",
"unit_id": "knative"
},
{
"declaration": {
"composition": {
"member_repos": [
{
"deployables": [
"vergabe"
],
"repo": "railiance-apps",
"role": "rollout owner"
},
{
"deployables": [
"vergabe-teilnahme"
],
"repo": "vergabe-teilnahme",
"role": "application source"
}
],
"purpose": "Serves the Vergabe participation application for one company."
},
"contract_version": "1.0.0",
"criticality": "high",
"data_classification": "confidential",
"depends_on": [
{
"readiness": "available",
"unit_id": "vergabe-db",
"version": "1.0.0"
}
],
"kind": "release-unit",
"owner": "railiance-apps",
"pins": {
"component": {
"name": "ocm.railiance.dev/vergabe",
"version": "0.2.1"
},
"references": [
{
"componentName": "vergabe-db",
"name": "database",
"version": "1.0.0"
}
],
"resources": [
{
"digest": {
"hashAlgorithm": "SHA-256",
"normalisationAlgorithm": "ociArtifactDigest/v1",
"value": "sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91"
},
"name": "vergabe-teilnahme",
"type": "ociImage",
"version": "0.2.1"
}
],
"sources": [
{
"digest": {
"hashAlgorithm": "SHA-1",
"normalisationAlgorithm": "gitCommit/v1",
"value": "e9d2b5f39a6fe1c4accb5c54e3ba0b37d05b23ee"
},
"name": "railiance-apps",
"type": "git",
"version": "0.2.1"
},
{
"digest": {
"hashAlgorithm": "SHA-1",
"normalisationAlgorithm": "gitCommit/v1",
"value": "dca6683a8ab7d4aa0de2a537924f73318b199f60"
},
"name": "vergabe-teilnahme",
"type": "git",
"version": "0.2.1"
}
]
},
"rollback_contract": {
"order": [
"helm-rollback"
]
},
"rollout_contract": {
"default_mode": "helm-upgrade-install"
},
"smoke_contract": {
"required": [
"http-ready"
]
},
"unit_id": "vergabe",
"workloads": [
{
"kind": "Deployment",
"name": "vergabe-teilnahme",
"namespace": "vergabe-demo-company"
}
]
},
"owner": "railiance-apps",
"path": "/home/worsch/railiance-apps/registry/release-units/vergabe.yaml",
"source": "dir:/home/worsch/railiance-apps",
"unit_id": "vergabe"
}
]
}

View file

@ -8,6 +8,7 @@
"provenance": null,
"previous": null,
"reviews": null,
"release_index": "release-index.json",
"as_of": null,
"output": null
}

View file

@ -239,6 +239,16 @@ def run(args, *, now: datetime, get, query) -> dict:
"weekly_allocation_incluster": Path(__file__).resolve(),
"queries": args.queries, "ownership": args.ownership, "inventory": args.inventory,
}
release_index_path = getattr(args, "release_index", None)
if release_index_path is None:
raise ValueError("release index must be bound")
release_index_path = Path(release_index_path)
sources["release_index"] = release_index_path
release_index = json.loads(release_index_path.read_text())
if not isinstance(release_index, dict) or (
"units" not in release_index and "findings" not in release_index
):
raise ValueError("release index is not a composed index or coverage report")
weekly = load(sources["weekly_allocation_report"], "weekly_allocation_report")
reconcile = load(sources["reconcile_railiance_allocation"], "reconcile_railiance_allocation")
observer = load(sources["observe_cluster_resources"], "observe_cluster_resources")
@ -280,7 +290,8 @@ def run(args, *, now: datetime, get, query) -> dict:
report = weekly.build_report(
**inputs, as_of=iso(now), max_age_hours=args.max_age_hours,
previous=json.loads(previous_path.read_text()) if previous_path else None,
reviews=json.loads(args.reviews.read_text()) if args.reviews and args.reviews.exists() else None)
reviews=json.loads(args.reviews.read_text()) if args.reviews and args.reviews.exists() else None,
release_index=release_index)
weekly.retain(target, report)
failed = sorted(n for n, q in telemetry["queries"].items() if q["response"].get("status") != "success")
return {**receipt, "status": "retained", "report_sha256": sha256(target),
@ -302,6 +313,8 @@ def parser() -> argparse.ArgumentParser:
p.add_argument("--ownership", type=Path, default=env("ASSESSMENT_OWNERSHIP") and Path(env("ASSESSMENT_OWNERSHIP")))
p.add_argument("--inventory", type=Path, default=env("ASSESSMENT_INVENTORY") and Path(env("ASSESSMENT_INVENTORY")))
p.add_argument("--reviews", type=Path, default=env("ASSESSMENT_REVIEWS") and Path(env("ASSESSMENT_REVIEWS")))
p.add_argument("--release-index", type=Path, default=Path(env(
"ASSESSMENT_RELEASE_INDEX", str(HERE.parent / "config/release-index.json"))))
p.add_argument("--prometheus-url", default=env("PROMETHEUS_URL", "http://telemetry-prometheus.telemetry.svc:9090"))
p.add_argument("--hub-url", default=env("STATE_HUB_URL"), help="State Hub API; unset disables delivery")
p.add_argument("--executor", default=env("ASSESSMENT_EXECUTOR", "unspecified"))

View file

@ -10,7 +10,6 @@ import math
import os
from pathlib import Path
import re
import sys
import tempfile
SCHEMA = "custodian.weekly-allocation.v1"
@ -95,14 +94,35 @@ def recommendation_id(resource, namespace, workload):
def release_lookup(index, *, namespace, name, label=None):
"""Call the release-registry claim lookup. A missing index assigns nothing."""
if not index:
"""Unit id and declaration owner for one workload.
A composed index is the authority: one claim on namespace and name.
``label`` is ``railiance.io/release-unit`` and is not used. A coverage
report has no declaration owner, so findings alone assign neither.
The same rule lives in release_registry.ownership.lookup_release. This
copy stays in the assessment so the slim in-cluster image can run it.
"""
del label
matches = []
for unit in (index or {}).get("units") or []:
if not isinstance(unit, dict):
continue
declaration = unit.get("declaration") or {}
if not isinstance(declaration, dict):
continue
unit_id = str(unit.get("unit_id") or declaration.get("unit_id") or "")
owner = declaration.get("owner")
if not unit_id or not isinstance(owner, str) or not owner:
continue
for claim in declaration.get("workloads") or []:
if not isinstance(claim, dict):
continue
if str(claim.get("namespace") or "") == namespace and str(claim.get("name") or "") == name:
matches.append((unit_id, owner))
if len(matches) != 1:
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)
unit_id, owner = matches[0]
return {"unit_id": unit_id, "owner": owner}
def build_report(cluster, allocation, telemetry, inventory, provenance, *, as_of,

View file

@ -51,7 +51,9 @@ class InClusterRunnerTests(unittest.TestCase):
self.args = argparse.Namespace(
data_dir=self.data, source_dir=ROOT / "scripts", observer=base / "observer.py",
queries=ROOT / "config/weekly-allocation-queries.json", ownership=base / "ownership.json",
inventory=base / "inventory.json", reviews=None, prometheus_url="http://prom.test:9090",
inventory=base / "inventory.json", reviews=None,
release_index=ROOT / "config/release-index.json",
prometheus_url="http://prom.test:9090",
executor="test", max_age_hours=48.0)
def tearDown(self):
@ -89,6 +91,45 @@ class InClusterRunnerTests(unittest.TestCase):
self.assertEqual(incluster.missed_weeks("2026-W52", "2027-W01"), ["2026-W53"]) # 2026 has 53 ISO weeks
self.assertEqual(incluster.missed_weeks("2026-W53", "2027-W01"), [])
def test_claimed_workload_names_unit_and_declaration_owner(self):
observer = (Path(self.tmp.name) / "observer.py").read_text()
(Path(self.tmp.name) / "observer.py").write_text(observer.replace(
'"workload": p["metadata"]["labels"]["app"]',
'"workload": p["metadata"]["labels"]["app"], "labels": p["metadata"].get("labels") or {}',
))
self.get = self._claimed_get
(Path(self.tmp.name) / "ownership.json").write_text(json.dumps({
"host": {"cluster_resource_id": RESOURCE},
"namespaces": {"flex-auth": {"owner": "namespace-map", "service": "flex-auth", "tenant": "unknown"}}}))
receipt = self.run_at(datetime(2026, 10, 19, 6, 0, 30, tzinfo=timezone.utc))
report = json.loads((self.data / "reports/2026-W43.json").read_text())
named = {item["workload"]: item for item in report["recommendations"]}
claimed = named["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.assertIsNone(named["unclaimed"]["release_unit"])
self.assertIsNone(named["labeled"]["release_unit"])
self.assertIsNone(named["labeled"]["release_owner"])
self.assertEqual(named["labeled"]["owner"], "namespace-map")
self.assertEqual(receipt["status"], "retained")
def _claimed_get(self, path):
self.reads.append(path)
if path.startswith("/apis/metrics.k8s.io"):
if path.endswith("/nodes"):
return {"items": [{"metadata": {"name": "railiance01"}, "usage": {"cpu": "1500000000n", "memory": "2048Ki"}}]}
return {"items": []}
if path == "/api/v1/pods":
labeled = pod("flex-auth", "labeled-1", "labeled")
labeled["metadata"]["labels"]["railiance.io/release-unit"] = "flex-auth-secrets-engine"
return {"items": [
pod("flex-auth", "flex-auth-secrets-engine-1", "flex-auth-secrets-engine"),
pod("flex-auth", "other-1", "unclaimed"),
labeled,
]}
return {"items": []}
def test_reads_only_collector_paths_and_retains_report(self):
receipt = self.run_at(datetime(2026, 10, 19, 6, 0, 30, tzinfo=timezone.utc))
self.assertEqual(receipt["status"], "retained")