2026-06-29 10:10:51 -04:00
|
|
|
#!/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 = [
|
2026-06-29 10:20:13 -04:00
|
|
|
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"),
|
2026-06-29 10:10:51 -04:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"],
|
2026-06-29 10:20:13 -04:00
|
|
|
"environment": m.get("environment", "prod"),
|
2026-06-29 10:10:51 -04:00
|
|
|
"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")
|
2026-06-29 10:20:13 -04:00
|
|
|
print(f" {'PROJECT'.ljust(w)} {'ENV'.ljust(4)} {'HOST'.ljust(8)} {'PORT'.ljust(5)} {'PROVIDER'.ljust(12)} DB / DEPS")
|
2026-06-29 10:10:51 -04:00
|
|
|
for r in rows:
|
|
|
|
|
deps = (" deps:" + ",".join(r["depends_on"])) if r["depends_on"] else ""
|
|
|
|
|
dbp = (r["db"] or "") + deps
|
2026-06-29 10:20:13 -04:00
|
|
|
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}")
|
2026-06-29 10:10:51 -04:00
|
|
|
|
|
|
|
|
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())
|