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)