Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 239 additions & 0 deletions server/src/api/pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# Copyright 2025 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
API routes for Pool resource management.

Pools are pre-warmed sets of sandbox pods that reduce cold-start latency.
These endpoints are only available when the runtime is configured as 'kubernetes'.
"""

from typing import Optional

from fastapi import APIRouter, Header, status
from fastapi.exceptions import HTTPException
from fastapi.responses import Response

from src.api.schema import (
CreatePoolRequest,
ErrorResponse,
ListPoolsResponse,
PoolResponse,
UpdatePoolRequest,
)
from src.config import get_config
from src.services.constants import SandboxErrorCodes

router = APIRouter(tags=["Pools"])

_POOL_NOT_K8S_DETAIL = {
"code": SandboxErrorCodes.K8S_POOL_NOT_SUPPORTED,
"message": "Pool management is only available when runtime.type is 'kubernetes'.",
}


def _get_pool_service():
"""
Lazily create the PoolService, raising 501 if the runtime is not Kubernetes.

This deferred approach means the pool router can be registered unconditionally
in main.py; non-k8s deployments simply receive a clear 501 on every call.
"""
from src.services.k8s.client import K8sClient
from src.services.k8s.pool_service import PoolService

config = get_config()
if config.runtime.type != "kubernetes":
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail=_POOL_NOT_K8S_DETAIL,
)

if not config.kubernetes:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail=_POOL_NOT_K8S_DETAIL,
)

k8s_client = K8sClient(config.kubernetes)
return PoolService(k8s_client, namespace=config.kubernetes.namespace)


# ============================================================================
# Pool CRUD Endpoints
# ============================================================================

@router.post(
"/pools",
response_model=PoolResponse,
response_model_exclude_none=True,
status_code=status.HTTP_201_CREATED,
responses={
201: {"description": "Pool created successfully"},
400: {"model": ErrorResponse, "description": "The request was invalid or malformed"},
401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"},
409: {"model": ErrorResponse, "description": "A pool with the same name already exists"},
501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"},
500: {"model": ErrorResponse, "description": "An unexpected server error occurred"},
},
)
async def create_pool(
request: CreatePoolRequest,
x_request_id: Optional[str] = Header(None, alias="X-Request-ID"),
) -> PoolResponse:
"""
Create a pre-warmed resource pool.

Creates a Pool CRD resource that manages a set of pre-warmed pods.
Once created, sandboxes can reference the pool via ``extensions.poolRef``
during sandbox creation to benefit from reduced cold-start latency.

Args:
request: Pool creation request including name, pod template, and capacity spec.
x_request_id: Optional request tracing identifier.

Returns:
PoolResponse: The newly created pool.
"""
pool_service = _get_pool_service()
return pool_service.create_pool(request)


@router.get(
"/pools",
response_model=ListPoolsResponse,
response_model_exclude_none=True,
responses={
200: {"description": "List of pools"},
401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"},
501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"},
500: {"model": ErrorResponse, "description": "An unexpected server error occurred"},
},
)
async def list_pools(
x_request_id: Optional[str] = Header(None, alias="X-Request-ID"),
) -> ListPoolsResponse:
"""
List all pre-warmed resource pools.

Returns all Pool resources in the configured namespace.

Args:
x_request_id: Optional request tracing identifier.

Returns:
ListPoolsResponse: Collection of all pools.
"""
pool_service = _get_pool_service()
return pool_service.list_pools()


@router.get(
"/pools/{pool_name}",
response_model=PoolResponse,
response_model_exclude_none=True,
responses={
200: {"description": "Pool retrieved successfully"},
401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"},
404: {"model": ErrorResponse, "description": "The requested pool does not exist"},
501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"},
500: {"model": ErrorResponse, "description": "An unexpected server error occurred"},
},
)
async def get_pool(
pool_name: str,
x_request_id: Optional[str] = Header(None, alias="X-Request-ID"),
) -> PoolResponse:
"""
Retrieve a pool by name.

Args:
pool_name: Name of the pool to retrieve.
x_request_id: Optional request tracing identifier.

Returns:
PoolResponse: Current state of the pool including runtime status.
"""
pool_service = _get_pool_service()
return pool_service.get_pool(pool_name)


@router.put(
"/pools/{pool_name}",
response_model=PoolResponse,
response_model_exclude_none=True,
responses={
200: {"description": "Pool capacity updated successfully"},
400: {"model": ErrorResponse, "description": "The request was invalid or malformed"},
401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"},
404: {"model": ErrorResponse, "description": "The requested pool does not exist"},
501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"},
500: {"model": ErrorResponse, "description": "An unexpected server error occurred"},
},
)
async def update_pool(
pool_name: str,
request: UpdatePoolRequest,
x_request_id: Optional[str] = Header(None, alias="X-Request-ID"),
) -> PoolResponse:
"""
Update pool capacity configuration.

Only ``capacitySpec`` (bufferMax, bufferMin, poolMax, poolMin) can be
modified after creation. To change the pod template, delete and recreate
the pool.

Args:
pool_name: Name of the pool to update.
request: Update request with the new capacity spec.
x_request_id: Optional request tracing identifier.

Returns:
PoolResponse: Updated pool state.
"""
pool_service = _get_pool_service()
return pool_service.update_pool(pool_name, request)


@router.delete(
"/pools/{pool_name}",
status_code=status.HTTP_204_NO_CONTENT,
responses={
204: {"description": "Pool deleted successfully"},
401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"},
404: {"model": ErrorResponse, "description": "The requested pool does not exist"},
501: {"model": ErrorResponse, "description": "Pool management is not supported in this runtime"},
500: {"model": ErrorResponse, "description": "An unexpected server error occurred"},
},
)
async def delete_pool(
pool_name: str,
x_request_id: Optional[str] = Header(None, alias="X-Request-ID"),
) -> Response:
"""
Delete a pool.

