generated from coulomb/repo-seed
Implement NK-WP-0013 playbook capability contract
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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))
|
||||
Reference in New Issue
Block a user