feat: Implement multi-node support with MQTT and mDNS for service discovery and coordination
This commit is contained in:
115
castle-api/src/castle_api/nodes.py
Normal file
115
castle-api/src/castle_api/nodes.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Nodes router — discover and inspect mesh nodes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
|
||||
router = APIRouter(tags=["nodes"])
|
||||
|
||||
|
||||
def _local_node_summary(registry: object) -> NodeSummary:
|
||||
"""Build a NodeSummary for the local node from the registry."""
|
||||
return NodeSummary(
|
||||
hostname=registry.node.hostname,
|
||||
gateway_port=registry.node.gateway_port,
|
||||
deployed_count=len(registry.deployed),
|
||||
service_count=sum(1 for d in registry.deployed.values() if d.port is not None),
|
||||
is_local=True,
|
||||
online=True,
|
||||
is_stale=False,
|
||||
)
|
||||
|
||||
|
||||
def _remote_node_summary(hostname: str, remote: object) -> NodeSummary:
|
||||
"""Build a NodeSummary from a RemoteNode."""
|
||||
reg = remote.registry
|
||||
return NodeSummary(
|
||||
hostname=hostname,
|
||||
gateway_port=reg.node.gateway_port,
|
||||
deployed_count=len(reg.deployed),
|
||||
service_count=sum(1 for d in reg.deployed.values() if d.port is not None),
|
||||
is_local=False,
|
||||
online=remote.online,
|
||||
is_stale=remote.is_stale,
|
||||
last_seen=remote.last_seen,
|
||||
)
|
||||
|
||||
|
||||
def _deployed_to_summaries(registry: object, hostname: str) -> list[ComponentSummary]:
|
||||
"""Convert deployed components from a registry into ComponentSummary list."""
|
||||
summaries = []
|
||||
for name, d in registry.deployed.items():
|
||||
summaries.append(
|
||||
ComponentSummary(
|
||||
id=name,
|
||||
description=d.description,
|
||||
category=d.category,
|
||||
runner=d.runner,
|
||||
port=d.port,
|
||||
health_path=d.health_path,
|
||||
proxy_path=d.proxy_path,
|
||||
managed=d.managed,
|
||||
schedule=d.schedule,
|
||||
node=hostname,
|
||||
)
|
||||
)
|
||||
return summaries
|
||||
|
||||
|
||||
@router.get("/mesh/status", response_model=MeshStatus)
|
||||
def get_mesh_status(request: Request) -> MeshStatus:
|
||||
"""Get the current state of the mesh coordination layer."""
|
||||
mqtt_client = getattr(request.app.state, "mqtt_client", None)
|
||||
mdns = getattr(request.app.state, "mdns", None)
|
||||
|
||||
peers = list(mesh_state.all_nodes(include_stale=True).keys())
|
||||
|
||||
return MeshStatus(
|
||||
enabled=settings.mqtt_enabled,
|
||||
mqtt_connected=mqtt_client.connected if mqtt_client else False,
|
||||
mqtt_broker_host=mqtt_client.broker_host if mqtt_client else None,
|
||||
mqtt_broker_port=mqtt_client.broker_port if mqtt_client else None,
|
||||
mdns_enabled=settings.mdns_enabled,
|
||||
peer_count=len(peers),
|
||||
peers=peers,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/nodes", response_model=list[NodeSummary])
|
||||
def list_nodes() -> list[NodeSummary]:
|
||||
"""List all known nodes (local + discovered remote)."""
|
||||
registry = get_registry()
|
||||
nodes = [_local_node_summary(registry)]
|
||||
|
||||
for hostname, remote in mesh_state.all_nodes(include_stale=True).items():
|
||||
nodes.append(_remote_node_summary(hostname, remote))
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
@router.get("/nodes/{hostname}", response_model=NodeDetail)
|
||||
def get_node(hostname: str) -> NodeDetail:
|
||||
"""Get detailed info for a specific node."""
|
||||
registry = get_registry()
|
||||
|
||||
# Local node
|
||||
if hostname == registry.node.hostname:
|
||||
summary = _local_node_summary(registry)
|
||||
deployed = _deployed_to_summaries(registry, hostname)
|
||||
return NodeDetail(**summary.model_dump(), deployed=deployed)
|
||||
|
||||
# Remote node
|
||||
remote = mesh_state.get_node(hostname)
|
||||
if remote is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Node '{hostname}' not found",
|
||||
)
|
||||
|
||||
summary = _remote_node_summary(hostname, remote)
|
||||
deployed = _deployed_to_summaries(remote.registry, hostname)
|
||||
return NodeDetail(**summary.model_dump(), deployed=deployed)
|
||||
Reference in New Issue
Block a user