net-tools/bin/infra-net

114 lines
4.6 KiB
Text
Raw Normal View History

#!/usr/bin/env python3
"""infra-net — reconcile every project's .infra.yaml against mesh-hosts.json.
Walks ~/Code for .infra.yaml manifests (convention:infra_manifest), validates each
against the convention's JSON Schema, checks that `service.host` is a known
mesh-hosts.json host, flags per-host port collisions, prints the live infra-net
table, and writes the reconciled inventory to data/infra-net.json (non-destructive
— it does NOT overwrite mesh-hosts.json's services map, so existing consumers are
untouched).
net-tools/bin/infra-net # print + write inventory
net-tools/bin/infra-net --check # validate only, non-zero exit on problems
"""
import glob
import json
import os
import sys
HOME = os.path.expanduser("~")
CODE = os.path.join(HOME, "Code")
NET_TOOLS = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
MESH = os.path.join(NET_TOOLS, "data", "mesh-hosts.json")
SCHEMA_CONV = os.path.join(CODE, "@conventions", "programming_general", "infra_manifest.yaml")
OUT = os.path.join(NET_TOOLS, "data", "infra-net.json")
# Where deployable projects live (each may carry a root .infra.yaml).
ROOTS = [
os.path.join(CODE, "@applications", "*", ".infra*.yaml"),
os.path.join(CODE, "@projects", "@cocottetech", "@platform", "codebase", "@features", "*", ".infra*.yaml"),
os.path.join(CODE, "@projects", "@cocottetech", ".infra*.yaml"),
os.path.join(CODE, "@projects", "@magic-civilization", ".infra*.yaml"),
]
def main() -> int:
try:
import yaml
import jsonschema
except ImportError as e:
print(f"infra-net needs pyyaml + jsonschema ({e})", file=sys.stderr)
return 2
check_only = "--check" in sys.argv
mesh = json.load(open(MESH))
# Accept canonical host names AND their aliases (e.g. lime == lilith-store-backend).
hosts = {h["name"] for h in mesh.get("hosts", [])}
hosts |= {a for h in mesh.get("hosts", []) for a in h.get("aliases", [])}
schema = yaml.safe_load(open(SCHEMA_CONV))["providesFile"]["schema"]
manifests = []
for pat in ROOTS:
manifests.extend(sorted(glob.glob(pat)))
problems = []
seen_ports: dict[tuple[str, int], str] = {}
rows = []
for path in manifests:
rel = path.replace(CODE + "/", "")
try:
m = yaml.safe_load(open(path))
jsonschema.validate(m, schema)
except Exception as e: # noqa: BLE001 — surface any parse/validation error
problems.append(f"{rel}: {getattr(e, 'message', e)}")
continue
svc = m.get("service", {}) or {}
host, port = svc.get("host"), svc.get("port")
if host and host not in hosts:
problems.append(f"{rel}: service.host '{host}' not in mesh-hosts.json {sorted(hosts)}")
if host and port is not None:
key = (host, port)
if key in seen_ports:
problems.append(f"port collision on {host}:{port} — {m['project']} vs {seen_ports[key]}")
else:
seen_ports[key] = m["project"]
db = m.get("database", {}) or {}
rows.append({
"project": m["project"],
"environment": m.get("environment", "prod"),
"provider": m.get("provider"),
"host": host,
"port": port,
"db": (f"{db.get('name')}@{db.get('cluster')}" if db else None),
"depends_on": m.get("depends_on", []),
"source": rel,
})
rows.sort(key=lambda r: (r["host"] or "~", r["port"] or 0))
w = max([len(r["project"]) for r in rows] + [7])
print(f"\n infra-net — {len(rows)} services across {len(hosts)} hosts\n")
print(f" {'PROJECT'.ljust(w)} {'ENV'.ljust(4)} {'HOST'.ljust(8)} {'PORT'.ljust(5)} {'PROVIDER'.ljust(12)} DB / DEPS")
for r in rows:
deps = (" deps:" + ",".join(r["depends_on"])) if r["depends_on"] else ""
dbp = (r["db"] or "") + deps
print(f" {r['project'].ljust(w)} {str(r['environment']).ljust(4)} {str(r['host']).ljust(8)} {str(r['port'] or '').ljust(5)} {str(r['provider']).ljust(12)} {dbp}")
if problems:
print("\n PROBLEMS:")
for p in problems:
print(f" ✗ {p}")
else:
print("\n ✓ all manifests valid; hosts known; no port collisions")
if not check_only:
json.dump({"_generated_by": "net-tools/bin/infra-net", "_source": "project .infra.yaml + mesh-hosts.json",
"services": rows, "problems": problems}, open(OUT, "w"), indent=2)
print(f"\n wrote {os.path.relpath(OUT, NET_TOOLS)}")
return 1 if problems else 0
if __name__ == "__main__":
sys.exit(main())