Files
wild-pc/core/tests/test_caddyfile_remote.py
Paul Payne 5beebfd739 core: drop pre-migration read-compat now both nodes are current-format
Both machines (civil, primer) are on the current on-disk layout, so the
read-compat for older formats is dead weight. Remove it:

- config loader: only deployments/<kind>/<name>.yaml (drop flat deployments/*.yaml
  and the services/+jobs/ split); drop the run.runner→manager normalizer.
- registry loader: require composite <kind>/<name> keys + manager/kind (registry.yaml
  is regenerated every apply, so it's always current).
- manifest: drop the proxy/public→reach legacy input and the component→program alias
  (the derived proxy/public read accessors stay). reach field defaults already match.
- config_editor: parse incoming specs directly (frontend sends the current shape).
- save_config: drop the flat-file migration cleanup.

Bootstrap seeds move to bootstrap/deployments/<kind>/ and install.sh seeds that
per-kind tree directly. The legacy→current translation for terse test fixtures
moves into each test conftest (out of production). All suites green (362).
2026-07-07 09:16:03 -07:00

88 lines
3.0 KiB
Python

"""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 / "deployments" / "services"
svc_dir.mkdir(parents=True)
(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"]