Implement reproducible S1 handoff contracts
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
This commit is contained in:
parent
c8cb1c8edf
commit
b93af8cc78
44 changed files with 2035 additions and 342 deletions
174
scripts/inventory_contract.py
Normal file
174
scripts/inventory_contract.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validate the S1 host inventory without contacting a provider or host."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
SCHEMA_VERSION = "1.0"
|
||||
PROVIDERS = {"hosteurope", "hetzner"}
|
||||
LIFECYCLE_MODES = {"adopted", "provider-managed"}
|
||||
BASELINE_PROFILES = {"ufw-managed", "external-firewall"}
|
||||
HETZNER_REQUIRED = {"server_type", "region", "image", "role"}
|
||||
HETZNER_OPTIONAL = {"labels"}
|
||||
NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$")
|
||||
USER_RE = re.compile(r"^[a-z_][a-z0-9_-]{0,31}$")
|
||||
|
||||
|
||||
class InventoryError(ValueError):
|
||||
"""The inventory does not satisfy the S1 contract."""
|
||||
|
||||
|
||||
def _nonempty_string(value: Any) -> bool:
|
||||
return isinstance(value, str) and bool(value.strip())
|
||||
|
||||
|
||||
def validate_inventory(payload: Any) -> dict[str, Any]:
|
||||
if not isinstance(payload, dict):
|
||||
raise InventoryError("inventory must be a YAML object")
|
||||
if str(payload.get("schema_version")) != SCHEMA_VERSION:
|
||||
raise InventoryError(f"schema_version must be {SCHEMA_VERSION!r}")
|
||||
servers = payload.get("servers")
|
||||
if not isinstance(servers, list) or not servers:
|
||||
raise InventoryError("servers must be a non-empty list")
|
||||
|
||||
errors: list[str] = []
|
||||
names: set[str] = set()
|
||||
for index, server in enumerate(servers):
|
||||
label = f"servers[{index}]"
|
||||
if not isinstance(server, dict):
|
||||
errors.append(f"{label} must be an object")
|
||||
continue
|
||||
name = server.get("name")
|
||||
if not _nonempty_string(name) or not NAME_RE.fullmatch(name):
|
||||
errors.append(f"{label}.name must be a stable host identifier")
|
||||
name = label
|
||||
elif name in names:
|
||||
errors.append(f"{label}.name duplicates {name!r}")
|
||||
else:
|
||||
names.add(name)
|
||||
label = str(name)
|
||||
|
||||
provider = server.get("provider")
|
||||
lifecycle = server.get("lifecycle_mode")
|
||||
profile = server.get("baseline_profile")
|
||||
ssh_user = server.get("ssh_user")
|
||||
if provider not in PROVIDERS:
|
||||
errors.append(f"{label}: provider must be one of {sorted(PROVIDERS)}")
|
||||
if lifecycle not in LIFECYCLE_MODES:
|
||||
errors.append(
|
||||
f"{label}: lifecycle_mode must be one of {sorted(LIFECYCLE_MODES)}"
|
||||
)
|
||||
if profile not in BASELINE_PROFILES:
|
||||
errors.append(
|
||||
f"{label}: baseline_profile must be one of {sorted(BASELINE_PROFILES)}"
|
||||
)
|
||||
if not _nonempty_string(ssh_user) or not USER_RE.fullmatch(ssh_user):
|
||||
errors.append(f"{label}: ssh_user must be a valid Unix user name")
|
||||
|
||||
if lifecycle == "adopted":
|
||||
address = server.get("ip")
|
||||
try:
|
||||
ipaddress.ip_address(address)
|
||||
except (TypeError, ValueError):
|
||||
errors.append(f"{label}: adopted hosts require a literal ip address")
|
||||
if "provisioning" in server:
|
||||
errors.append(
|
||||
f"{label}: adopted hosts must not carry provider provisioning fields"
|
||||
)
|
||||
elif lifecycle == "provider-managed":
|
||||
if provider != "hetzner":
|
||||
errors.append(
|
||||
f"{label}: provider-managed is currently implemented only for hetzner"
|
||||
)
|
||||
if "ip" in server:
|
||||
errors.append(
|
||||
f"{label}: provider-managed addresses come from provider output; remove ip"
|
||||
)
|
||||
provisioning = server.get("provisioning")
|
||||
if not isinstance(provisioning, dict):
|
||||
errors.append(f"{label}: provider-managed hosts require provisioning")
|
||||
else:
|
||||
missing = sorted(
|
||||
key
|
||||
for key in HETZNER_REQUIRED
|
||||
if not _nonempty_string(provisioning.get(key))
|
||||
)
|
||||
unknown = sorted(
|
||||
set(provisioning) - HETZNER_REQUIRED - HETZNER_OPTIONAL
|
||||
)
|
||||
if missing:
|
||||
errors.append(
|
||||
f"{label}: provisioning missing {', '.join(missing)}"
|
||||
)
|
||||
if unknown:
|
||||
errors.append(
|
||||
f"{label}: provisioning has unknown fields {', '.join(unknown)}"
|
||||
)
|
||||
labels = provisioning.get("labels", [])
|
||||
if not isinstance(labels, list) or not all(
|
||||
_nonempty_string(item) for item in labels
|
||||
):
|
||||
errors.append(
|
||||
f"{label}: provisioning.labels must be a list of strings"
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise InventoryError("inventory contract failed:\n- " + "\n- ".join(errors))
|
||||
return payload
|
||||
|
||||
|
||||
def load_inventory(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
payload = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
except (OSError, yaml.YAMLError) as exc:
|
||||
raise InventoryError(f"cannot read {path}: {exc}") from exc
|
||||
return validate_inventory(payload)
|
||||
|
||||
|
||||
def managed_hetzner_servers(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
validate_inventory(payload)
|
||||
return [
|
||||
server
|
||||
for server in payload["servers"]
|
||||
if server["provider"] == "hetzner"
|
||||
and server["lifecycle_mode"] == "provider-managed"
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"inventory", nargs="?", type=Path, default=Path("inventory/servers.yaml")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--managed-hetzner", action="store_true", help="print selected names"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
payload = load_inventory(args.inventory)
|
||||
except InventoryError as exc:
|
||||
print(exc, file=sys.stderr)
|
||||
return 1
|
||||
selected = managed_hetzner_servers(payload)
|
||||
result = {
|
||||
"ok": True,
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"server_count": len(payload["servers"]),
|
||||
"managed_hetzner": [server["name"] for server in selected],
|
||||
}
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue