pool.py
1 # Copyright 2025 Alibaba Group Holding Ltd. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """ 16 API routes for Pool resource management. 17 18 Pools are pre-warmed sets of sandbox pods that reduce cold-start latency. 19 These endpoints are only available when the runtime is configured as 'kubernetes'. 20 """ 21 22 from typing import Optional 23 24 from fastapi import APIRouter, Header, status 25 from fastapi.exceptions import HTTPException 26 from fastapi.responses import Response 27 28 from opensandbox_server.api.schema import ( 29 CreatePoolRequest, 30 ErrorResponse, 31 ListPoolsResponse, 32 PoolResponse, 33 UpdatePoolRequest, 34 ) 35 from opensandbox_server.config import get_config 36 from opensandbox_server.services.constants import SandboxErrorCodes 37 38 router = APIRouter(tags=["Pools"]) 39 40 _POOL_NOT_K8S_DETAIL = { 41 "code": SandboxErrorCodes.K8S_POOL_NOT_SUPPORTED, 42 "message": "Pool management is only available when runtime.type is 'kubernetes'.", 43 } 44 45 46 def _get_pool_service(): 47 """ 48 Lazily create the PoolService, raising 501 if the runtime is not Kubernetes. 49 50 This deferred approach means the pool router can be registered unconditionally 51 in main.py; non-k8s deployments simply receive a clear 501 on every call. 52 """ 53 from opensandbox_server.services.k8s.client import K8sClient 54 from opensandbox_server.services.k8s.pool_service import PoolService 55 56 config = get_config() 57 if config.runtime.type != "kubernetes": 58 raise HTTPException( 59 status_code=status.HTTP_501_NOT_IMPLEMENTED, 60 detail=_POOL_NOT_K8S_DETAIL, 61 ) 62 63 if not config.kubernetes: 64 raise HTTPException( 65 status_code=status.HTTP_501_NOT_IMPLEMENTED, 66 detail=_POOL_NOT_K8S_DETAIL, 67 ) 68 69 k8s_client = K8sClient(config.kubernetes) 70 return PoolService(k8s_client, namespace=config.kubernetes.namespace) 71 72 73 # ============================================================================ 74 # Pool CRUD Endpoints 75 # ============================================================================ 76 77 @router.post( 78 "/pools", 79 response_model=PoolResponse, 80 response_model_exclude_none=True, 81 status_code=status.HTTP_201_CREATED, 82 responses={ 83 201: {"description": "Pool created successfully"}, 84 400: {"model": ErrorResponse, "description": "The request was invalid or malformed"}, 85 401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"}, 86 409: {"model": ErrorResponse, "description": "A pool with the same name already exists"}, 87 501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"}, 88 500: {"model": ErrorResponse, "description": "An unexpected server error occurred"}, 89 }, 90 ) 91 async def create_pool( 92 request: CreatePoolRequest, 93 x_request_id: Optional[str] = Header(None, alias="X-Request-ID"), 94 ) -> PoolResponse: 95 """ 96 Create a pre-warmed resource pool. 97 98 Creates a Pool CRD resource that manages a set of pre-warmed pods. 99 Once created, sandboxes can reference the pool via ``extensions.poolRef`` 100 during sandbox creation to benefit from reduced cold-start latency. 101 102 Args: 103 request: Pool creation request including name, pod template, and capacity spec. 104 x_request_id: Optional request tracing identifier. 105 106 Returns: 107 PoolResponse: The newly created pool. 108 """ 109 pool_service = _get_pool_service() 110 return pool_service.create_pool(request) 111 112 113 @router.get( 114 "/pools", 115 response_model=ListPoolsResponse, 116 response_model_exclude_none=True, 117 responses={ 118 200: {"description": "List of pools"}, 119 401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"}, 120 501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"}, 121 500: {"model": ErrorResponse, "description": "An unexpected server error occurred"}, 122 }, 123 ) 124 async def list_pools( 125 x_request_id: Optional[str] = Header(None, alias="X-Request-ID"), 126 ) -> ListPoolsResponse: 127 """ 128 List all pre-warmed resource pools. 129 130 Returns all Pool resources in the configured namespace. 131 132 Args: 133 x_request_id: Optional request tracing identifier. 134 135 Returns: 136 ListPoolsResponse: Collection of all pools. 137 """ 138 pool_service = _get_pool_service() 139 return pool_service.list_pools() 140 141 142 @router.get( 143 "/pools/{pool_name}", 144 response_model=PoolResponse, 145 response_model_exclude_none=True, 146 responses={ 147 200: {"description": "Pool retrieved successfully"}, 148 401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"}, 149 404: {"model": ErrorResponse, "description": "The requested pool does not exist"}, 150 501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"}, 151 500: {"model": ErrorResponse, "description": "An unexpected server error occurred"}, 152 }, 153 ) 154 async def get_pool( 155 pool_name: str, 156 x_request_id: Optional[str] = Header(None, alias="X-Request-ID"), 157 ) -> PoolResponse: 158 """ 159 Retrieve a pool by name. 160 161 Args: 162 pool_name: Name of the pool to retrieve. 163 x_request_id: Optional request tracing identifier. 164 165 Returns: 166 PoolResponse: Current state of the pool including runtime status. 167 """ 168 pool_service = _get_pool_service() 169 return pool_service.get_pool(pool_name) 170 171 172 @router.put( 173 "/pools/{pool_name}", 174 response_model=PoolResponse, 175 response_model_exclude_none=True, 176 responses={ 177 200: {"description": "Pool capacity updated successfully"}, 178 400: {"model": ErrorResponse, "description": "The request was invalid or malformed"}, 179 401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"}, 180 404: {"model": ErrorResponse, "description": "The requested pool does not exist"}, 181 501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"}, 182 500: {"model": ErrorResponse, "description": "An unexpected server error occurred"}, 183 }, 184 ) 185 async def update_pool( 186 pool_name: str, 187 request: UpdatePoolRequest, 188 x_request_id: Optional[str] = Header(None, alias="X-Request-ID"), 189 ) -> PoolResponse: 190 """ 191 Update pool capacity configuration. 192 193 Only ``capacitySpec`` (bufferMax, bufferMin, poolMax, poolMin) can be 194 modified after creation. To change the pod template, delete and recreate 195 the pool. 196 197 Args: 198 pool_name: Name of the pool to update. 199 request: Update request with the new capacity spec. 200 x_request_id: Optional request tracing identifier. 201 202 Returns: 203 PoolResponse: Updated pool state. 204 """ 205 pool_service = _get_pool_service() 206 return pool_service.update_pool(pool_name, request) 207 208 209 @router.delete( 210 "/pools/{pool_name}", 211 status_code=status.HTTP_204_NO_CONTENT, 212 responses={ 213 204: {"description": "Pool deleted successfully"}, 214 401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"}, 215 404: {"model": ErrorResponse, "description": "The requested pool does not exist"}, 216 501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"}, 217 500: {"model": ErrorResponse, "description": "An unexpected server error occurred"}, 218 }, 219 ) 220 async def delete_pool( 221 pool_name: str, 222 x_request_id: Optional[str] = Header(None, alias="X-Request-ID"), 223 ) -> Response: 224 """ 225 Delete a pool. 226 227 Removes the Pool CRD resource. Pre-warmed pods managed by the pool will 228 be terminated by the pool controller. 229 230 Args: 231 pool_name: Name of the pool to delete. 232 x_request_id: Optional request tracing identifier. 233 234 Returns: 235 Response: 204 No Content. 236 """ 237 pool_service = _get_pool_service() 238 pool_service.delete_pool(pool_name) 239 return Response(status_code=status.HTTP_204_NO_CONTENT)