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:
@@ -1,118 +1,24 @@
|
||||
"""castle gateway - manage the Caddy reverse proxy gateway."""
|
||||
"""castle gateway — inspect the Caddy reverse proxy gateway.
|
||||
|
||||
The gateway is itself a deployment (`castle-gateway`): start/stop/reload it the
|
||||
same way as anything else — `castle apply` (render routes + reload), `castle
|
||||
restart castle-gateway` (bounce), or `enabled: false` + apply (stop). This command
|
||||
is the read-only inspection lens: is it up, and what's the route table.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
|
||||
from castle_core.config import SPECS_DIR
|
||||
from castle_core.generators.caddyfile import generate_caddyfile_from_registry
|
||||
from castle_core.registry import REGISTRY_PATH, load_registry
|
||||
|
||||
from castle_cli.config import CastleConfig, ensure_dirs, load_config
|
||||
|
||||
GATEWAY_COMPONENT = "castle-gateway"
|
||||
GATEWAY_UNIT = "castle-castle-gateway.service"
|
||||
|
||||
|
||||
def _write_generated_files() -> None:
|
||||
"""Write generated Caddyfile from registry."""
|
||||
ensure_dirs()
|
||||
|
||||
if not REGISTRY_PATH.exists():
|
||||
print("Error: no registry found. Run 'castle apply' first.")
|
||||
return
|
||||
|
||||
registry = load_registry()
|
||||
caddyfile_path = SPECS_DIR / "Caddyfile"
|
||||
caddyfile_path.write_text(generate_caddyfile_from_registry(registry))
|
||||
print(f" Generated {caddyfile_path}")
|
||||
|
||||
|
||||
def run_gateway(args: argparse.Namespace) -> int:
|
||||
"""Manage the Caddy gateway."""
|
||||
if not args.gateway_command:
|
||||
print("Usage: castle gateway {start|stop|reload|status}")
|
||||
return 1
|
||||
|
||||
config = load_config()
|
||||
|
||||
if args.gateway_command == "start":
|
||||
if getattr(args, "dry_run", False):
|
||||
return _gateway_dry_run()
|
||||
return _gateway_start(config)
|
||||
elif args.gateway_command == "stop":
|
||||
return _gateway_stop()
|
||||
elif args.gateway_command == "reload":
|
||||
if getattr(args, "dry_run", False):
|
||||
return _gateway_dry_run()
|
||||
return _gateway_reload()
|
||||
elif args.gateway_command == "status":
|
||||
return _gateway_status()
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
def _gateway_dry_run() -> int:
|
||||
"""Print generated Caddyfile without applying."""
|
||||
if not REGISTRY_PATH.exists():
|
||||
print("Error: no registry found. Run 'castle apply' first.")
|
||||
return 1
|
||||
|
||||
registry = load_registry()
|
||||
print("# Caddyfile")
|
||||
print(generate_caddyfile_from_registry(registry))
|
||||
return 0
|
||||
|
||||
|
||||
def _gateway_start(config: CastleConfig) -> int:
|
||||
"""Generate config and enable the gateway service."""
|
||||
from castle_cli.commands.service import _service_enable
|
||||
|
||||
if GATEWAY_COMPONENT not in config.services:
|
||||
print(f"Error: '{GATEWAY_COMPONENT}' not found in services section")
|
||||
return 1
|
||||
|
||||
print("Generating gateway configuration...")
|
||||
_write_generated_files()
|
||||
|
||||
print(f"\nStarting gateway on port {config.gateway.port}...")
|
||||
return _service_enable(config, GATEWAY_COMPONENT)
|
||||
|
||||
|
||||
def _gateway_stop() -> int:
|
||||
"""Stop the gateway service."""
|
||||
from castle_cli.commands.service import _service_disable
|
||||
|
||||
return _service_disable(GATEWAY_COMPONENT)
|
||||
|
||||
|
||||
def _gateway_reload() -> int:
|
||||
"""Regenerate config and reload Caddy."""
|
||||
print("Regenerating gateway configuration...")
|
||||
_write_generated_files()
|
||||
|
||||
result = subprocess.run(
|
||||
["systemctl", "--user", "reload", GATEWAY_UNIT],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("Gateway reloaded.")
|
||||
else:
|
||||
print("Reload signal sent. Verifying...")
|
||||
result = subprocess.run(
|
||||
["systemctl", "--user", "is-active", GATEWAY_UNIT],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.stdout.strip() == "active":
|
||||
print("Gateway running.")
|
||||
else:
|
||||
print("Warning: gateway may not be running. Try: castle gateway start")
|
||||
|
||||
return 0
|
||||
"""Show the gateway's status + route table (the only gateway verb)."""
|
||||
return _gateway_status()
|
||||
|
||||
|
||||
def _gateway_status() -> int:
|
||||
|
||||
@@ -14,7 +14,6 @@ from castle_core.manifest import kind_for
|
||||
|
||||
from castle_cli.config import (
|
||||
CastleConfig,
|
||||
ensure_dirs,
|
||||
load_config,
|
||||
)
|
||||
|
||||
@@ -178,30 +177,6 @@ def run_status(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _service_enable(config: CastleConfig, name: str) -> int:
|
||||
"""Enable a service in its mode (systemd unit / gateway route / PATH install)."""
|
||||
import asyncio
|
||||
|
||||
from castle_core.lifecycle import activate
|
||||
|
||||
ensure_dirs()
|
||||
result = asyncio.run(activate(name, config, config.root))
|
||||
print(result.output)
|
||||
return 0 if result.status == "ok" else 1
|
||||
|
||||
|
||||
def _service_disable(config: CastleConfig, name: str) -> int:
|
||||
"""Disable a service in its mode (stop unit / drop route / uninstall)."""
|
||||
import asyncio
|
||||
|
||||
from castle_core.lifecycle import deactivate
|
||||
|
||||
print(f"Disabling {name}...")
|
||||
result = asyncio.run(deactivate(name, config, config.root))
|
||||
print(f" {result.output}")
|
||||
return 0
|
||||
|
||||
|
||||
def _service_status(config: CastleConfig) -> int:
|
||||
"""Show status of all services and jobs, dispatched by manager."""
|
||||
from castle_core.lifecycle import is_active
|
||||
|
||||
@@ -159,15 +159,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
_build_deployment_group(subparsers, "job")
|
||||
_build_tool_group(subparsers)
|
||||
|
||||
# Gateway (infrastructure)
|
||||
gw = subparsers.add_parser("gateway", help="Manage the Caddy gateway")
|
||||
# Gateway (inspection). The gateway is a deployment — start/stop/reload it via
|
||||
# `castle apply` / `castle restart castle-gateway`; this lens just shows routes.
|
||||
gw = subparsers.add_parser("gateway", help="Show the gateway's status + route table")
|
||||
gw_sub = gw.add_subparsers(dest="gateway_command")
|
||||
p = gw_sub.add_parser("start", help="Start the gateway")
|
||||
p.add_argument("--dry-run", action="store_true")
|
||||
gw_sub.add_parser("stop", help="Stop the gateway")
|
||||
p = gw_sub.add_parser("reload", help="Reload gateway configuration")
|
||||
p.add_argument("--dry-run", action="store_true")
|
||||
gw_sub.add_parser("status", help="Show gateway status")
|
||||
gw_sub.add_parser("status", help="Show gateway status + routes (the default)")
|
||||
|
||||
# Convergence — the one lifecycle verb. Renders units/Caddyfile/tunnel, then
|
||||
# reconciles the runtime to match config (activate/restart/deactivate).
|
||||
|
||||
Reference in New Issue
Block a user