feat(mesh): cross-node routing + presence breaker (Phase 3)
- 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.
This commit is contained in:
87
core/tests/test_caddyfile_remote.py
Normal file
87
core/tests/test_caddyfile_remote.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Cross-node (remote) gateway routes + presence breaker."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from castle_core.config import load_config
|
||||
from castle_core.generators.caddyfile import compute_routes
|
||||
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
|
||||
|
||||
|
||||
def _config_requiring_widget(root: Path) -> None:
|
||||
(root / "castle.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
|
||||
svc_dir = root / "services"
|
||||
svc_dir.mkdir()
|
||||
(svc_dir / "consumer.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"description": "consumes a remote widget",
|
||||
"manager": "systemd",
|
||||
"run": {"launcher": "command", "argv": ["consumer"]},
|
||||
"requires": [{"kind": "deployment", "ref": "widget"}],
|
||||
"manage": {"systemd": {}},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _peer_with_widget(address: str | None) -> NodeRegistry:
|
||||
widget = Deployment(
|
||||
manager="systemd",
|
||||
launcher="python",
|
||||
run_cmd=[],
|
||||
name="widget",
|
||||
kind="service",
|
||||
port=9099,
|
||||
subdomain="widget",
|
||||
managed=True,
|
||||
)
|
||||
return NodeRegistry(
|
||||
node=NodeConfig(hostname="tower", address=address),
|
||||
deployed={NodeRegistry.key("service", "widget"): widget},
|
||||
)
|
||||
|
||||
|
||||
def _local() -> NodeRegistry:
|
||||
return NodeRegistry(node=NodeConfig(hostname="civil"), deployed={})
|
||||
|
||||
|
||||
def test_remote_route_emitted_for_consumed_peer_service(tmp_path: Path) -> None:
|
||||
_config_requiring_widget(tmp_path)
|
||||
config = load_config(tmp_path)
|
||||
routes = compute_routes(
|
||||
_local(), config, {"tower": _peer_with_widget("10.0.0.5")}
|
||||
)
|
||||
remote = [r for r in routes if r.kind == "remote"]
|
||||
assert len(remote) == 1
|
||||
assert remote[0].address == "widget"
|
||||
assert remote[0].target == "10.0.0.5:9099"
|
||||
assert remote[0].node == "tower"
|
||||
|
||||
|
||||
def test_address_falls_back_to_hostname(tmp_path: Path) -> None:
|
||||
_config_requiring_widget(tmp_path)
|
||||
config = load_config(tmp_path)
|
||||
routes = compute_routes(_local(), config, {"tower": _peer_with_widget(None)})
|
||||
remote = [r for r in routes if r.kind == "remote"]
|
||||
assert remote[0].target == "tower:9099"
|
||||
|
||||
|
||||
def test_breaker_no_route_when_peer_absent(tmp_path: Path) -> None:
|
||||
"""Presence expiry removes the peer from remote_registries -> no route."""
|
||||
_config_requiring_widget(tmp_path)
|
||||
config = load_config(tmp_path)
|
||||
routes = compute_routes(_local(), config, {}) # no online peers
|
||||
assert not [r for r in routes if r.kind == "remote"]
|
||||
|
||||
|
||||
def test_no_remote_route_for_unconsumed_service(tmp_path: Path) -> None:
|
||||
"""A peer service nobody requires is not routed."""
|
||||
(tmp_path / "castle.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
|
||||
config = load_config(tmp_path) # no requires anywhere
|
||||
routes = compute_routes(
|
||||
_local(), config, {"tower": _peer_with_widget("10.0.0.5")}
|
||||
)
|
||||
assert not [r for r in routes if r.kind == "remote"]
|
||||
Reference in New Issue
Block a user