feat: Enhance gateway management and add Caddyfile generation support
This commit is contained in:
@@ -3,12 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from castle_cli.config import GENERATED_DIR, CastleConfig, ensure_dirs, load_config
|
||||
|
||||
|
||||
GATEWAY_COMPONENT = "gateway"
|
||||
GATEWAY_UNIT = "castle-gateway.service"
|
||||
|
||||
|
||||
def _find_dashboard_dist(config: CastleConfig) -> str | None:
|
||||
"""Find the dashboard dist/ directory if it exists."""
|
||||
dist = config.root / "dashboard" / "dist"
|
||||
@@ -82,14 +85,15 @@ def run_gateway(args: argparse.Namespace) -> int:
|
||||
|
||||
config = load_config()
|
||||
|
||||
if args.gateway_command in ("start", "reload") and getattr(args, "dry_run", False):
|
||||
return _gateway_dry_run(config)
|
||||
|
||||
if args.gateway_command == "start":
|
||||
if getattr(args, "dry_run", False):
|
||||
return _gateway_dry_run(config)
|
||||
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(config)
|
||||
return _gateway_reload(config)
|
||||
elif args.gateway_command == "status":
|
||||
return _gateway_status()
|
||||
@@ -98,52 +102,32 @@ def run_gateway(args: argparse.Namespace) -> int:
|
||||
|
||||
|
||||
def _gateway_dry_run(config: CastleConfig) -> int:
|
||||
"""Print generated Caddyfile and gateway unit without applying."""
|
||||
from castle_cli.commands.service import _generate_gateway_unit
|
||||
|
||||
"""Print generated Caddyfile without applying."""
|
||||
print("# Caddyfile")
|
||||
print(_generate_caddyfile(config))
|
||||
print()
|
||||
print("# castle-gateway.service")
|
||||
print(_generate_gateway_unit(config))
|
||||
return 0
|
||||
|
||||
|
||||
def _gateway_start(config: CastleConfig) -> int:
|
||||
"""Generate config and start Caddy."""
|
||||
if not shutil.which("caddy"):
|
||||
print("Error: caddy is not installed.")
|
||||
print("Install with: sudo apt install caddy")
|
||||
"""Generate config and enable the gateway service."""
|
||||
from castle_cli.commands.service import _service_enable
|
||||
|
||||
if GATEWAY_COMPONENT not in config.managed:
|
||||
print(f"Error: '{GATEWAY_COMPONENT}' not found in castle.yaml or not managed")
|
||||
return 1
|
||||
|
||||
print("Generating gateway configuration...")
|
||||
_write_generated_files(config)
|
||||
|
||||
caddyfile = GENERATED_DIR / "Caddyfile"
|
||||
print(f"\nStarting Caddy on port {config.gateway.port}...")
|
||||
|
||||
result = subprocess.run(
|
||||
["caddy", "start", "--config", str(caddyfile), "--adapter", "caddyfile"],
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"Gateway running at http://localhost:{config.gateway.port}")
|
||||
else:
|
||||
print("Failed to start gateway.")
|
||||
|
||||
return result.returncode
|
||||
print(f"\nStarting gateway on port {config.gateway.port}...")
|
||||
return _service_enable(config, GATEWAY_COMPONENT)
|
||||
|
||||
|
||||
def _gateway_stop() -> int:
|
||||
"""Stop Caddy."""
|
||||
if not shutil.which("caddy"):
|
||||
print("Error: caddy is not installed.")
|
||||
return 1
|
||||
"""Stop the gateway service."""
|
||||
from castle_cli.commands.service import _service_disable
|
||||
|
||||
result = subprocess.run(["caddy", "stop"])
|
||||
if result.returncode == 0:
|
||||
print("Gateway stopped.")
|
||||
return result.returncode
|
||||
return _service_disable(GATEWAY_COMPONENT)
|
||||
|
||||
|
||||
def _gateway_reload(config: CastleConfig) -> int:
|
||||
@@ -151,36 +135,39 @@ def _gateway_reload(config: CastleConfig) -> int:
|
||||
print("Regenerating gateway configuration...")
|
||||
_write_generated_files(config)
|
||||
|
||||
caddyfile = GENERATED_DIR / "Caddyfile"
|
||||
result = subprocess.run(
|
||||
["caddy", "reload", "--config", str(caddyfile), "--adapter", "caddyfile"],
|
||||
["systemctl", "--user", "reload", GATEWAY_UNIT],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("Gateway reloaded.")
|
||||
else:
|
||||
print("Failed to reload gateway. Is it running?")
|
||||
# Fall back to restart if reload not supported
|
||||
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 result.returncode
|
||||
return 0
|
||||
|
||||
|
||||
def _gateway_status() -> int:
|
||||
"""Show gateway status."""
|
||||
if not shutil.which("caddy"):
|
||||
print("Gateway: not installed")
|
||||
return 1
|
||||
|
||||
"""Show gateway status via systemd."""
|
||||
result = subprocess.run(
|
||||
["pgrep", "-x", "caddy"],
|
||||
capture_output=True,
|
||||
["systemctl", "--user", "is-active", GATEWAY_UNIT],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
status = result.stdout.strip()
|
||||
|
||||
if result.returncode == 0:
|
||||
if status == "active":
|
||||
print("Gateway: running")
|
||||
caddyfile = GENERATED_DIR / "Caddyfile"
|
||||
if caddyfile.exists():
|
||||
print(f" Config: {caddyfile}")
|
||||
else:
|
||||
print("Gateway: stopped")
|
||||
print(f"Gateway: {status}")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -8,7 +8,6 @@ import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from castle_cli.config import (
|
||||
GENERATED_DIR,
|
||||
CastleConfig,
|
||||
ensure_dirs,
|
||||
load_config,
|
||||
@@ -193,6 +192,13 @@ RestartSec={restart_sec}
|
||||
SuccessExitStatus=143
|
||||
"""
|
||||
|
||||
if sd and sd.exec_reload:
|
||||
reload_argv = sd.exec_reload.split()
|
||||
resolved_reload = shutil.which(reload_argv[0])
|
||||
if resolved_reload:
|
||||
reload_argv[0] = resolved_reload
|
||||
unit += f"ExecReload={' '.join(reload_argv)}\n"
|
||||
|
||||
if sd and sd.no_new_privileges:
|
||||
unit += "NoNewPrivileges=true\n"
|
||||
|
||||
@@ -234,27 +240,6 @@ WantedBy=timers.target
|
||||
"""
|
||||
|
||||
|
||||
def _generate_gateway_unit(config: CastleConfig) -> str:
|
||||
"""Generate a systemd unit for the Caddy gateway."""
|
||||
caddy_path = shutil.which("caddy") or "caddy"
|
||||
caddyfile = GENERATED_DIR / "Caddyfile"
|
||||
|
||||
return f"""[Unit]
|
||||
Description=Castle Gateway (Caddy)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={caddy_path} run --config {caddyfile} --adapter caddyfile
|
||||
ExecReload={caddy_path} reload --config {caddyfile} --adapter caddyfile
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
"""
|
||||
|
||||
|
||||
def _install_unit(unit_name: str, content: str) -> None:
|
||||
"""Write a systemd unit file."""
|
||||
SYSTEMD_USER_DIR.mkdir(parents=True, exist_ok=True)
|
||||
@@ -432,17 +417,6 @@ def _service_status(config: CastleConfig) -> int:
|
||||
port_str = f":{manifest.expose.http.internal.port}"
|
||||
print(f" {color}{status:10s}{reset} {name}{port_str}")
|
||||
|
||||
# Gateway status
|
||||
gw_unit = f"{UNIT_PREFIX}gateway.service"
|
||||
result = subprocess.run(
|
||||
["systemctl", "--user", "is-active", gw_unit],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
gw_status = result.stdout.strip()
|
||||
color = "\033[92m" if gw_status == "active" else "\033[90m"
|
||||
reset = "\033[0m"
|
||||
print(f" {color}{gw_status:10s}{reset} gateway :{config.gateway.port}")
|
||||
|
||||
print()
|
||||
return 0
|
||||
|
||||
@@ -476,21 +450,12 @@ def _services_start(config: CastleConfig) -> int:
|
||||
"""Start all managed services and gateway."""
|
||||
ensure_dirs()
|
||||
|
||||
# Generate Caddyfile before starting gateway
|
||||
from castle_cli.commands.gateway import _write_generated_files
|
||||
|
||||
print("Generating gateway configuration...")
|
||||
_write_generated_files(config)
|
||||
|
||||
if shutil.which("caddy"):
|
||||
gw_unit_name = f"{UNIT_PREFIX}gateway.service"
|
||||
gw_content = _generate_gateway_unit(config)
|
||||
_install_unit(gw_unit_name, gw_content)
|
||||
subprocess.run(["systemctl", "--user", "enable", gw_unit_name], check=False)
|
||||
subprocess.run(["systemctl", "--user", "start", gw_unit_name], check=False)
|
||||
print(f"Gateway: started on port {config.gateway.port}")
|
||||
else:
|
||||
print("Warning: caddy not installed, skipping gateway")
|
||||
|
||||
for name, manifest in config.managed.items():
|
||||
if not manifest.run:
|
||||
continue
|
||||
@@ -511,8 +476,4 @@ def _services_stop(config: CastleConfig) -> int:
|
||||
subprocess.run(["systemctl", "--user", "stop", unit_name], check=False)
|
||||
print(f" {name}: stopped")
|
||||
|
||||
gw_unit = f"{UNIT_PREFIX}gateway.service"
|
||||
subprocess.run(["systemctl", "--user", "stop", gw_unit], check=False)
|
||||
print(" gateway: stopped")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -221,6 +221,7 @@ class ProxySpec(BaseModel):
|
||||
|
||||
|
||||
class BuildSpec(BaseModel):
|
||||
working_dir: str | None = None
|
||||
commands: list[list[str]] = Field(default_factory=list)
|
||||
outputs: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user