refactor: Separate runtime from build. Enhance configuration and registry management, migrate to registry-based component handling
This commit is contained in:
@@ -7,9 +7,9 @@ from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from castle_core.config import load_config
|
||||
from castle_core.generators.caddyfile import generate_caddyfile_from_registry
|
||||
|
||||
from castle_api.config import settings
|
||||
from castle_api.config import get_castle_root, get_registry
|
||||
from castle_api.health import check_all_health
|
||||
from castle_api.models import (
|
||||
ComponentDetail,
|
||||
@@ -22,8 +22,43 @@ from castle_api.models import (
|
||||
router = APIRouter(tags=["dashboard"])
|
||||
|
||||
|
||||
def _summary_from_deployed(name: str, deployed: object) -> ComponentSummary:
|
||||
"""Build a ComponentSummary from a DeployedComponent."""
|
||||
managed = deployed.managed
|
||||
|
||||
systemd_info: SystemdInfo | None = None
|
||||
if managed:
|
||||
unit_name = f"castle-{name}.service"
|
||||
unit_path = str(Path("~/.config/systemd/user") / unit_name)
|
||||
has_timer = deployed.schedule is not None
|
||||
systemd_info = SystemdInfo(
|
||||
unit_name=unit_name,
|
||||
unit_path=unit_path,
|
||||
timer=has_timer,
|
||||
)
|
||||
|
||||
# Check if tool is installed on PATH
|
||||
installed: bool | None = None
|
||||
if "tool" in deployed.roles:
|
||||
installed = shutil.which(name) is not None
|
||||
|
||||
return ComponentSummary(
|
||||
id=name,
|
||||
description=deployed.description,
|
||||
roles=deployed.roles,
|
||||
runner=deployed.runner,
|
||||
port=deployed.port,
|
||||
health_path=deployed.health_path,
|
||||
proxy_path=deployed.proxy_path,
|
||||
managed=managed,
|
||||
systemd=systemd_info,
|
||||
schedule=deployed.schedule,
|
||||
installed=installed,
|
||||
)
|
||||
|
||||
|
||||
def _summary_from_manifest(name: str, manifest: object, root: Path) -> ComponentSummary:
|
||||
"""Build a ComponentSummary from a manifest."""
|
||||
"""Build a ComponentSummary from a manifest (for non-deployed components)."""
|
||||
port = None
|
||||
health_path = None
|
||||
proxy_path = None
|
||||
@@ -37,26 +72,25 @@ def _summary_from_manifest(name: str, manifest: object, root: Path) -> Component
|
||||
manifest.manage and manifest.manage.systemd and manifest.manage.systemd.enable
|
||||
)
|
||||
|
||||
# Systemd info for managed components
|
||||
systemd_info: SystemdInfo | None = None
|
||||
if managed:
|
||||
unit_name = f"castle-{name}.service"
|
||||
unit_path = str(Path("~/.config/systemd/user") / unit_name)
|
||||
has_timer = any(getattr(t, "type", None) == "schedule" for t in manifest.triggers)
|
||||
has_timer = any(
|
||||
getattr(t, "type", None) == "schedule" for t in manifest.triggers
|
||||
)
|
||||
systemd_info = SystemdInfo(
|
||||
unit_name=unit_name,
|
||||
unit_path=unit_path,
|
||||
timer=has_timer,
|
||||
)
|
||||
|
||||
# Extract cron schedule from first schedule trigger, if any
|
||||
schedule = None
|
||||
for t in manifest.triggers:
|
||||
if t.type == "schedule":
|
||||
schedule = t.cron
|
||||
break
|
||||
|
||||
# Infer runner — from run block or from tool source
|
||||
runner = manifest.run.runner if manifest.run else None
|
||||
if runner is None and manifest.tool and manifest.tool.source:
|
||||
source_dir = root / manifest.tool.source
|
||||
@@ -65,7 +99,6 @@ def _summary_from_manifest(name: str, manifest: object, root: Path) -> Component
|
||||
elif source_dir.is_file():
|
||||
runner = "command"
|
||||
|
||||
# Check if tool is actually installed on PATH
|
||||
installed: bool | None = None
|
||||
if manifest.install and manifest.install.path:
|
||||
alias = manifest.install.path.alias or name
|
||||
@@ -91,53 +124,95 @@ def _summary_from_manifest(name: str, manifest: object, root: Path) -> Component
|
||||
|
||||
@router.get("/components", response_model=list[ComponentSummary])
|
||||
def list_components() -> list[ComponentSummary]:
|
||||
"""List all registered components."""
|
||||
config = load_config(settings.castle_root)
|
||||
return [
|
||||
_summary_from_manifest(name, m, config.root)
|
||||
for name, m in config.components.items()
|
||||
]
|
||||
"""List all components — deployed from registry, non-deployed from castle.yaml."""
|
||||
registry = get_registry()
|
||||
summaries: list[ComponentSummary] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
# Deployed components from registry
|
||||
for name, deployed in registry.deployed.items():
|
||||
summaries.append(_summary_from_deployed(name, deployed))
|
||||
seen.add(name)
|
||||
|
||||
# Non-deployed components from castle.yaml (if repo available)
|
||||
root = get_castle_root()
|
||||
if root:
|
||||
try:
|
||||
from castle_core.config import load_config
|
||||
|
||||
config = load_config(root)
|
||||
for name, manifest in config.components.items():
|
||||
if name not in seen:
|
||||
summaries.append(_summary_from_manifest(name, manifest, root))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return summaries
|
||||
|
||||
|
||||
@router.get("/components/{name}", response_model=ComponentDetail)
|
||||
def get_component(name: str) -> ComponentDetail:
|
||||
"""Get detailed info for a single component."""
|
||||
config = load_config(settings.castle_root)
|
||||
if name not in config.components:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Component '{name}' not found",
|
||||
)
|
||||
manifest = config.components[name]
|
||||
summary = _summary_from_manifest(name, manifest, config.root)
|
||||
raw = manifest.model_dump(mode="json", exclude_none=True)
|
||||
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
||||
registry = get_registry()
|
||||
|
||||
if name in registry.deployed:
|
||||
deployed = registry.deployed[name]
|
||||
summary = _summary_from_deployed(name, deployed)
|
||||
raw = {
|
||||
"runner": deployed.runner,
|
||||
"run_cmd": deployed.run_cmd,
|
||||
"env": deployed.env,
|
||||
"port": deployed.port,
|
||||
"health_path": deployed.health_path,
|
||||
"proxy_path": deployed.proxy_path,
|
||||
"managed": deployed.managed,
|
||||
"roles": deployed.roles,
|
||||
}
|
||||
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
||||
|
||||
# Fall back to castle.yaml
|
||||
root = get_castle_root()
|
||||
if root:
|
||||
from castle_core.config import load_config
|
||||
|
||||
config = load_config(root)
|
||||
if name in config.components:
|
||||
manifest = config.components[name]
|
||||
summary = _summary_from_manifest(name, manifest, root)
|
||||
raw = manifest.model_dump(mode="json", exclude_none=True)
|
||||
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Component '{name}' not found",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status", response_model=StatusResponse)
|
||||
async def get_status() -> StatusResponse:
|
||||
"""Get live health status for all exposed services."""
|
||||
config = load_config(settings.castle_root)
|
||||
statuses = await check_all_health(config)
|
||||
"""Get live health status for all deployed services."""
|
||||
registry = get_registry()
|
||||
statuses = await check_all_health(registry)
|
||||
return StatusResponse(statuses=statuses)
|
||||
|
||||
|
||||
@router.get("/gateway", response_model=GatewayInfo)
|
||||
def get_gateway() -> GatewayInfo:
|
||||
"""Get gateway configuration summary."""
|
||||
config = load_config(settings.castle_root)
|
||||
registry = get_registry()
|
||||
deployed_count = len(registry.deployed)
|
||||
service_count = sum(1 for d in registry.deployed.values() if d.port is not None)
|
||||
managed_count = sum(1 for d in registry.deployed.values() if d.managed)
|
||||
return GatewayInfo(
|
||||
port=config.gateway.port,
|
||||
component_count=len(config.components),
|
||||
service_count=len(config.services),
|
||||
managed_count=len(config.managed),
|
||||
port=registry.node.gateway_port,
|
||||
component_count=deployed_count,
|
||||
service_count=service_count,
|
||||
managed_count=managed_count,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/gateway/caddyfile")
|
||||
def get_caddyfile() -> dict[str, str]:
|
||||
"""Return the generated Caddyfile content."""
|
||||
from castle_core.generators import generate_caddyfile
|
||||
|
||||
config = load_config(settings.castle_root)
|
||||
return {"content": generate_caddyfile(config)}
|
||||
registry = get_registry()
|
||||
return {"content": generate_caddyfile_from_registry(registry)}
|
||||
|
||||
Reference in New Issue
Block a user