Removes the Pool CRD resource. Pre-warmed pods managed by the pool will
be terminated by the pool controller.

Args:
pool_name: Name of the pool to delete.
x_request_id: Optional request tracing identifier.

Returns:
Response: 204 No Content.
"""
pool_service = _get_pool_service()
pool_service.delete_pool(pool_name)
return Response(status_code=status.HTTP_204_NO_CONTENT)
125 changes: 125 additions & 0 deletions server/src/api/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,128 @@ class ErrorResponse(BaseModel):
...,
description="Human-readable error message describing what went wrong and how to fix it",
)


# ============================================================================
# Pool Models
# ============================================================================

class PoolCapacitySpec(BaseModel):
"""
Capacity configuration that controls the size of the resource pool.
"""
buffer_max: int = Field(
...,
alias="bufferMax",
ge=0,
description="Maximum number of nodes kept in the warm buffer.",
)
buffer_min: int = Field(
...,
alias="bufferMin",
ge=0,
description="Minimum number of nodes that must remain in the buffer.",
)
pool_max: int = Field(
...,
alias="poolMax",
ge=0,
description="Maximum total number of nodes allowed in the entire pool.",
)
pool_min: int = Field(
...,
alias="poolMin",
ge=0,
description="Minimum total size of the pool.",
)

class Config:
populate_by_name = True


class CreatePoolRequest(BaseModel):
"""
Request to create a new pre-warmed resource pool.

A Pool manages a set of pre-warmed pods that can be rapidly allocated
to sandboxes, reducing cold-start latency.
"""
name: str = Field(
...,
description="Unique name for the pool (must be a valid Kubernetes resource name).",
pattern=r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
max_length=253,
)
template: Dict = Field(
...,
description=(
"Kubernetes PodTemplateSpec defining the pod configuration for pre-warmed nodes. "
"Follows the same schema as spec.template in a Kubernetes Deployment."
),
)
capacity_spec: PoolCapacitySpec = Field(
...,
alias="capacitySpec",
description="Capacity configuration controlling pool size and buffer behavior.",
)

class Config:
populate_by_name = True


class UpdatePoolRequest(BaseModel):
"""
Request to update an existing pool's capacity configuration.

Only capacity settings can be updated after pool creation.
Updating the pod template requires recreating the pool.
"""
capacity_spec: PoolCapacitySpec = Field(
...,
alias="capacitySpec",
description="New capacity configuration for the pool.",
)

class Config:
populate_by_name = True


class PoolStatus(BaseModel):
"""
Observed runtime state of a pool.
"""
total: int = Field(..., description="Total number of nodes in the pool.")
allocated: int = Field(..., description="Number of nodes currently allocated to sandboxes.")
available: int = Field(..., description="Number of nodes currently available in the pool.")
revision: str = Field(..., description="Latest revision identifier of the pool.")


class PoolResponse(BaseModel):
"""
Full representation of a Pool resource.
"""
name: str = Field(..., description="Unique pool name.")
capacity_spec: PoolCapacitySpec = Field(
...,
alias="capacitySpec",
description="Capacity configuration of the pool.",
)
status: Optional[PoolStatus] = Field(
None,
description="Observed runtime state of the pool. May be absent if not yet reconciled.",
)
created_at: Optional[datetime] = Field(
None,
alias="createdAt",
description="Pool creation timestamp.",
)

class Config:
populate_by_name = True


class ListPoolsResponse(BaseModel):
"""
Collection of pools.
"""
items: List[PoolResponse] = Field(..., description="List of pools.")
3 changes: 3 additions & 0 deletions server/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
)

from src.api.lifecycle import router # noqa: E402
from src.api.pool import router as pool_router # noqa: E402
from src.middleware.auth import AuthMiddleware # noqa: E402
from src.middleware.request_id import RequestIdMiddleware # noqa: E402
from src.services.runtime_resolver import ( # noqa: E402
Expand Down Expand Up @@ -149,6 +150,8 @@ async def lifespan(app: FastAPI):
# Include API routes at root and versioned prefix
app.include_router(router)
app.include_router(router, prefix="/v1")
app.include_router(pool_router)
app.include_router(pool_router, prefix="/v1")

DEFAULT_ERROR_CODE = "GENERAL::UNKNOWN_ERROR"
DEFAULT_ERROR_MESSAGE = "An unexpected error occurred."
Expand Down
6 changes: 6 additions & 0 deletions server/src/services/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ class SandboxErrorCodes:
INVALID_METADATA_LABEL = "SANDBOX::INVALID_METADATA_LABEL"
INVALID_PARAMETER = "SANDBOX::INVALID_PARAMETER"

# Pool error codes
K8S_POOL_NOT_FOUND = "KUBERNETES::POOL_NOT_FOUND"
K8S_POOL_ALREADY_EXISTS = "KUBERNETES::POOL_ALREADY_EXISTS"
K8S_POOL_API_ERROR = "KUBERNETES::POOL_API_ERROR"
K8S_POOL_NOT_SUPPORTED = "KUBERNETES::POOL_NOT_SUPPORTED"

# Volume error codes
INVALID_VOLUME_NAME = "VOLUME::INVALID_NAME"
DUPLICATE_VOLUME_NAME = "VOLUME::DUPLICATE_NAME"
Expand Down
Loading