generated from coulomb/repo-seed
199 lines
7.1 KiB
Python
199 lines
7.1 KiB
Python
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
|
|
TOOL_PATH = Path(__file__).resolve().parents[1] / "playbook_contract_validator.py"
|
|
SPEC = importlib.util.spec_from_file_location("playbook_contract_validator", TOOL_PATH)
|
|
validator = importlib.util.module_from_spec(SPEC)
|
|
assert SPEC.loader is not None
|
|
sys.modules[SPEC.name] = validator
|
|
SPEC.loader.exec_module(validator)
|
|
|
|
|
|
def valid_declaration() -> dict:
|
|
return {
|
|
"apiVersion": validator.API_VERSION,
|
|
"kind": validator.KIND,
|
|
"metadata": {
|
|
"id": "railiance-infra.bootstrap-host",
|
|
"name": "Railiance S1 host bootstrap",
|
|
"owner": "railiance-infra",
|
|
"repo": "railiance-infra",
|
|
"domain": "railiance",
|
|
"contract_version": "0.1",
|
|
},
|
|
"spec": {
|
|
"playbook": {
|
|
"path": "ansible/playbooks/bootstrap.yaml",
|
|
"type": "ansible",
|
|
"invocation": "make converge",
|
|
"description": "Converges the S1 host baseline.",
|
|
},
|
|
"capabilities": [
|
|
{
|
|
"id": "s1.os-baseline",
|
|
"tier": "S1",
|
|
"resource_kinds": ["infrastructure_resources", "secrets_credentials"],
|
|
"description": "OS baseline and bootstrap secret handling.",
|
|
}
|
|
],
|
|
"parameters": [
|
|
{
|
|
"name": "target_hosts",
|
|
"type": "array",
|
|
"required": True,
|
|
"constraints": {"min_items": 1},
|
|
"sensitivity": "operational",
|
|
"tuning_authority": "netkingdom_tunable",
|
|
"description": "Inventory hosts to converge.",
|
|
},
|
|
{
|
|
"name": "swapfile_size_mb",
|
|
"type": "integer",
|
|
"required": False,
|
|
"default": 4096,
|
|
"constraints": {"minimum": 0, "maximum": 65536},
|
|
"sensitivity": "operational",
|
|
"tuning_authority": "netkingdom_tunable",
|
|
"description": "Swap file size.",
|
|
},
|
|
{
|
|
"name": "wireguard_enabled",
|
|
"type": "boolean",
|
|
"required": False,
|
|
"default": False,
|
|
"sensitivity": "security_sensitive",
|
|
"tuning_authority": "platform_only",
|
|
"description": "Enable WireGuard role.",
|
|
},
|
|
],
|
|
"responsibilities": [
|
|
{
|
|
"resource_kind": "infrastructure_resources",
|
|
"owner": "railiance-infra",
|
|
"resources": ["server:target_hosts", "os-baseline"],
|
|
"repo_owns": "Ansible convergence mechanics.",
|
|
"netkingdom_orchestrates": "Whether S1 is selected for the scenario.",
|
|
}
|
|
],
|
|
"trust": {
|
|
"requires": [],
|
|
"satisfies": [
|
|
{
|
|
"state": "bare_host_trust",
|
|
"readiness_checks": [
|
|
{
|
|
"id": "os-baseline-converged",
|
|
"description": "Ansible baseline converged.",
|
|
"evidence": "bootstrap playbook completed",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
},
|
|
"catalog": {
|
|
"publish": "capabilities/playbooks/railiance-infra.bootstrap-host.yaml",
|
|
"maturity": "reference",
|
|
"consumers": ["netkingdom-meta-orchestration"],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def declaration_from(data: dict, tmp_path: Path) -> validator.Declaration:
|
|
path = tmp_path / "declaration.yaml"
|
|
path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
|
return validator.Declaration(path=path, data=data)
|
|
|
|
|
|
def error_messages(issues):
|
|
return [item.message for item in issues if item.level == "ERROR"]
|
|
|
|
|
|
def test_valid_declaration_passes(tmp_path):
|
|
declaration = declaration_from(valid_declaration(), tmp_path)
|
|
|
|
issues = validator.validate_declaration(declaration)
|
|
|
|
assert error_messages(issues) == []
|
|
|
|
|
|
def test_unknown_capability_fails(tmp_path):
|
|
data = valid_declaration()
|
|
data["spec"]["capabilities"][0]["id"] = "s9.magic"
|
|
declaration = declaration_from(data, tmp_path)
|
|
|
|
issues = validator.validate_declaration(declaration)
|
|
|
|
assert any("unknown capability id" in msg for msg in error_messages(issues))
|
|
|
|
|
|
def test_tenant_tunable_secret_reference_fails(tmp_path):
|
|
data = valid_declaration()
|
|
data["spec"]["parameters"][2]["sensitivity"] = "secret_reference"
|
|
data["spec"]["parameters"][2]["tuning_authority"] = "tenant_tunable"
|
|
declaration = declaration_from(data, tmp_path)
|
|
|
|
issues = validator.validate_declaration(declaration)
|
|
|
|
assert any("security-sensitive parameters cannot be tenant_tunable" in msg for msg in error_messages(issues))
|
|
|
|
|
|
def test_scenario_composition_selects_and_overrides(tmp_path):
|
|
declaration = declaration_from(valid_declaration(), tmp_path)
|
|
scenario = {
|
|
"id": "scenario:s1-host-bootstrap-reference",
|
|
"authority": "platform",
|
|
"requires": {"capabilities": ["s1.os-baseline"]},
|
|
"parameter_overrides": {
|
|
"railiance-infra.bootstrap-host": {
|
|
"target_hosts": ["railiance01"],
|
|
"swapfile_size_mb": 8192,
|
|
}
|
|
},
|
|
}
|
|
|
|
issues, composed = validator.compose_scenario([declaration], scenario)
|
|
|
|
assert error_messages(issues) == []
|
|
selected = composed["selected_declarations"][0]
|
|
assert selected["id"] == "railiance-infra.bootstrap-host"
|
|
assert selected["parameters"]["target_hosts"]["source"] == "override"
|
|
assert selected["parameters"]["swapfile_size_mb"]["value"] == 8192
|
|
|
|
|
|
def test_tenant_authority_cannot_override_platform_only(tmp_path):
|
|
declaration = declaration_from(valid_declaration(), tmp_path)
|
|
scenario = {
|
|
"id": "scenario:bad-tenant-override",
|
|
"authority": "tenant",
|
|
"requires": {"capabilities": ["s1.os-baseline"]},
|
|
"parameter_overrides": {
|
|
"railiance-infra.bootstrap-host": {
|
|
"target_hosts": ["tenant-host"],
|
|
"wireguard_enabled": True,
|
|
}
|
|
},
|
|
}
|
|
|
|
issues, _ = validator.compose_scenario([declaration], scenario)
|
|
|
|
assert any("tenant authority cannot override platform_only" in msg for msg in error_messages(issues))
|
|
|
|
|
|
def test_required_parameter_without_override_fails(tmp_path):
|
|
declaration = declaration_from(valid_declaration(), tmp_path)
|
|
scenario = {
|
|
"id": "scenario:missing-required-parameter",
|
|
"authority": "platform",
|
|
"requires": {"capabilities": ["s1.os-baseline"]},
|
|
"parameter_overrides": {},
|
|
}
|
|
|
|
issues, _ = validator.compose_scenario([declaration], scenario)
|
|
|
|
assert any("required parameter has no default or override" in msg for msg in error_messages(issues))
|