refactor: Align vocabulary — component → program / deployment
Canonical terms (see docs/registry.md glossary): - program: any catalog entry (tool/daemon/frontend) — the software - service / job: how a program is deployed (systemd .service / .timer) - deployment: umbrella for the unified service+job+program view Changes: - core: ServiceSpec/JobSpec component: → program: (component accepted as back-compat validation_alias); DeployedComponent → Deployment. - api: ComponentSummary/Detail → DeploymentSummary/Detail; GET /components → /deployments; GatewayRoute.component → program; GatewayInfo .component_count → deployment_count; ServiceActionResponse/action keys component → program. - app: matching type renames, /components → /deployments hook, route /component/:name → /deployment/:name, GatewayRoute.program. - docs: component-registry.md → registry.md (+ canonical glossary); CLAUDE.md endpoint list refreshed (drop removed /tools, add typed program/service/job + config routes). Tests: core 92, cli 24, api 52 green; app build clean; ruff clean.
This commit is contained in:
@@ -65,7 +65,7 @@ async def get_logs(
|
||||
)
|
||||
stdout, _ = await proc.communicate()
|
||||
lines = (stdout or b"").decode().splitlines()
|
||||
return {"component": name, "lines": lines}
|
||||
return {"name": name, "lines": lines}
|
||||
|
||||
|
||||
async def _follow_logs(unit: str, n: int) -> AsyncGenerator[str, None]:
|
||||
|
||||
@@ -11,7 +11,7 @@ class SystemdInfo(BaseModel):
|
||||
timer: bool = False
|
||||
|
||||
|
||||
class ComponentSummary(BaseModel):
|
||||
class DeploymentSummary(BaseModel):
|
||||
"""Summary of a single component."""
|
||||
|
||||
id: str
|
||||
@@ -37,7 +37,7 @@ class ComponentSummary(BaseModel):
|
||||
node: str | None = None
|
||||
|
||||
|
||||
class ComponentDetail(ComponentSummary):
|
||||
class DeploymentDetail(DeploymentSummary):
|
||||
"""Full detail for a single component, including raw manifest."""
|
||||
|
||||
manifest: dict
|
||||
@@ -130,7 +130,7 @@ class GatewayRoute(BaseModel):
|
||||
|
||||
path: str
|
||||
target_port: int
|
||||
component: str
|
||||
program: str
|
||||
node: str
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class GatewayInfo(BaseModel):
|
||||
|
||||
port: int
|
||||
hostname: str
|
||||
component_count: int
|
||||
deployment_count: int
|
||||
service_count: int
|
||||
managed_count: int
|
||||
routes: list[GatewayRoute] = []
|
||||
@@ -161,7 +161,7 @@ class NodeSummary(BaseModel):
|
||||
class NodeDetail(NodeSummary):
|
||||
"""Full detail for a node, including its deployed components."""
|
||||
|
||||
deployed: list[ComponentSummary] = []
|
||||
deployed: list[DeploymentSummary] = []
|
||||
|
||||
|
||||
class MeshStatus(BaseModel):
|
||||
@@ -179,6 +179,6 @@ class MeshStatus(BaseModel):
|
||||
class ServiceActionResponse(BaseModel):
|
||||
"""Response from a service management action."""
|
||||
|
||||
component: str
|
||||
program: str
|
||||
action: str
|
||||
status: str
|
||||
|
||||
@@ -16,7 +16,7 @@ import logging
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from castle_core.registry import (
|
||||
DeployedComponent,
|
||||
Deployment,
|
||||
NodeConfig,
|
||||
NodeRegistry,
|
||||
)
|
||||
@@ -74,9 +74,9 @@ def _json_to_registry(payload: str) -> NodeRegistry:
|
||||
castle_root=node_data.get("castle_root"),
|
||||
gateway_port=node_data.get("gateway_port", 9000),
|
||||
)
|
||||
deployed: dict[str, DeployedComponent] = {}
|
||||
deployed: dict[str, Deployment] = {}
|
||||
for name, comp_data in data.get("deployed", {}).items():
|
||||
deployed[name] = DeployedComponent(
|
||||
deployed[name] = Deployment(
|
||||
runner=comp_data.get("runner", "command"),
|
||||
run_cmd=comp_data.get("run_cmd", []),
|
||||
env=comp_data.get("env", {}),
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException, Request, status
|
||||
|
||||
from castle_api.config import get_registry, settings
|
||||
from castle_api.mesh import mesh_state
|
||||
from castle_api.models import ComponentSummary, MeshStatus, NodeDetail, NodeSummary
|
||||
from castle_api.models import DeploymentSummary, MeshStatus, NodeDetail, NodeSummary
|
||||
|
||||
router = APIRouter(tags=["nodes"])
|
||||
|
||||
@@ -39,12 +39,12 @@ def _remote_node_summary(hostname: str, remote: object) -> NodeSummary:
|
||||
)
|
||||
|
||||
|
||||
def _deployed_to_summaries(registry: object, hostname: str) -> list[ComponentSummary]:
|
||||
"""Convert deployed components from a registry into ComponentSummary list."""
|
||||
def _deployed_to_summaries(registry: object, hostname: str) -> list[DeploymentSummary]:
|
||||
"""Convert deployed components from a registry into DeploymentSummary list."""
|
||||
summaries = []
|
||||
for name, d in registry.deployed.items():
|
||||
summaries.append(
|
||||
ComponentSummary(
|
||||
DeploymentSummary(
|
||||
id=name,
|
||||
category="job" if d.schedule else "service",
|
||||
description=d.description,
|
||||
|
||||
@@ -58,7 +58,7 @@ async def program_action(name: str, action: str) -> dict:
|
||||
raise HTTPException(status_code=500, detail=result.output or f"{action} failed")
|
||||
|
||||
return {
|
||||
"component": result.component,
|
||||
"program": result.program,
|
||||
"action": result.action,
|
||||
"status": result.status,
|
||||
"output": result.output,
|
||||
|
||||
@@ -17,8 +17,8 @@ from castle_api.config import get_castle_root, get_registry
|
||||
from castle_api.mesh import mesh_state
|
||||
from castle_api.health import check_all_health
|
||||
from castle_api.models import (
|
||||
ComponentDetail,
|
||||
ComponentSummary,
|
||||
DeploymentDetail,
|
||||
DeploymentSummary,
|
||||
GatewayInfo,
|
||||
GatewayRoute,
|
||||
JobDetail,
|
||||
@@ -47,8 +47,8 @@ def _declared_commands_dict(comp: ProgramSpec) -> dict[str, list[list[str]]] | N
|
||||
return out or None
|
||||
|
||||
|
||||
def _summary_from_deployed(name: str, deployed: object) -> ComponentSummary:
|
||||
"""Build a ComponentSummary from a DeployedComponent."""
|
||||
def _summary_from_deployed(name: str, deployed: object) -> DeploymentSummary:
|
||||
"""Build a DeploymentSummary from a Deployment."""
|
||||
managed = deployed.managed
|
||||
|
||||
systemd_info: SystemdInfo | None = None
|
||||
@@ -69,7 +69,7 @@ def _summary_from_deployed(name: str, deployed: object) -> ComponentSummary:
|
||||
|
||||
category = "job" if deployed.schedule else "service"
|
||||
|
||||
return ComponentSummary(
|
||||
return DeploymentSummary(
|
||||
id=name,
|
||||
category=category,
|
||||
description=deployed.description,
|
||||
@@ -88,8 +88,8 @@ def _summary_from_deployed(name: str, deployed: object) -> ComponentSummary:
|
||||
|
||||
def _summary_from_service(
|
||||
name: str, svc: ServiceSpec, config: object
|
||||
) -> ComponentSummary:
|
||||
"""Build a ComponentSummary from a ServiceSpec (non-deployed)."""
|
||||
) -> DeploymentSummary:
|
||||
"""Build a DeploymentSummary from a ServiceSpec (non-deployed)."""
|
||||
port = None
|
||||
health_path = None
|
||||
proxy_path = None
|
||||
@@ -112,8 +112,8 @@ def _summary_from_service(
|
||||
description = svc.description
|
||||
source = None
|
||||
stack = None
|
||||
if svc.component and svc.component in config.programs:
|
||||
comp = config.programs[svc.component]
|
||||
if svc.program and svc.program in config.programs:
|
||||
comp = config.programs[svc.program]
|
||||
if not description:
|
||||
description = comp.description
|
||||
source = comp.source
|
||||
@@ -121,7 +121,7 @@ def _summary_from_service(
|
||||
|
||||
runner = svc.run.runner
|
||||
|
||||
return ComponentSummary(
|
||||
return DeploymentSummary(
|
||||
id=name,
|
||||
category="service",
|
||||
description=description,
|
||||
@@ -137,8 +137,8 @@ def _summary_from_service(
|
||||
)
|
||||
|
||||
|
||||
def _summary_from_job(name: str, job: JobSpec, config: object) -> ComponentSummary:
|
||||
"""Build a ComponentSummary from a JobSpec (non-deployed)."""
|
||||
def _summary_from_job(name: str, job: JobSpec, config: object) -> DeploymentSummary:
|
||||
"""Build a DeploymentSummary from a JobSpec (non-deployed)."""
|
||||
managed = bool(job.manage and job.manage.systemd and job.manage.systemd.enable)
|
||||
|
||||
systemd_info: SystemdInfo | None = None
|
||||
@@ -150,14 +150,14 @@ def _summary_from_job(name: str, job: JobSpec, config: object) -> ComponentSumma
|
||||
description = job.description
|
||||
source = None
|
||||
stack = None
|
||||
if job.component and job.component in config.programs:
|
||||
comp = config.programs[job.component]
|
||||
if job.program and job.program in config.programs:
|
||||
comp = config.programs[job.program]
|
||||
if not description:
|
||||
description = comp.description
|
||||
source = comp.source
|
||||
stack = comp.stack
|
||||
|
||||
return ComponentSummary(
|
||||
return DeploymentSummary(
|
||||
id=name,
|
||||
category="job",
|
||||
description=description,
|
||||
@@ -171,8 +171,8 @@ def _summary_from_job(name: str, job: JobSpec, config: object) -> ComponentSumma
|
||||
)
|
||||
|
||||
|
||||
def _summary_from_program(name: str, comp: ProgramSpec, root: Path) -> ComponentSummary:
|
||||
"""Build a ComponentSummary from a ProgramSpec (tools/frontends)."""
|
||||
def _summary_from_program(name: str, comp: ProgramSpec, root: Path) -> DeploymentSummary:
|
||||
"""Build a DeploymentSummary from a ProgramSpec (tools/frontends)."""
|
||||
source = comp.source
|
||||
|
||||
# Infer runner from source directory
|
||||
@@ -188,7 +188,7 @@ def _summary_from_program(name: str, comp: ProgramSpec, root: Path) -> Component
|
||||
if comp.source and (comp.stack or comp.commands):
|
||||
installed = shutil.which(name) is not None
|
||||
|
||||
return ComponentSummary(
|
||||
return DeploymentSummary(
|
||||
id=name,
|
||||
category="program",
|
||||
description=comp.description,
|
||||
@@ -222,16 +222,16 @@ def _backfill_source(name: str, config: object) -> str | None:
|
||||
return config.programs[name].source
|
||||
ref = None
|
||||
if name in config.services:
|
||||
ref = config.services[name].component
|
||||
ref = config.services[name].program
|
||||
elif name in config.jobs:
|
||||
ref = config.jobs[name].component
|
||||
ref = config.jobs[name].program
|
||||
if ref and ref in config.programs:
|
||||
return config.programs[ref].source
|
||||
return None
|
||||
|
||||
|
||||
def _service_from_deployed(name: str, deployed: object) -> ServiceSummary:
|
||||
"""Build a ServiceSummary from a DeployedComponent."""
|
||||
"""Build a ServiceSummary from a Deployment."""
|
||||
systemd_info = _make_systemd_info(name) if deployed.managed else None
|
||||
return ServiceSummary(
|
||||
id=name,
|
||||
@@ -263,8 +263,8 @@ def _service_from_spec(name: str, svc: ServiceSpec, config: object) -> ServiceSu
|
||||
description = svc.description
|
||||
source = None
|
||||
stack = None
|
||||
if svc.component and svc.component in config.programs:
|
||||
comp = config.programs[svc.component]
|
||||
if svc.program and svc.program in config.programs:
|
||||
comp = config.programs[svc.program]
|
||||
if not description:
|
||||
description = comp.description
|
||||
source = comp.source
|
||||
@@ -285,7 +285,7 @@ def _service_from_spec(name: str, svc: ServiceSpec, config: object) -> ServiceSu
|
||||
|
||||
|
||||
def _job_from_deployed(name: str, deployed: object) -> JobSummary:
|
||||
"""Build a JobSummary from a DeployedComponent."""
|
||||
"""Build a JobSummary from a Deployment."""
|
||||
systemd_info = _make_systemd_info(name, timer=True) if deployed.managed else None
|
||||
return JobSummary(
|
||||
id=name,
|
||||
@@ -306,8 +306,8 @@ def _job_from_spec(name: str, job: JobSpec, config: object) -> JobSummary:
|
||||
description = job.description
|
||||
source = None
|
||||
stack = None
|
||||
if job.component and job.component in config.programs:
|
||||
comp = config.programs[job.component]
|
||||
if job.program and job.program in config.programs:
|
||||
comp = config.programs[job.program]
|
||||
if not description:
|
||||
description = comp.description
|
||||
source = comp.source
|
||||
@@ -636,15 +636,15 @@ def get_program(name: str) -> ProgramDetail:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/components", response_model=list[ComponentSummary])
|
||||
def list_components(include_remote: bool = False) -> list[ComponentSummary]:
|
||||
@router.get("/deployments", response_model=list[DeploymentSummary])
|
||||
def list_components(include_remote: bool = False) -> list[DeploymentSummary]:
|
||||
"""List all components — deployed from registry, non-deployed from castle.yaml.
|
||||
|
||||
Pass ?include_remote=true to include components from remote mesh nodes.
|
||||
"""
|
||||
registry = get_registry()
|
||||
local_hostname = registry.node.hostname
|
||||
summaries: list[ComponentSummary] = []
|
||||
summaries: list[DeploymentSummary] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
# Deployed components from registry
|
||||
@@ -686,9 +686,9 @@ def list_components(include_remote: bool = False) -> list[ComponentSummary]:
|
||||
# Check if a service/job references a program
|
||||
ref = None
|
||||
if s.id in config.services:
|
||||
ref = config.services[s.id].component
|
||||
ref = config.services[s.id].program
|
||||
elif s.id in config.jobs:
|
||||
ref = config.jobs[s.id].component
|
||||
ref = config.jobs[s.id].program
|
||||
if ref and ref in config.programs:
|
||||
s.source = config.programs[ref].source
|
||||
|
||||
@@ -708,7 +708,7 @@ def list_components(include_remote: bool = False) -> list[ComponentSummary]:
|
||||
for name, d in remote.registry.deployed.items():
|
||||
if name not in seen:
|
||||
summaries.append(
|
||||
ComponentSummary(
|
||||
DeploymentSummary(
|
||||
id=name,
|
||||
category="job" if d.schedule else "service",
|
||||
description=d.description,
|
||||
@@ -728,8 +728,8 @@ def list_components(include_remote: bool = False) -> list[ComponentSummary]:
|
||||
return summaries
|
||||
|
||||
|
||||
@router.get("/components/{name}", response_model=ComponentDetail)
|
||||
def get_component(name: str) -> ComponentDetail:
|
||||
@router.get("/deployments/{name}", response_model=DeploymentDetail)
|
||||
def get_component(name: str) -> DeploymentDetail:
|
||||
"""Get detailed info for a single component."""
|
||||
registry = get_registry()
|
||||
|
||||
@@ -749,9 +749,9 @@ def get_component(name: str) -> ComponentDetail:
|
||||
else:
|
||||
ref = None
|
||||
if name in config.services:
|
||||
ref = config.services[name].component
|
||||
ref = config.services[name].program
|
||||
elif name in config.jobs:
|
||||
ref = config.jobs[name].component
|
||||
ref = config.jobs[name].program
|
||||
if ref and ref in config.programs:
|
||||
summary.source = config.programs[ref].source
|
||||
except FileNotFoundError:
|
||||
@@ -768,7 +768,7 @@ def get_component(name: str) -> ComponentDetail:
|
||||
"behavior": deployed.behavior,
|
||||
"stack": deployed.stack,
|
||||
}
|
||||
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
||||
return DeploymentDetail(**summary.model_dump(), manifest=raw)
|
||||
|
||||
# Fall back to castle.yaml
|
||||
root = get_castle_root()
|
||||
@@ -781,23 +781,23 @@ def get_component(name: str) -> ComponentDetail:
|
||||
svc = config.services[name]
|
||||
summary = _summary_from_service(name, svc, config)
|
||||
raw = svc.model_dump(mode="json", exclude_none=True)
|
||||
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
||||
return DeploymentDetail(**summary.model_dump(), manifest=raw)
|
||||
|
||||
if name in config.jobs:
|
||||
job = config.jobs[name]
|
||||
summary = _summary_from_job(name, job, config)
|
||||
raw = job.model_dump(mode="json", exclude_none=True)
|
||||
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
||||
return DeploymentDetail(**summary.model_dump(), manifest=raw)
|
||||
|
||||
if name in config.programs:
|
||||
comp = config.programs[name]
|
||||
summary = _summary_from_program(name, comp, root)
|
||||
raw = comp.model_dump(mode="json", exclude_none=True)
|
||||
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
||||
return DeploymentDetail(**summary.model_dump(), manifest=raw)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Component '{name}' not found",
|
||||
detail=f"'{name}' not found",
|
||||
)
|
||||
|
||||
|
||||
@@ -822,7 +822,7 @@ def get_gateway() -> GatewayInfo:
|
||||
GatewayRoute(
|
||||
path=d.proxy_path,
|
||||
target_port=d.port,
|
||||
component=name,
|
||||
program=name,
|
||||
node=registry.node.hostname,
|
||||
)
|
||||
for name, d in registry.deployed.items()
|
||||
@@ -838,7 +838,7 @@ def get_gateway() -> GatewayInfo:
|
||||
GatewayRoute(
|
||||
path=d.proxy_path,
|
||||
target_port=d.port,
|
||||
component=name,
|
||||
program=name,
|
||||
node=hostname,
|
||||
)
|
||||
)
|
||||
@@ -848,7 +848,7 @@ def get_gateway() -> GatewayInfo:
|
||||
return GatewayInfo(
|
||||
port=registry.node.gateway_port,
|
||||
hostname=registry.node.hostname,
|
||||
component_count=deployed_count,
|
||||
deployment_count=deployed_count,
|
||||
service_count=service_count,
|
||||
managed_count=managed_count,
|
||||
routes=routes,
|
||||
|
||||
@@ -108,7 +108,7 @@ async def _do_action(name: str, action: str) -> JSONResponse:
|
||||
asyncio.create_task(_deferred_systemctl(action, unit))
|
||||
return JSONResponse(
|
||||
status_code=202,
|
||||
content={"component": name, "action": action, "status": "accepted"},
|
||||
content={"program": name, "action": action, "status": "accepted"},
|
||||
)
|
||||
|
||||
ok, output = await _systemctl(action, unit)
|
||||
@@ -120,7 +120,7 @@ async def _do_action(name: str, action: str) -> JSONResponse:
|
||||
await _broadcast_health_with_override(name, unit_status)
|
||||
|
||||
return JSONResponse(
|
||||
content={"component": name, "action": action, "status": unit_status},
|
||||
content={"program": name, "action": action, "status": unit_status},
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user