feat(workloads): define authoritative reference contract
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
parent
fa1f272ea4
commit
b36b68bc57
8 changed files with 705 additions and 0 deletions
|
|
@ -365,6 +365,23 @@ def main(argv: list[str] | None = None) -> int:
|
|||
p_cache_export.add_argument("--output", required=True)
|
||||
p_cache_export.add_argument("--force", action="store_true")
|
||||
|
||||
p_workload = sub.add_parser(
|
||||
"workload",
|
||||
help="Index or resolve authoritative rapp workload declarations",
|
||||
)
|
||||
workload_sub = p_workload.add_subparsers(dest="workload_command")
|
||||
p_workload_index = workload_sub.add_parser(
|
||||
"index", help="Index workload identities from rapp declarations"
|
||||
)
|
||||
p_workload_index.add_argument("--root", default=".", help="Fleet root or one rapp repo")
|
||||
p_workload_resolve = workload_sub.add_parser(
|
||||
"resolve", help="Resolve one explicit workload reference without inference"
|
||||
)
|
||||
p_workload_resolve.add_argument("--root", default=".", help="Fleet root or one rapp repo")
|
||||
p_workload_resolve.add_argument("--rapp-id", required=True)
|
||||
p_workload_resolve.add_argument("--name", required=True)
|
||||
p_workload_resolve.add_argument("--deployable", default=None)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.version or args.command in (None, "version"):
|
||||
|
|
@ -812,6 +829,24 @@ def main(argv: list[str] | None = None) -> int:
|
|||
print(json.dumps(result, indent=2))
|
||||
return 0 if result.get("ok") else 1
|
||||
|
||||
if args.command == "workload":
|
||||
if not args.workload_command:
|
||||
p_workload.print_help()
|
||||
return 2
|
||||
from repo_manager.workloads import index_workloads, resolve_workload
|
||||
|
||||
if args.workload_command == "index":
|
||||
result = index_workloads(Path(args.root))
|
||||
else:
|
||||
result = resolve_workload(
|
||||
Path(args.root),
|
||||
rapp_id=args.rapp_id,
|
||||
name=args.name,
|
||||
deployable=args.deployable,
|
||||
)
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0 if result.get("ok") else 1
|
||||
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
|
|
|
|||
238
src/repo_manager/workloads.py
Normal file
238
src/repo_manager/workloads.py
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
"""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"],
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue