routes.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import asyncio
  2. from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
  3. from sqlalchemy import text
  4. from sqlalchemy.orm import Session
  5. from core_domain import ServiceDescriptor, ServiceHealth
  6. from app.bootstrap.settings import ApiGatewaySettings
  7. from app.db.session import get_db
  8. from app.domain.repositories import ApiKeyRepository, GatewayRequestAuditRepository
  9. from app.infrastructure.api_keys import generate_api_key, get_api_key_prefix, hash_api_key
  10. from app.infrastructure.proxy import ProxyServiceName, ProxyTarget, ServiceProxy
  11. from app.schemas.gateway import (
  12. ApiKeyCreateRequest,
  13. ApiKeyCreateResponse,
  14. ApiKeyResponse,
  15. ApiKeyStatusUpdateRequest,
  16. GatewayRequestAuditResponse,
  17. GatewayServicesHealthResponse,
  18. )
  19. router = APIRouter()
  20. @router.get("/health", response_model=ServiceDescriptor)
  21. def health_check(db: Session = Depends(get_db)) -> ServiceDescriptor:
  22. db.execute(text("SELECT 1"))
  23. return ServiceDescriptor(name="api-gateway")
  24. @router.get("/ready", response_model=ServiceHealth)
  25. def readiness_check(db: Session = Depends(get_db)) -> ServiceHealth:
  26. db.execute(text("SELECT 1"))
  27. return ServiceHealth(service="api-gateway", status="ok", database="ok")
  28. @router.post("/gateway/api-keys", response_model=ApiKeyCreateResponse)
  29. def create_api_key(
  30. payload: ApiKeyCreateRequest,
  31. db: Session = Depends(get_db),
  32. ) -> ApiKeyCreateResponse:
  33. api_key = generate_api_key()
  34. entity = ApiKeyRepository(db).create(
  35. tenant_id=payload.tenant_id,
  36. name=payload.name,
  37. key_prefix=get_api_key_prefix(api_key),
  38. key_hash=hash_api_key(api_key),
  39. scopes=payload.scopes,
  40. expires_time=payload.expires_time,
  41. )
  42. return ApiKeyCreateResponse(
  43. id=entity.id,
  44. tenant_id=entity.tenant_id,
  45. name=entity.name,
  46. key_prefix=entity.key_prefix,
  47. api_key=api_key,
  48. status=entity.status,
  49. scopes=entity.scopes,
  50. expires_time=entity.expires_time,
  51. created_time=entity.created_time,
  52. )
  53. @router.get("/gateway/api-keys", response_model=list[ApiKeyResponse])
  54. def list_api_keys(
  55. tenant_id: str = Query(...),
  56. db: Session = Depends(get_db),
  57. ) -> list[ApiKeyResponse]:
  58. return [
  59. ApiKeyResponse.from_entity(item)
  60. for item in ApiKeyRepository(db).list_by_tenant(tenant_id=tenant_id)
  61. ]
  62. @router.patch("/gateway/api-keys/{api_key_id}/status", response_model=ApiKeyResponse)
  63. def update_api_key_status(
  64. api_key_id: str,
  65. payload: ApiKeyStatusUpdateRequest,
  66. db: Session = Depends(get_db),
  67. ) -> ApiKeyResponse:
  68. entity = ApiKeyRepository(db).update_status(
  69. tenant_id=payload.tenant_id,
  70. api_key_id=api_key_id,
  71. status=payload.status,
  72. )
  73. if entity is None:
  74. raise HTTPException(status_code=404, detail=f"api key not found: {api_key_id}")
  75. return ApiKeyResponse.from_entity(entity)
  76. @router.get("/gateway/audits", response_model=list[GatewayRequestAuditResponse])
  77. def list_gateway_audits(
  78. tenant_id: str = Query(...),
  79. request_id: str | None = Query(default=None),
  80. target_service: str | None = Query(default=None),
  81. limit: int = Query(default=100, ge=1, le=500),
  82. db: Session = Depends(get_db),
  83. ) -> list[GatewayRequestAuditResponse]:
  84. items = GatewayRequestAuditRepository(db).list_by_scope(
  85. tenant_id=tenant_id,
  86. request_id=request_id,
  87. target_service=target_service,
  88. limit=limit,
  89. )
  90. return [GatewayRequestAuditResponse.from_entity(item) for item in items]
  91. def get_gateway_settings() -> ApiGatewaySettings:
  92. return ApiGatewaySettings()
  93. def get_service_proxy(settings: ApiGatewaySettings = Depends(get_gateway_settings)) -> ServiceProxy:
  94. return ServiceProxy(timeout_seconds=settings.proxy_timeout_seconds)
  95. def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName, ProxyTarget]:
  96. return {
  97. "workflow-service": ProxyTarget(
  98. service_name="workflow-service",
  99. base_url=settings.workflow_service_url,
  100. path_prefix="/workflows",
  101. health_path="/workflows/health",
  102. ),
  103. "session-service": ProxyTarget(
  104. service_name="session-service",
  105. base_url=settings.session_service_url,
  106. path_prefix="/sessions",
  107. health_path="/sessions/health",
  108. ),
  109. "runtime-service": ProxyTarget(
  110. service_name="runtime-service",
  111. base_url=settings.runtime_service_url,
  112. path_prefix="/runtime",
  113. health_path="/runtime/health",
  114. ),
  115. "tool-service": ProxyTarget(
  116. service_name="tool-service",
  117. base_url=settings.tool_service_url,
  118. path_prefix="/tools",
  119. health_path="/tools/health",
  120. ),
  121. "model-gateway-service": ProxyTarget(
  122. service_name="model-gateway-service",
  123. base_url=settings.model_gateway_service_url,
  124. path_prefix="/models",
  125. health_path="/models/health",
  126. ),
  127. "code-runner-service": ProxyTarget(
  128. service_name="code-runner-service",
  129. base_url=settings.code_runner_service_url,
  130. path_prefix="/code",
  131. health_path="/code/health",
  132. ),
  133. }
  134. @router.get("/gateway/services/health", response_model=GatewayServicesHealthResponse)
  135. async def downstream_health_check(
  136. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  137. ) -> GatewayServicesHealthResponse:
  138. targets = build_proxy_targets(settings)
  139. health_proxy = ServiceProxy(timeout_seconds=settings.downstream_health_timeout_seconds)
  140. downstream_services = await asyncio.gather(
  141. *[health_proxy.check_health(target) for target in targets.values()]
  142. )
  143. status = "ok" if all(item.status == "ok" for item in downstream_services) else "degraded"
  144. return GatewayServicesHealthResponse(
  145. status=status,
  146. downstream_services=downstream_services,
  147. )
  148. @router.api_route(
  149. "/gateway/workflows",
  150. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  151. )
  152. @router.api_route(
  153. "/gateway/workflows/{path:path}",
  154. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  155. )
  156. async def proxy_workflow_service(
  157. request: Request,
  158. path: str = "",
  159. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  160. proxy: ServiceProxy = Depends(get_service_proxy),
  161. ) -> Response:
  162. return await proxy.forward(
  163. request=request,
  164. target=build_proxy_targets(settings)["workflow-service"],
  165. path=path,
  166. )
  167. @router.api_route(
  168. "/gateway/sessions",
  169. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  170. )
  171. @router.api_route(
  172. "/gateway/sessions/{path:path}",
  173. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  174. )
  175. async def proxy_session_service(
  176. request: Request,
  177. path: str = "",
  178. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  179. proxy: ServiceProxy = Depends(get_service_proxy),
  180. ) -> Response:
  181. return await proxy.forward(
  182. request=request,
  183. target=build_proxy_targets(settings)["session-service"],
  184. path=path,
  185. )
  186. @router.api_route(
  187. "/gateway/runtime",
  188. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  189. )
  190. @router.api_route(
  191. "/gateway/runtime/{path:path}",
  192. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  193. )
  194. async def proxy_runtime_service(
  195. request: Request,
  196. path: str = "",
  197. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  198. proxy: ServiceProxy = Depends(get_service_proxy),
  199. ) -> Response:
  200. return await proxy.forward(
  201. request=request,
  202. target=build_proxy_targets(settings)["runtime-service"],
  203. path=path,
  204. )
  205. @router.api_route(
  206. "/gateway/tools",
  207. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  208. )
  209. @router.api_route(
  210. "/gateway/tools/{path:path}",
  211. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  212. )
  213. async def proxy_tool_service(
  214. request: Request,
  215. path: str = "",
  216. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  217. proxy: ServiceProxy = Depends(get_service_proxy),
  218. ) -> Response:
  219. return await proxy.forward(
  220. request=request,
  221. target=build_proxy_targets(settings)["tool-service"],
  222. path=path,
  223. )
  224. @router.api_route(
  225. "/gateway/models",
  226. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  227. )
  228. @router.api_route(
  229. "/gateway/models/{path:path}",
  230. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  231. )
  232. async def proxy_model_gateway_service(
  233. request: Request,
  234. path: str = "",
  235. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  236. proxy: ServiceProxy = Depends(get_service_proxy),
  237. ) -> Response:
  238. return await proxy.forward(
  239. request=request,
  240. target=build_proxy_targets(settings)["model-gateway-service"],
  241. path=path,
  242. )
  243. @router.api_route(
  244. "/gateway/code",
  245. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  246. )
  247. @router.api_route(
  248. "/gateway/code/{path:path}",
  249. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  250. )
  251. async def proxy_code_runner_service(
  252. request: Request,
  253. path: str = "",
  254. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  255. proxy: ServiceProxy = Depends(get_service_proxy),
  256. ) -> Response:
  257. return await proxy.forward(
  258. request=request,
  259. target=build_proxy_targets(settings)["code-runner-service"],
  260. path=path,
  261. )