rail-kubernetes/tools/validate_contracts.py

118 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Validate Railiance rail and workload contracts without reading credentials."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
import yaml
from jsonschema import Draft202012Validator, RefResolver
READINESS_STATES = {
"declared",
"installed",
"verified",
"production-approved",
"deprecated",
}
def load(path: Path) -> Any:
with path.open(encoding="utf-8") as handle:
if path.suffix in {".yaml", ".yml"}:
return yaml.safe_load(handle)
return json.load(handle)
def validate_schema(instance_path: Path, schema_path: Path) -> list[str]:
instance = load(instance_path)
schema = load(schema_path)
store: dict[str, Any] = {}
for candidate in schema_path.parent.glob("*.json"):
candidate_schema = load(candidate)
store[candidate.resolve().as_uri()] = candidate_schema
if schema_id := candidate_schema.get("$id"):
store[schema_id] = candidate_schema
resolver = RefResolver(
base_uri=schema_path.parent.resolve().as_uri() + "/",
referrer=schema,
store=store,
)
validator = Draft202012Validator(schema, resolver=resolver)
return [
f"{instance_path}: {'/'.join(str(part) for part in error.path) or '<root>'}: {error.message}"
for error in sorted(validator.iter_errors(instance), key=lambda item: list(item.path))
]
def validate_rail(path: Path) -> list[str]:
data = load(path)
errors: list[str] = []
required = {
"rail_id",
"ownership_repo",
"contract_version",
"composition_kind",
"execution_architecture",
"required_substrate_capabilities",
"readiness_state",
}
for field in sorted(required - set(data or {})):
errors.append(f"{path}: missing required field {field}")
if not isinstance(data, dict):
return [f"{path}: declaration must be an object"]
if data.get("readiness_state") not in READINESS_STATES:
errors.append(f"{path}: invalid readiness_state {data.get('readiness_state')!r}")
kind = data.get("composition_kind")
if kind not in {"base", "derived"}:
errors.append(f"{path}: composition_kind must be base or derived")
if kind == "derived":
for field in (
"base_rail",
"base_rail_contract",
"inherited_semantics",
"overridden_semantics",
"compatibility_constraints",
):
if field not in data:
errors.append(f"{path}: derived rail missing {field}")
if data.get("base_rail") == data.get("rail_id"):
errors.append(f"{path}: derived rail cannot inherit from itself")
return errors
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--rail", action="append", default=[], type=Path)
parser.add_argument("--workload", action="append", default=[], type=Path)
parser.add_argument(
"--schema-dir",
type=Path,
default=Path(__file__).resolve().parents[1] / "schemas",
)
args = parser.parse_args()
errors: list[str] = []
for path in args.rail:
errors.extend(validate_rail(path))
for path in args.workload:
errors.extend(
validate_schema(path, args.schema_dir / "common-workload.schema.json")
)
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
print(
f"validated {len(args.rail)} rail declaration(s) and "
f"{len(args.workload)} workload declaration(s)"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())