diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8f6dc6c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +FROM python:3.12-slim AS build + +# Install build dependencies and UV +RUN apt-get update && \ + apt-get install -y build-essential curl && \ + pip install --no-cache-dir uv && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy only requirements file first to leverage Docker cache +COPY requirements.txt* ./ + +# Install dependencies using UV for faster installation +RUN if [ -f "requirements.txt" ]; then \ + uv pip install --system -r requirements.txt; \ + fi + +# Final stage +FROM python:3.12-slim + +# Accept module name and port as build arguments +ARG MODULE_NAME +ARG PORT=8080 + +# Set environment variables from build args +ENV PORT=$PORT +ENV MODULE_NAME=coordinator_node + +WORKDIR /app + +# Install only runtime dependencies (curl for healthcheck) +RUN apt-get update && \ + apt-get install -y curl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Copy installed dependencies from build stage +COPY --from=build /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/ +COPY --from=build /usr/local/bin/ /usr/local/bin/ + +# Copy project files and source code +COPY . /app/ + +# Expose port for container +EXPOSE $PORT + +# Configure healthcheck +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD curl --fail http://localhost:$PORT/koi-net/health || exit 1 + +# Start server using environment variables +# The module name is used to determine which server module to load +CMD uvicorn coordinator_node.server:app --host 0.0.0.0 --port $PORT diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..96e5265 --- /dev/null +++ b/config.yaml @@ -0,0 +1,20 @@ +server: + host: 127.0.0.1 + port: 8080 + path: /koi-net +koi_net: + node_name: coordinator + node_rid: orn:koi-net.node:coordinator+40610903-4272-4494-91fd-1e57501a0980 + node_profile: + base_url: http://coordinator:8080/koi-net + node_type: FULL + provides: + event: + - orn:koi-net.node + - orn:koi-net.edge + state: + - orn:koi-net.node + - orn:koi-net.edge + cache_directory_path: .koi + event_queues_path: .koi/coordinator/queues.json + first_contact: '' diff --git a/coordinator_node/server.py b/coordinator_node/server.py index 57d1b84..60a6bd4 100644 --- a/coordinator_node/server.py +++ b/coordinator_node/server.py @@ -1,4 +1,5 @@ import logging +import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI from koi_net.processor.knowledge_object import KnowledgeSource @@ -20,30 +21,51 @@ FETCH_BUNDLES_PATH ) from .core import node - +from fastapi.openapi.utils import get_openapi logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): - node.start() + # Schedule node.start() *after* the server is ready to accept connections + task = asyncio.create_task(_delayed_node_start()) yield node.stop() + if not task.done(): + task.cancel() + +async def _delayed_node_start(): + await asyncio.sleep(0.5) + node.start() app = FastAPI( - lifespan=lifespan, + lifespan=lifespan, root_path="/koi-net", title="KOI-net Protocol API", version="1.0.0" ) +def get_all_paths(app): + openapi_schema = get_openapi( + title=app.title, + version=app.version, + routes=app.routes, + ) + return list(openapi_schema["paths"].keys()) + +@app.get("/health", tags=["System"]) +async def health_check(): + """Basic health check for the service.""" + return {"status": "healthy", "node_id": str(node.identity.rid) if node.identity else "uninitialized"} + + @app.post(BROADCAST_EVENTS_PATH) def broadcast_events(req: EventsPayload): logger.info(f"Request to {BROADCAST_EVENTS_PATH}, received {len(req.events)} event(s)") for event in req.events: node.processor.handle(event=event, source=KnowledgeSource.External) - + @app.post(POLL_EVENTS_PATH) def poll_events(req: PollEvents) -> EventsPayload: logger.info(f"Request to {POLL_EVENTS_PATH}") @@ -61,4 +83,7 @@ def fetch_manifests(req: FetchManifests) -> ManifestsPayload: @app.post(FETCH_BUNDLES_PATH) def fetch_bundles(req: FetchBundles) -> BundlesPayload: return node.network.response_handler.fetch_bundles(req) - \ No newline at end of file + +# Example usage: +paths = get_all_paths(app) +print(f"Paths: {paths}")