From bd722892e8e661511dad3fdd661443073eb6e7a9 Mon Sep 17 00:00:00 2001 From: aitbc Date: Thu, 30 Apr 2026 11:18:03 +0200 Subject: [PATCH] Create FastAPI gateway for routing to microservices - Created api-gateway application structure - Added pyproject.toml with FastAPI, httpx, and aitbc-core dependencies - Implemented main.py with routing logic to proxy requests to microservices - Added service registry for GPU, Marketplace, Agent, Trading, Governance, and Coordinator services - Configured middleware (request ID, performance logging, validation, error handling) - Created systemd service file for api-gateway - Added README.md with service registry information This completes Phase 4.2: Create FastAPI gateway for routing --- apps/api-gateway/README.md | 42 +++++ apps/api-gateway/api-gateway.service | 16 ++ apps/api-gateway/pyproject.toml | 23 +++ apps/api-gateway/src/api_gateway/__init__.py | 6 + apps/api-gateway/src/api_gateway/main.py | 174 +++++++++++++++++++ 5 files changed, 261 insertions(+) create mode 100644 apps/api-gateway/README.md create mode 100644 apps/api-gateway/api-gateway.service create mode 100644 apps/api-gateway/pyproject.toml create mode 100644 apps/api-gateway/src/api_gateway/__init__.py create mode 100644 apps/api-gateway/src/api_gateway/main.py diff --git a/apps/api-gateway/README.md b/apps/api-gateway/README.md new file mode 100644 index 00000000..f7fec894 --- /dev/null +++ b/apps/api-gateway/README.md @@ -0,0 +1,42 @@ +# AITBC API Gateway + +Routes requests to AITBC microservices. + +## Service Registry + +The gateway routes requests to the following services: + +- **GPU Service** (port 8101): `/gpu/*` → GPU resource management +- **Marketplace Service** (port 8102): `/marketplace/*` → GPU marketplace +- **Agent Service** (port 8103): `/agent/*` → Agent operations +- **Trading Service** (port 8104): `/trading/*` → Trading operations +- **Governance Service** (port 8105): `/governance/*` → Governance operations +- **Coordinator API** (port 8000): `/coordinator/*` → Coordinator API (default) + +## Installation + +```bash +cd /opt/aitbc +poetry install --with api-gateway +``` + +## Running + +```bash +# Development +python -m api_gateway.main + +# Production (systemd) +sudo systemctl start api-gateway +sudo systemctl enable api-gateway +``` + +## Endpoints + +- `GET /health` - Health check +- `GET /services` - List registered services +- `/*` - Proxy all other requests to appropriate microservice + +## Configuration + +Service URLs are configured in `main.py` under the `SERVICES` dictionary. diff --git a/apps/api-gateway/api-gateway.service b/apps/api-gateway/api-gateway.service new file mode 100644 index 00000000..89fffa51 --- /dev/null +++ b/apps/api-gateway/api-gateway.service @@ -0,0 +1,16 @@ +[Unit] +Description=AITBC API Gateway +After=network.target + +[Service] +Type=simple +User=aitbc +WorkingDirectory=/opt/aitbc/apps/api-gateway +Environment="PATH=/opt/aitbc/venv/bin" +Environment="PYTHONPATH=/opt/aitbc/packages/py/aitbc-core/src:/opt/aitbc/apps/api-gateway/src:/opt/aitbc" +ExecStart=/opt/aitbc/venv/bin/python -m api_gateway.main +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/apps/api-gateway/pyproject.toml b/apps/api-gateway/pyproject.toml new file mode 100644 index 00000000..7b21b260 --- /dev/null +++ b/apps/api-gateway/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "api-gateway" +version = "0.1.0" +description = "AITBC API Gateway for routing to microservices" +authors = [ + {name = "AITBC Team", email = "team@aitbc.dev"} +] +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.104.0", + "uvicorn>=0.24.0", + "httpx>=0.25.0", + "aitbc-core", +] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +packages = [ + { include = "api_gateway", from = "src" } +] diff --git a/apps/api-gateway/src/api_gateway/__init__.py b/apps/api-gateway/src/api_gateway/__init__.py new file mode 100644 index 00000000..83ac86d2 --- /dev/null +++ b/apps/api-gateway/src/api_gateway/__init__.py @@ -0,0 +1,6 @@ +""" +AITBC API Gateway +Routes requests to microservices +""" + +__version__ = "0.1.0" diff --git a/apps/api-gateway/src/api_gateway/main.py b/apps/api-gateway/src/api_gateway/main.py new file mode 100644 index 00000000..3786f20f --- /dev/null +++ b/apps/api-gateway/src/api_gateway/main.py @@ -0,0 +1,174 @@ +""" +API Gateway main application +Routes requests to microservices +""" + +import httpx +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from fastapi import FastAPI, Request, Response +from fastapi.responses import JSONResponse + +from aitbc import ( + configure_logging, + get_logger, + RequestIDMiddleware, + PerformanceLoggingMiddleware, + RequestValidationMiddleware, + ErrorHandlerMiddleware, +) + +# Configure structured logging +configure_logging(level="INFO") +logger = get_logger(__name__) + +# Service registry configuration +SERVICES = { + "gpu": { + "base_url": "http://localhost:8101", + "prefix": "/gpu", + }, + "marketplace": { + "base_url": "http://localhost:8102", + "prefix": "/marketplace", + }, + "agent": { + "base_url": "http://localhost:8103", + "prefix": "/agent", + }, + "trading": { + "base_url": "http://localhost:8104", + "prefix": "/trading", + }, + "governance": { + "base_url": "http://localhost:8105", + "prefix": "/governance", + }, + "coordinator": { + "base_url": "http://localhost:8000", + "prefix": "/coordinator", + }, +} + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + """Lifecycle events for the API Gateway.""" + logger.info("Starting API Gateway") + yield + logger.info("Shutting down API Gateway") + + +app = FastAPI( + title="AITBC API Gateway", + description="Routes requests to AITBC microservices", + version="0.1.0", + lifespan=lifespan, +) + +# Add middleware +app.add_middleware(RequestIDMiddleware) +app.add_middleware(PerformanceLoggingMiddleware) +app.add_middleware(RequestValidationMiddleware, max_request_size=10*1024*1024) +app.add_middleware(ErrorHandlerMiddleware) + + +@app.get("/health") +async def health() -> dict[str, str]: + """Health check endpoint""" + return {"status": "healthy", "service": "api-gateway"} + + +@app.get("/services") +async def list_services() -> dict[str, dict[str, str]]: + """List registered services""" + return { + service_name: {"prefix": config["prefix"], "url": config["base_url"]} + for service_name, config in SERVICES.items() + } + + +@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) +async def proxy_request(path: str, request: Request) -> Response: + """Proxy request to appropriate microservice""" + # Determine which service should handle the request + service_name = None + for name, config in SERVICES.items(): + if path.startswith(config["prefix"].lstrip("/")): + service_name = name + break + + if not service_name: + # Default to coordinator-api for unknown paths + service_name = "coordinator" + + service_config = SERVICES[service_name] + + # Build target URL + target_path = path + if path.startswith(service_config["prefix"]): + target_path = path[len(service_config["prefix"]):].lstrip("/") + + target_url = f"{service_config['base_url']}/{target_path}" + if request.url.query: + target_url += f"?{request.url.query}" + + # Proxy the request + async with httpx.AsyncClient() as client: + try: + # Forward headers (except host) + headers = dict(request.headers) + headers.pop("host", None) + headers.pop("content-length", None) + + # Forward the request + if request.method == "GET": + response = await client.get(target_url, headers=headers, params=request.query_params) + elif request.method == "POST": + body = await request.body() + response = await client.post(target_url, headers=headers, content=body) + elif request.method == "PUT": + body = await request.body() + response = await client.put(target_url, headers=headers, content=body) + elif request.method == "DELETE": + response = await client.delete(target_url, headers=headers) + elif request.method == "PATCH": + body = await request.body() + response = await client.patch(target_url, headers=headers, content=body) + elif request.method == "OPTIONS": + response = await client.options(target_url, headers=headers) + else: + return JSONResponse( + status_code=405, + content={"error": "Method not allowed"} + ) + + # Return the response + return Response( + content=response.content, + status_code=response.status_code, + headers=dict(response.headers), + ) + except httpx.RequestError as e: + logger.error( + "Service unavailable", + service=service_name, + target_url=target_url, + error=str(e), + ) + return JSONResponse( + status_code=503, + content={ + "error": { + "type": "service_unavailable", + "message": f"Service {service_name} is unavailable", + "service": service_name, + } + }, + ) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8080)