- NodeConfig.address: routable host peers proxy to (wired registry + wire) - compute_routes emits 'remote' routes for consumed peer services (local requires ref satisfied by an online peer -> <address>:<port>) - _host_remote_block: fail-fast reverse_proxy (2s dial, passive health); presence expiry removes the route entirely = the primary circuit-breaker - mesh_gateway.py: API re-renders + reloads the Caddyfile on mesh change, iff content changed (no-op until a cross-node service is consumed) - tests: route emit / address fallback / breaker-absent / unconsumed Logic verified hermetically + against primer's real registry (castle-api -> primer:9020); live integration proven a no-op on civil.
56 lines
1.8 KiB
Python
56 lines
1.8 KiB
Python
"""Regenerate the gateway with cross-node (remote) routes from the live mesh.
|
|
|
|
Local routes come from `castle apply` (static). Remote routes are dynamic — they
|
|
appear/vanish as peers join/leave — so the API owns them: on a mesh change it
|
|
re-renders the Caddyfile (same generator `apply` uses, plus remote routes for
|
|
online peers) and reloads the gateway iff the content changed.
|
|
|
|
Safety: with no cross-node `requires`, the output equals the local-only Caddyfile,
|
|
so this is a verified no-op until this node actually consumes a peer service.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import subprocess
|
|
|
|
from castle_core.config import SPECS_DIR
|
|
from castle_core.generators.caddyfile import generate_caddyfile_from_registry
|
|
|
|
from castle_api.config import get_registry
|
|
from castle_api.mesh import mesh_state
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_GATEWAY_UNIT = "castle-castle-gateway.service"
|
|
|
|
|
|
def _regenerate(reload: bool) -> bool:
|
|
try:
|
|
reg = get_registry()
|
|
except Exception:
|
|
return False
|
|
remotes = {h: n.registry for h, n in mesh_state.all_nodes().items()}
|
|
try:
|
|
content = generate_caddyfile_from_registry(reg, remotes)
|
|
except Exception:
|
|
logger.exception("mesh gateway: Caddyfile generation failed")
|
|
return False
|
|
path = SPECS_DIR / "Caddyfile"
|
|
old = path.read_text() if path.exists() else ""
|
|
if content == old:
|
|
return False
|
|
path.write_text(content)
|
|
logger.info("mesh gateway: Caddyfile updated with cross-node routes")
|
|
if reload:
|
|
subprocess.run(
|
|
["systemctl", "--user", "reload", _GATEWAY_UNIT], check=False
|
|
)
|
|
return True
|
|
|
|
|
|
async def refresh_remote_routes(reload: bool = True) -> bool:
|
|
"""Async wrapper — runs the blocking regen off the event loop."""
|
|
return await asyncio.to_thread(_regenerate, reload)
|