repo-manager/src/repo_manager/workloads.py
tegwick b36b68bc57 feat(workloads): define authoritative reference contract
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
2026-08-22 14:03:07 +02:00

238 lines
7.9 KiB
Python

"""Read-only index and resolver for authoritative rapp workload declarations."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
import yaml
REFERENCE_CONTRACT = "helixforge.workload-reference/v1"
@dataclass(frozen=True)
class WorkloadRecord:
rapp_id: str
name: str
declaration_repo: str
declaration_path: str
ownership_repo: str | None
readiness_state: str | None
data_classification: str | None
criticality: str | None
deployables: tuple[str, ...]
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["deployables"] = list(self.deployables)
data["reference"] = {"rapp_id": self.rapp_id, "name": self.name}
return data
def _error(code: str, path: Path, message: str) -> dict[str, str]:
return {"code": code, "path": str(path), "message": message}
def _package_dirs(root: Path) -> list[Path]:
root = root.expanduser().resolve()
if root.name.startswith("rapp-"):
return [root]
return sorted(path for path in root.glob("rapp-*") if path.is_dir())
def _text(value: Any) -> str | None:
if not isinstance(value, str):
return None
stripped = value.strip()
return stripped or None
def index_workloads(root: Path) -> dict[str, Any]:
"""Index authoritative declarations without inventing missing identity."""
root = root.expanduser().resolve()
records: list[WorkloadRecord] = []
errors: list[dict[str, str]] = []
for package_dir in _package_dirs(root):
declaration = package_dir / "declarations" / "rapp.yaml"
if not declaration.is_file():
errors.append(
_error(
"declaration_missing",
declaration,
"rapp repository has no authoritative declarations/rapp.yaml",
)
)
continue
try:
raw = yaml.safe_load(declaration.read_text(encoding="utf-8")) or {}
except (OSError, yaml.YAMLError) as exc:
errors.append(_error("declaration_invalid", declaration, str(exc)))
continue
if not isinstance(raw, dict):
errors.append(
_error("declaration_invalid", declaration, "declaration root must be a mapping")
)
continue
if raw.get("kind") != "managed-workload-package":
errors.append(
_error(
"declaration_kind_invalid",
declaration,
"kind must be managed-workload-package",
)
)
continue
rapp_id = _text(raw.get("rapp_id"))
identity = raw.get("workload_identity")
name = _text(identity.get("name")) if isinstance(identity, dict) else None
if not rapp_id or not name:
errors.append(
_error(
"identity_missing",
declaration,
"rapp_id and workload_identity.name are required",
)
)
continue
if rapp_id != package_dir.name:
errors.append(
_error(
"rapp_repo_mismatch",
declaration,
f"rapp_id {rapp_id!r} does not match declaration repo {package_dir.name!r}",
)
)
continue
deployables: list[str] = []
composition = raw.get("composition")
members = composition.get("member_repos", []) if isinstance(composition, dict) else []
if not isinstance(members, list):
errors.append(
_error(
"composition_invalid",
declaration,
"composition.member_repos must be a list",
)
)
continue
for member in members:
if not isinstance(member, dict) or not isinstance(member.get("deployables"), list):
errors.append(
_error(
"composition_invalid",
declaration,
"every composition member must declare a deployables list",
)
)
continue
deployables.extend(
value for value in (_text(item) for item in member["deployables"]) if value
)
if not deployables:
errors.append(
_error(
"deployables_missing",
declaration,
"the workload declaration has no running deployables",
)
)
continue
records.append(
WorkloadRecord(
rapp_id=rapp_id,
name=name,
declaration_repo=package_dir.name,
declaration_path=str(declaration.relative_to(root)),
ownership_repo=_text(raw.get("ownership_repo")),
readiness_state=_text(raw.get("readiness_state")),
data_classification=_text(raw.get("data_classification")),
criticality=_text(raw.get("criticality")),
deployables=tuple(sorted(set(deployables))),
)
)
references: dict[tuple[str, str], list[WorkloadRecord]] = {}
deployable_owners: dict[str, list[WorkloadRecord]] = {}
for record in records:
references.setdefault((record.rapp_id, record.name), []).append(record)
for deployable in record.deployables:
deployable_owners.setdefault(deployable, []).append(record)
for reference, owners in sorted(references.items()):
if len(owners) > 1:
errors.append(
_error(
"reference_duplicate",
root,
f"workload reference {reference!r} resolves to {len(owners)} declarations",
)
)
for deployable, owners in sorted(deployable_owners.items()):
if len(owners) > 1:
errors.append(
_error(
"deployable_duplicate",
root,
f"deployable {deployable!r} belongs to more than one rapp: "
+ ", ".join(sorted(owner.rapp_id for owner in owners)),
)
)
return {
"ok": not errors,
"contract": REFERENCE_CONTRACT,
"root": str(root),
"declaration_count": len(records),
"workloads": [record.to_dict() for record in sorted(records, key=lambda r: r.rapp_id)],
"errors": errors,
}
def resolve_workload(
root: Path,
*,
rapp_id: str,
name: str,
deployable: str | None = None,
) -> dict[str, Any]:
"""Resolve an explicit reference, returning unknown instead of guessing."""
index = index_workloads(root)
matches = [
record
for record in index["workloads"]
if record["rapp_id"] == rapp_id and record["name"] == name
]
reference: dict[str, str] = {"rapp_id": rapp_id, "name": name}
if deployable:
reference["deployable"] = deployable
if len(matches) != 1:
return {
"ok": True,
"contract": REFERENCE_CONTRACT,
"status": "unknown",
"reference": reference,
"reason": "not_found" if not matches else "ambiguous",
"index_errors": index["errors"],
}
workload = matches[0]
if deployable and deployable not in workload["deployables"]:
return {
"ok": True,
"contract": REFERENCE_CONTRACT,
"status": "unknown",
"reference": reference,
"reason": "deployable_not_declared",
"index_errors": index["errors"],
}
return {
"ok": True,
"contract": REFERENCE_CONTRACT,
"status": "resolved",
"reference": reference,
"workload": workload,
"index_errors": index["errors"],
}