gateway: converge, don't start/stop/reload

The gateway is a deployment (castle-gateway) — its lifecycle is the same
convergence as everything else, so the bespoke start/stop/reload verbs
(which still routed through the retired enable/disable path) are gone.

- CLI: `castle gateway` is now inspection only — bare or `status` shows
  the route table. Start/stop/reload it via `castle apply` (render routes
  + reload) or `castle restart castle-gateway`. Deletes the last users of
  `_service_enable`/`_service_disable`, so those are removed too.
- API: retire `POST /gateway/reload` (making routes live is `POST /apply`);
  `PUT /gateway/config` message + docstring now say apply, not deploy.
- UI: GatewayPanel's "Reload" button → "Apply" (useApply); drop the
  useGatewayReload hook; GatewaySettings points at `castle apply`.
- docs: `castle gateway reload|status` → `castle gateway` (status lens).

core 129 / cli 31 / api 59 pass; dashboard builds clean; live `castle
gateway` shows routes with no start/stop/reload; /gateway/reload removed
from the API.
This commit is contained in:
2026-07-02 12:12:00 -07:00
parent ca32e6f29f
commit ef588cb806
11 changed files with 32 additions and 197 deletions

View File

@@ -2,13 +2,11 @@
from __future__ import annotations
import asyncio
import subprocess
from pathlib import Path
from fastapi import APIRouter, HTTPException, status
from castle_core.config import SPECS_DIR
from castle_core.generators.caddyfile import generate_caddyfile_from_registry
from castle_core.manifest import (
ProgramSpec,
@@ -937,7 +935,7 @@ def get_gateway() -> GatewayInfo:
def save_gateway_config(request: GatewayConfigRequest) -> dict[str, str]:
"""Update the gateway's routing/exposure settings in castle.yaml.
Saves only; the caller runs deploy to regenerate the Caddyfile + tunnel config.
Saves only; the caller runs apply to regenerate the Caddyfile + tunnel config.
An empty string clears a field.
"""
root = get_castle_root()
@@ -952,7 +950,7 @@ def save_gateway_config(request: GatewayConfigRequest) -> dict[str, str]:
config.gateway.public_domain = norm(request.public_domain)
config.gateway.tunnel_id = norm(request.tunnel_id)
save_config(config)
return {"status": "saved", "message": "Saved. Run deploy to apply."}
return {"status": "saved", "message": "Saved. Run apply to converge."}
@router.get("/gateway/caddyfile")
@@ -962,35 +960,5 @@ def get_caddyfile() -> dict[str, str]:
return {"content": generate_caddyfile_from_registry(registry)}
@router.post("/gateway/reload")
async def reload_gateway() -> dict[str, str]:
"""Regenerate Caddyfile and reload Caddy."""
registry = get_registry()
SPECS_DIR.mkdir(parents=True, exist_ok=True)
caddyfile_path = SPECS_DIR / "Caddyfile"
# Include remote registries for cross-node routing
remote_regs = {h: n.registry for h, n in mesh_state.all_nodes().items()}
caddyfile_path.write_text(
generate_caddyfile_from_registry(
registry, remote_registries=remote_regs or None
)
)
proc = await asyncio.create_subprocess_exec(
"systemctl",
"--user",
"reload",
"castle-castle-gateway.service",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
_, stderr = await proc.communicate()
if proc.returncode != 0:
raise HTTPException(
status_code=500,
detail=f"Reload failed: {(stderr or b'').decode().strip()}",
)
return {"status": "ok"}
# Note: gateway reload is not a standalone endpoint. Making routes/config live is
# convergence — POST /apply renders the Caddyfile (+ tunnel) and reloads Caddy.