Driven by adopting lakehouse (an existing daemon that bundles its own SPA) — see docs/findings-lakehouse.md. #3 deploy reloads the gateway. castle deploy regenerated the Caddyfile but left the running Caddy on the old config, so new proxy routes were silently dead. Deploy now reloads the gateway when it's running. #1 castle expose <program>. Turns an adopted program into a service (run/port/health/proxy/systemd) in one command — the missing daemon-to-service step. Flags: --port --health --path --run --port-env --host --no-proxy. #2 port_env mapping. A service can declare expose.http.internal.port_env so castle sets the env var the program actually reads (e.g. lakehoused reads LAKEHOUSED_DAEMON_PORT, not castle's convention LAKEHOUSE_PORT). Castle now genuinely drives an adopted daemon's bind port. #4a auto-base for react-vite. The build passes VITE_BASE = the gateway serve prefix (/<name>/, or / for castle-app); vite.config reads it. A castle-built frontend now works at its subpath with no hand-tuned base. castle-app's vite.config updated as the reference. #4b host-based routing. proxy.caddy.host routes a whole hostname to the backend root via a host matcher inside the :9000 site, so a root-based SPA (base="/") serves unchanged — the fix for proxying an app castle can't rebuild. Caddyfile now emits 'auto_https off' (HTTP-only gateway on a non-standard port). Nit: activate skips the editable reinstall when the tool is already on PATH. Tests: core 94, cli 24, api 52; ruff + app build clean. Verified live: lakehouse runs under systemd, API at /lakehouse, full UI (SPA boots) via host routing.
207 lines
7.6 KiB
Python
207 lines
7.6 KiB
Python
"""Tests for Caddyfile generation from registry."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import pytest
|
|
|
|
from castle_core.generators.caddyfile import generate_caddyfile_from_registry
|
|
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Isolate the generator from the real ~/.castle config so static-frontend
|
|
routes don't leak into these registry-focused tests."""
|
|
import castle_core.config as config_mod
|
|
|
|
def _no_config(*args: object, **kwargs: object) -> object:
|
|
raise FileNotFoundError("isolated in tests")
|
|
|
|
monkeypatch.setattr(config_mod, "load_config", _no_config)
|
|
|
|
|
|
def _make_registry(
|
|
deployed: dict[str, Deployment] | None = None,
|
|
gateway_port: int = 9000,
|
|
) -> NodeRegistry:
|
|
"""Create a test registry."""
|
|
return NodeRegistry(
|
|
node=NodeConfig(hostname="test", gateway_port=gateway_port),
|
|
deployed=deployed or {},
|
|
)
|
|
|
|
|
|
class TestCaddyfileFromRegistry:
|
|
"""Tests for registry-based Caddyfile generation."""
|
|
|
|
def test_contains_gateway_port(self) -> None:
|
|
"""Caddyfile uses the configured gateway port."""
|
|
registry = _make_registry(gateway_port=18000)
|
|
caddyfile = generate_caddyfile_from_registry(registry)
|
|
assert ":18000 {" in caddyfile
|
|
|
|
def test_contains_service_routes(self) -> None:
|
|
"""Caddyfile has reverse proxy routes for deployed services."""
|
|
registry = _make_registry(
|
|
deployed={
|
|
"test-svc": Deployment(
|
|
runner="python",
|
|
run_cmd=["uv", "run", "test-svc"],
|
|
port=19000,
|
|
proxy_path="/test-svc",
|
|
),
|
|
}
|
|
)
|
|
caddyfile = generate_caddyfile_from_registry(registry)
|
|
assert "handle_path /test-svc/*" in caddyfile
|
|
assert "reverse_proxy localhost:19000" in caddyfile
|
|
|
|
def test_disables_auto_https(self) -> None:
|
|
"""The gateway is HTTP-only; auto_https must be off so named hosts don't
|
|
flip the listener to TLS or try to bind :80."""
|
|
caddyfile = generate_caddyfile_from_registry(_make_registry())
|
|
assert "auto_https off" in caddyfile
|
|
|
|
def test_host_route_uses_matcher_in_main_site(self) -> None:
|
|
"""A proxy_host becomes a host matcher inside the :9000 site (not a
|
|
separate site block, which would split the listener into TLS)."""
|
|
registry = _make_registry(
|
|
deployed={
|
|
"lake": Deployment(
|
|
runner="python",
|
|
run_cmd=["lake"],
|
|
port=8420,
|
|
proxy_host="lake.example.lan",
|
|
),
|
|
}
|
|
)
|
|
caddyfile = generate_caddyfile_from_registry(registry)
|
|
assert "@host_lake host lake.example.lan" in caddyfile
|
|
assert "handle @host_lake {" in caddyfile
|
|
assert "reverse_proxy localhost:8420" in caddyfile
|
|
# No separate hostname site block on the gateway port.
|
|
assert "lake.example.lan:9000 {" not in caddyfile
|
|
|
|
def test_skips_non_proxied(self) -> None:
|
|
"""Components without proxy_path are not in Caddyfile."""
|
|
registry = _make_registry(
|
|
deployed={
|
|
"test-tool": Deployment(
|
|
runner="command",
|
|
run_cmd=["test-tool"],
|
|
),
|
|
}
|
|
)
|
|
caddyfile = generate_caddyfile_from_registry(registry)
|
|
assert "test-tool" not in caddyfile
|
|
|
|
def test_fallback_when_no_static(self) -> None:
|
|
"""Uses fallback dashboard path when static dir doesn't exist."""
|
|
registry = _make_registry()
|
|
caddyfile = generate_caddyfile_from_registry(registry)
|
|
assert "handle / {" in caddyfile
|
|
assert "file_server" in caddyfile
|
|
|
|
def test_proxy_routes_before_dashboard(self) -> None:
|
|
"""Service proxy routes appear before the dashboard catch-all."""
|
|
registry = _make_registry(
|
|
deployed={
|
|
"test-svc": Deployment(
|
|
runner="python",
|
|
run_cmd=["uv", "run", "test-svc"],
|
|
port=19000,
|
|
proxy_path="/test-svc",
|
|
),
|
|
}
|
|
)
|
|
caddyfile = generate_caddyfile_from_registry(registry)
|
|
proxy_pos = caddyfile.index("handle_path")
|
|
handle_pos = caddyfile.index("handle /")
|
|
assert proxy_pos < handle_pos
|
|
|
|
def test_multiple_services(self) -> None:
|
|
"""Multiple services get separate proxy routes."""
|
|
registry = _make_registry(
|
|
deployed={
|
|
"svc-a": Deployment(
|
|
runner="python",
|
|
run_cmd=["uv", "run", "svc-a"],
|
|
port=9001,
|
|
proxy_path="/svc-a",
|
|
),
|
|
"svc-b": Deployment(
|
|
runner="python",
|
|
run_cmd=["uv", "run", "svc-b"],
|
|
port=9002,
|
|
proxy_path="/svc-b",
|
|
),
|
|
}
|
|
)
|
|
caddyfile = generate_caddyfile_from_registry(registry)
|
|
assert "handle_path /svc-a/*" in caddyfile
|
|
assert "reverse_proxy localhost:9001" in caddyfile
|
|
assert "handle_path /svc-b/*" in caddyfile
|
|
assert "reverse_proxy localhost:9002" in caddyfile
|
|
|
|
|
|
class TestCaddyfileRemoteRegistries:
|
|
"""Tests for cross-node routing in Caddyfile."""
|
|
|
|
def test_remote_routes_included(self) -> None:
|
|
"""Remote services get reverse_proxy entries to their hostname."""
|
|
local = _make_registry(
|
|
deployed={
|
|
"local-svc": Deployment(
|
|
runner="python", run_cmd=["svc"], port=9001, proxy_path="/local"
|
|
),
|
|
}
|
|
)
|
|
remote = _make_registry(
|
|
deployed={
|
|
"remote-svc": Deployment(
|
|
runner="python", run_cmd=["svc"], port=9050, proxy_path="/remote"
|
|
),
|
|
}
|
|
)
|
|
remote.node.hostname = "devbox"
|
|
caddyfile = generate_caddyfile_from_registry(local, remote_registries={"devbox": remote})
|
|
assert "reverse_proxy localhost:9001" in caddyfile
|
|
assert "reverse_proxy devbox:9050" in caddyfile
|
|
assert "handle_path /remote/*" in caddyfile
|
|
|
|
def test_local_takes_precedence(self) -> None:
|
|
"""If local and remote use the same path, local wins."""
|
|
local = _make_registry(
|
|
deployed={
|
|
"svc": Deployment(
|
|
runner="python", run_cmd=["svc"], port=9001, proxy_path="/api"
|
|
),
|
|
}
|
|
)
|
|
remote = _make_registry(
|
|
deployed={
|
|
"svc": Deployment(
|
|
runner="python", run_cmd=["svc"], port=9001, proxy_path="/api"
|
|
),
|
|
}
|
|
)
|
|
remote.node.hostname = "devbox"
|
|
caddyfile = generate_caddyfile_from_registry(local, remote_registries={"devbox": remote})
|
|
assert "reverse_proxy localhost:9001" in caddyfile
|
|
assert "devbox" not in caddyfile
|
|
|
|
def test_no_remote_when_none(self) -> None:
|
|
"""No remote routes when remote_registries is None."""
|
|
local = _make_registry(
|
|
deployed={
|
|
"svc": Deployment(
|
|
runner="python", run_cmd=["svc"], port=9001, proxy_path="/api"
|
|
),
|
|
}
|
|
)
|
|
caddyfile = generate_caddyfile_from_registry(local, remote_registries=None)
|
|
assert "reverse_proxy localhost:9001" in caddyfile
|
|
# Only one reverse_proxy line
|
|
assert caddyfile.count("reverse_proxy") == 1
|