feat(infra-net): reconcile project .infra.yaml against mesh-hosts.json
New bin/infra-net walks every project .infra.yaml (convention:infra_manifest), validates schema + host∈mesh-hosts (alias-aware) + port collisions, prints the live infra-net and writes data/infra-net.json (gitignored, non-destructive — does not touch the services map). Caught prospector's stale host name on first run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f6eae0791
commit
1bd8f0f8b9
2 changed files with 114 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,4 +7,6 @@ __pycache__/
|
|||
# Volatile discovered state (current LAN IPs) — written by the daemon, not source.
|
||||
data/lan-state.json
|
||||
data/agent-status.json
|
||||
data/.tray-disabled
|
||||
tray/.venv/
|
||||
data/infra-net.json
|
||||
|
|
|
|||
112
bin/infra-net
Executable file
112
bin/infra-net
Executable file
|
|
@ -0,0 +1,112 @@
|
|||
#!/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"],
|
||||
"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)} {'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['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())
|
||||
Loading…
Add table
Reference in a new issue