Uvicorn's h11/httptools HTTP protocols don't populate scope['extensions']['tls'], so /swarm/heartbeat's per-request cert pinning was 403ing every call despite CERT_REQUIRED validating the cert at handshake. Patch RequestResponseCycle.__init__ on both protocol modules to read the peer cert off the asyncio transport and write DER bytes into scope['extensions']['tls']['client_cert_chain']. Importing the module from swarm_api.py auto-installs the patch in the swarmctl uvicorn worker before any request is served.
68 lines
2.1 KiB
Python
68 lines
2.1 KiB
Python
"""DECNET SWARM Controller — master-side control plane.
|
|
|
|
Runs as an independent FastAPI/uvicorn process. Isolated from
|
|
``decnet.web.api`` so controller failure cannot cascade to the main API,
|
|
ingester, or dashboard (mirrors the existing pattern used by
|
|
``decnet api`` with ``start_new_session=True``).
|
|
|
|
Responsibilities:
|
|
* host enrollment (issues CA-signed worker bundles);
|
|
* dispatching DecnetConfig shards to worker agents over mTLS;
|
|
* active health probes of enrolled workers.
|
|
|
|
The controller *reuses* the same ``get_repo`` dependency as the main API,
|
|
so SwarmHost / DeckyShard state is visible to both processes via the
|
|
shared DB.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from decnet.web import _uvicorn_tls_scope # noqa: F401 # patches uvicorn on import
|
|
|
|
from contextlib import asynccontextmanager
|
|
from typing import AsyncGenerator
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import ORJSONResponse
|
|
|
|
from decnet.logging import get_logger
|
|
from decnet.swarm import pki
|
|
from decnet.swarm.client import ensure_master_identity
|
|
from decnet.web.dependencies import repo
|
|
from decnet.web.router.swarm import swarm_router
|
|
|
|
log = get_logger("swarm_api")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
log.info("swarm-controller starting up")
|
|
# Make sure the CA and master client cert exist before we accept any
|
|
# request — enrollment needs them and AgentClient needs them.
|
|
pki.ensure_ca()
|
|
ensure_master_identity()
|
|
await repo.initialize()
|
|
log.info("swarm-controller ready")
|
|
yield
|
|
log.info("swarm-controller shutdown")
|
|
|
|
|
|
app: FastAPI = FastAPI(
|
|
title="DECNET SWARM Controller",
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
default_response_class=ORJSONResponse,
|
|
# No interactive docs: the controller is an internal management plane,
|
|
# not a public surface. Enable explicitly in dev if needed.
|
|
docs_url=None,
|
|
redoc_url=None,
|
|
openapi_url=None,
|
|
)
|
|
|
|
app.include_router(swarm_router)
|
|
|
|
|
|
@app.get("/health")
|
|
async def root_health() -> dict[str, str]:
|
|
"""Top-level liveness probe (no DB I/O)."""
|
|
return {"status": "ok", "role": "swarm-controller"}
|