schema.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 Pydantic schemas for OpenSandbox Lifecycle API. 17 18 This module defines data models based on the OpenAPI specification 19 for request/response validation and serialization. 20 """ 21 22 from datetime import datetime 23 from typing import Dict, List, Literal, Optional 24 25 from pydantic import BaseModel, Field, RootModel, model_validator 26 27 28 # ============================================================================ 29 # Image Specification 30 # ============================================================================ 31 32 class ImageAuth(BaseModel): 33 """ 34 Registry authentication credentials for private container registries. 35 """ 36 username: str = Field(..., description="Registry username or service account") 37 password: str = Field(..., description="Registry password or authentication token") 38 39 40 class ImageSpec(BaseModel): 41 """ 42 Container image specification for sandbox provisioning. 43 44 Supports public registry images and private registry images with authentication. 45 """ 46 uri: str = Field( 47 ..., 48 description="Container image URI in standard format (e.g., 'python:3.11', 'gcr.io/my-project/app:v1.0')", 49 ) 50 auth: Optional[ImageAuth] = Field( 51 None, 52 description="Registry authentication credentials (required for private registries)", 53 ) 54 55 56 class PlatformSpec(BaseModel): 57 """ 58 Runtime platform constraint for scheduling/provisioning. 59 """ 60 61 os: str = Field( 62 ..., 63 description="Target operating system (for example 'linux').", 64 ) 65 arch: str = Field( 66 ..., 67 description="Target CPU architecture (for example 'amd64' or 'arm64').", 68 ) 69 70 71 # ============================================================================ 72 # Resource Limits 73 # ============================================================================ 74 75 class ResourceLimits(RootModel[Dict[str, str]]): 76 """ 77 Runtime resource constraints as key-value pairs. 78 79 Similar to Kubernetes resource specifications, allows flexible definition 80 of resource limits. Common resource types include cpu, memory, and gpu. 81 """ 82 root: Dict[str, str] = Field( 83 default_factory=dict, 84 example={"cpu": "500m", "memory": "512Mi", "gpu": "1"}, 85 ) 86 87 88 class NetworkRule(BaseModel): 89 """ 90 Egress rule: allow/deny a specific domain or wildcard. 91 """ 92 93 action: str = Field(..., description="Whether to allow or deny matching targets (allow | deny).") 94 target: str = Field( 95 ..., 96 description="FQDN or wildcard domain (e.g., 'example.com', '*.example.com').", 97 min_length=1, 98 ) 99 100 class Config: 101 populate_by_name = True 102 103 104 class NetworkPolicy(BaseModel): 105 """ 106 Egress network policy matching the sidecar /policy payload. 107 """ 108 109 default_action: Optional[str] = Field( 110 default=None, 111 alias="defaultAction", 112 description="Default action when no egress rule matches (allow | deny). If omitted, sidecar defaults to deny.", 113 ) 114 egress: list[NetworkRule] = Field( 115 default_factory=list, 116 description="Ordered egress rules. Empty/omitted yields allow-all at startup.", 117 ) 118 119 class Config: 120 populate_by_name = True 121 122 123 # ============================================================================ 124 # Volume Definitions 125 # ============================================================================ 126 127 128 class Host(BaseModel): 129 """ 130 Host path bind mount backend. 131 132 Maps a directory on the host filesystem into the container. 133 Only available when the runtime supports host mounts. 134 135 Security note: Host paths are restricted by server-side allowlist. 136 Users must specify paths under permitted prefixes. 137 """ 138 139 path: str = Field( 140 ..., 141 description="Absolute path on the host filesystem to mount.", 142 pattern=r"^(/|[A-Za-z]:[\\/])", 143 ) 144 145 146 class PVC(BaseModel): 147 """ 148 Platform-managed named volume backend. 149 150 A runtime-neutral abstraction for referencing a platform-managed named volume. 151 If ``createIfNotExists`` is true (the default) and the volume does not 152 yet exist, it will be created automatically using the provisioning hints below. 153 154 - Kubernetes: maps to a PersistentVolumeClaim in the same namespace. 155 - Docker: maps to a Docker named volume (created via ``docker volume create``). 156 """ 157 158 claim_name: str = Field( 159 ..., 160 alias="claimName", 161 description=( 162 "Name of the volume on the target platform. " 163 "In Kubernetes this is the PVC name; in Docker this is the named volume name." 164 ), 165 pattern=r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", 166 max_length=253, 167 ) 168 169 create_if_not_exists: bool = Field( 170 True, 171 alias="createIfNotExists", 172 description=( 173 "When true, the volume is automatically created if it does not exist. " 174 "When false, referencing a non-existent volume fails with an error." 175 ), 176 ) 177 delete_on_sandbox_termination: bool = Field( 178 False, 179 alias="deleteOnSandboxTermination", 180 description=( 181 "When true, the volume is automatically removed when the sandbox is " 182 "deleted. Only applies to volumes that were auto-created by the server " 183 "(Docker only). Pre-existing volumes are never removed. Has no effect " 184 "on Kubernetes PVCs, whose lifecycle is managed by the StorageClass " 185 "reclaim policy." 186 ), 187 ) 188 189 # Provisioning hints — used only when auto-creating a new volume. 190 # Ignored if the volume already exists on the platform. 191 storage_class: Optional[str] = Field( 192 None, 193 alias="storageClass", 194 description=( 195 "Kubernetes StorageClass name for auto-created PVCs. " 196 "None means use the cluster default. Ignored for Docker volumes." 197 ), 198 ) 199 storage: Optional[str] = Field( 200 None, 201 description=( 202 "Storage capacity request for auto-created PVCs (e.g. '1Gi', '10Gi'). " 203 "Defaults to server-side configured value when omitted. " 204 "Ignored for Docker volumes." 205 ), 206 pattern=r"^\d+(\.\d+)?(Ki|Mi|Gi|Ti|Pi|Ei)?$", 207 ) 208 access_modes: Optional[List[str]] = Field( 209 None, 210 alias="accessModes", 211 description=( 212 "Access modes for auto-created PVCs (e.g. ['ReadWriteOnce']). " 213 "Defaults to ['ReadWriteOnce'] when omitted. Ignored for Docker volumes." 214 ), 215 ) 216 217 class Config: 218 populate_by_name = True 219 220 221 class OSSFS(BaseModel): 222 """ 223 Alibaba Cloud OSS mount backend via ossfs. 224 225 The runtime mounts a host-side OSS path under ``storage.ossfs_mount_root`` 226 and then bind-mounts the resolved path into the sandbox container. Prefix 227 selection is expressed via ``Volume.subPath``. 228 In Docker runtime, OSSFS backend requires the server host to be Linux with FUSE support. 229 """ 230 231 bucket: str = Field( 232 ..., 233 description="OSS bucket name.", 234 min_length=3, 235 max_length=63, 236 ) 237 endpoint: str = Field( 238 ..., 239 description="OSS endpoint, e.g. 'oss-cn-hangzhou.aliyuncs.com'.", 240 min_length=1, 241 ) 242 version: Literal["1.0", "2.0"] = Field( 243 "2.0", 244 description="ossfs major version used by runtime mount integration.", 245 ) 246 options: Optional[List[str]] = Field( 247 None, 248 description=( 249 "Additional ossfs mount options. Runtime encodes options by version: " 250 "1.0 => 'ossfs ... -o <option>', 2.0 => 'ossfs2 config line --<option>'. " 251 "Provide raw option payloads without leading '-'." 252 ), 253 ) 254 access_key_id: Optional[str] = Field( 255 None, 256 alias="accessKeyId", 257 description="OSS access key ID for inline credentials mode.", 258 min_length=1, 259 ) 260 access_key_secret: Optional[str] = Field( 261 None, 262 alias="accessKeySecret", 263 description="OSS access key secret for inline credentials mode.", 264 min_length=1, 265 ) 266 class Config: 267 populate_by_name = True 268 269 @model_validator(mode="after") 270 def validate_inline_credentials(self) -> "OSSFS": 271 """Ensure inline credentials are provided for current OSSFS mode.""" 272 if not self.access_key_id or not self.access_key_secret: 273 raise ValueError( 274 "OSSFS inline credentials are required: accessKeyId and accessKeySecret." 275 ) 276 return self 277 278 279 class Volume(BaseModel): 280 """ 281 Storage mount definition for a sandbox. 282 283 Each volume entry contains: 284 - A unique name identifier 285 - Exactly one backend struct (host, pvc, etc.) with backend-specific fields 286 - Common mount settings (mountPath, readOnly, subPath) 287 """ 288 289 name: str = Field( 290 ..., 291 description="Unique identifier for the volume within the sandbox.", 292 pattern=r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", 293 max_length=63, 294 ) 295 host: Optional[Host] = Field( 296 None, 297 description="Host path bind mount backend.", 298 ) 299 pvc: Optional[PVC] = Field( 300 None, 301 description="Platform-managed named volume backend (PVC in Kubernetes, named volume in Docker).", 302 ) 303 ossfs: Optional[OSSFS] = Field( 304 None, 305 description="OSSFS mount backend.", 306 ) 307 mount_path: str = Field( 308 ..., 309 alias="mountPath", 310 description="Absolute path inside the container where the volume is mounted.", 311 pattern=r"^/.*", 312 ) 313 read_only: bool = Field( 314 False, 315 alias="readOnly", 316 description="If true, the volume is mounted as read-only. Defaults to false (read-write).", 317 ) 318 sub_path: Optional[str] = Field( 319 None, 320 alias="subPath", 321 description="Optional subdirectory under the backend path to mount.", 322 ) 323 324 class Config: 325 populate_by_name = True 326 327 @model_validator(mode="after") 328 def validate_exactly_one_backend(self) -> "Volume": 329 """Ensure exactly one backend type is specified.""" 330 backends = [self.host, self.pvc, self.ossfs] 331 specified = [b for b in backends if b is not None] 332 if len(specified) == 0: 333 raise ValueError("Exactly one backend (host, pvc, ossfs) must be specified, but none was provided.") 334 if len(specified) > 1: 335 raise ValueError("Exactly one backend (host, pvc, ossfs) must be specified, but multiple were provided.") 336 return self 337 338 339 # ============================================================================ 340 # Sandbox Status 341 # ============================================================================ 342 343 class SandboxStatus(BaseModel): 344 """ 345 Detailed status information with lifecycle state and transition details. 346 """ 347 state: str = Field( 348 ..., 349 description="Current lifecycle state (Pending, Running, Pausing, Paused, Stopping, Terminated, Failed)", 350 ) 351 reason: Optional[str] = Field( 352 None, 353 description="Short machine-readable reason code for the current state", 354 ) 355 message: Optional[str] = Field( 356 None, 357 description="Human-readable message describing the current state or reason for state transition", 358 ) 359 last_transition_at: Optional[datetime] = Field( 360 None, 361 alias="lastTransitionAt", 362 description="Timestamp of the last state transition", 363 ) 364 365 class Config: 366 populate_by_name = True 367 368 369 # ============================================================================ 370 # Sandbox Models 371 # ============================================================================ 372 373 class CreateSandboxRequest(BaseModel): 374 """ 375 Request to create a new sandbox from a container image. 376 """ 377 image: ImageSpec = Field(..., description="Container image specification for the sandbox") 378 platform: Optional[PlatformSpec] = Field( 379 None, 380 description=( 381 "Optional platform constraint for sandbox scheduling/runtime selection. " 382 "If omitted, runtime default behavior applies (runtime-specific and not a fixed " 383 "architecture guarantee). If specified, runtime must satisfy this platform or fail " 384 "explicitly." 385 ), 386 ) 387 timeout: Optional[int] = Field( 388 None, 389 ge=60, 390 description=( 391 "Sandbox timeout in seconds (minimum 60). " 392 "The maximum is controlled by server.max_sandbox_timeout_seconds. " 393 "When omitted or null, the sandbox will not auto-terminate and must be deleted explicitly. " 394 "Note: manual cleanup support is runtime-dependent; Kubernetes providers may reject " 395 "null timeout when the workload provider does not support non-expiring sandboxes." 396 ), 397 ) 398 resource_limits: ResourceLimits = Field( 399 ..., 400 alias="resourceLimits", 401 description="Runtime resource constraints for the sandbox instance", 402 ) 403 env: Optional[Dict[str, Optional[str]]] = Field( 404 None, 405 description="Environment variables to inject into the sandbox runtime", 406 ) 407 metadata: Optional[Dict[str, str]] = Field( 408 None, 409 description="Custom key-value metadata for management, filtering, and tagging", 410 ) 411 entrypoint: List[str] = Field( 412 ..., 413 min_length=1, 414 description="The command to execute as the sandbox's entry process", 415 example=["python", "/app/main.py"], 416 ) 417 network_policy: Optional[NetworkPolicy] = Field( 418 None, 419 alias="networkPolicy", 420 description=( 421 "Optional outbound network policy. Shape matches the egress sidecar /policy endpoint. " 422 "Empty/omitted means allow-all until updated." 423 ), 424 ) 425 secure_access: bool = Field( 426 False, 427 alias="secureAccess", 428 description=( 429 "Opts the sandbox into secured access for endpoint access. " 430 "Currently supported only for Kubernetes sandboxes exposed through ingress gateway mode. " 431 "When enabled, the server provisions access credentials and returns required endpoint headers." 432 ), 433 ) 434 volumes: Optional[List[Volume]] = Field( 435 None, 436 description=( 437 "Storage mounts for the sandbox. Each volume entry specifies a named backend-specific " 438 "storage source and common mount settings. Exactly one backend type must be specified per volume entry." 439 ), 440 ) 441 extensions: Optional[Dict[str, str]] = Field( 442 None, 443 description="Opaque container for provider-specific or transient parameters not covered by the core API", 444 ) 445 446 class Config: 447 populate_by_name = True 448 449 450 class CreateSandboxResponse(BaseModel): 451 """ 452 Response from creating a new sandbox. 453 454 Contains essential information without image and updatedAt. 455 """ 456 id: str = Field(..., description="Unique sandbox identifier") 457 status: SandboxStatus = Field(..., description="Current lifecycle status and detailed state information") 458 metadata: Optional[Dict[str, str]] = Field(None, description="Custom metadata from creation request") 459 platform: Optional[PlatformSpec] = Field( 460 None, 461 description=( 462 "Platform constraint echoed from request or workload template. " 463 "Null when no scheduling constraint is provided." 464 ), 465 ) 466 expires_at: Optional[datetime] = Field( 467 None, 468 alias="expiresAt", 469 description="Timestamp when sandbox will auto-terminate. Null when manual cleanup is enabled.", 470 ) 471 created_at: datetime = Field(..., alias="createdAt", description="Sandbox creation timestamp") 472 entrypoint: List[str] = Field(..., description="Entry process specification from creation request") 473 474 class Config: 475 populate_by_name = True 476 477 478 class Sandbox(BaseModel): 479 """ 480 Runtime execution environment provisioned from a container image. 481 482 This is the complete representation of the sandbox resource. 483 """ 484 id: str = Field(..., description="Unique sandbox identifier") 485 image: ImageSpec = Field(..., description="Container image specification used to provision this sandbox") 486 platform: Optional[PlatformSpec] = Field( 487 None, 488 description=( 489 "Platform constraint echoed from request or workload template. " 490 "Null when no scheduling constraint is provided." 491 ), 492 ) 493 status: SandboxStatus = Field(..., description="Current lifecycle status and detailed state information") 494 metadata: Optional[Dict[str, str]] = Field(None, description="Custom metadata from creation request") 495 entrypoint: List[str] = Field(..., description="The command to execute as the sandbox's entry process") 496 expires_at: Optional[datetime] = Field( 497 None, 498 alias="expiresAt", 499 description="Timestamp when sandbox will auto-terminate. Null when manual cleanup is enabled.", 500 ) 501 created_at: datetime = Field(..., alias="createdAt", description="Sandbox creation timestamp") 502 503 class Config: 504 populate_by_name = True 505 506 507 # ============================================================================ 508 # List Sandboxes 509 # ============================================================================ 510 511 class SandboxFilter(BaseModel): 512 """ 513 Filtering criteria for listing sandboxes. 514 """ 515 state: Optional[List[str]] = Field( 516 None, 517 min_length=1, 518 description="Filter by lifecycle state (status.state) - supports OR logic", 519 ) 520 metadata: Optional[Dict[str, str]] = Field( 521 None, 522 description="Filter by metadata key-value pairs (AND logic)", 523 ) 524 525 526 class PaginationRequest(BaseModel): 527 """ 528 Pagination parameters for list requests. 529 """ 530 page: int = Field(1, ge=1, description="Page number") 531 page_size: int = Field( 532 20, 533 ge=1, 534 le=200, 535 alias="pageSize", 536 description="Number of items per page", 537 ) 538 539 class Config: 540 populate_by_name = True 541 542 543 class ListSandboxesRequest(BaseModel): 544 """ 545 Request body for complex listing queries. 546 """ 547 filter: SandboxFilter = Field( 548 default_factory=SandboxFilter, 549 description="Filtering criteria (all conditions combined with AND logic)", 550 ) 551 pagination: Optional[PaginationRequest] = Field(None, description="Pagination parameters") 552 553 554 class PaginationInfo(BaseModel): 555 """ 556 Pagination metadata for list responses. 557 """ 558 page: int = Field(..., ge=1, description="Current page number") 559 page_size: int = Field(..., ge=1, alias="pageSize", description="Number of items per page") 560 total_items: int = Field(..., ge=0, alias="totalItems", description="Total number of items matching the filter") 561 total_pages: int = Field(..., ge=0, alias="totalPages", description="Total number of pages") 562 has_next_page: bool = Field(..., alias="hasNextPage", description="Whether there are more pages after the current one") 563 564 class Config: 565 populate_by_name = True 566 567 568 class ListSandboxesResponse(BaseModel): 569 """ 570 Paginated collection of sandboxes. 571 """ 572 items: List[Sandbox] = Field(..., description="List of sandboxes") 573 pagination: PaginationInfo = Field(..., description="Pagination metadata") 574 575 576 # ============================================================================ 577 # Renew Expiration 578 # ============================================================================ 579 580 class RenewSandboxExpirationRequest(BaseModel): 581 """ 582 Request to renew sandbox expiration time. 583 """ 584 expires_at: datetime = Field( 585 ..., 586 alias="expiresAt", 587 description="New absolute expiration time in UTC (RFC 3339 format). Must be in the future.", 588 ) 589 590 class Config: 591 populate_by_name = True 592 593 594 class RenewSandboxExpirationResponse(BaseModel): 595 """ 596 Response for renewing sandbox expiration. 597 """ 598 expires_at: datetime = Field( 599 ..., 600 alias="expiresAt", 601 description="The new absolute expiration time in UTC (RFC 3339 format)", 602 ) 603 604 class Config: 605 populate_by_name = True 606 607 608 # ============================================================================ 609 # Endpoint 610 # ============================================================================ 611 612 class Endpoint(BaseModel): 613 """ 614 Endpoint for accessing a service running in the sandbox. 615 """ 616 endpoint: str = Field( 617 ..., 618 description="Public endpoint string (host[:port]/path) exposed for the sandbox service", 619 ) 620 headers: Optional[dict[str, str]] = Field( 621 default=None, 622 description="Optional headers required when accessing the endpoint (e.g., for header-based routing).", 623 ) 624 625 626 # ============================================================================ 627 # Error Response 628 # ============================================================================ 629 630 class ErrorResponse(BaseModel): 631 """ 632 Standard error response for all non-2xx HTTP responses. 633 634 HTTP status code indicates the error category; code and message provide details. 635 """ 636 code: str = Field( 637 ..., 638 description="Machine-readable error code (e.g., INVALID_REQUEST, NOT_FOUND, INTERNAL_ERROR)", 639 ) 640 message: str = Field( 641 ..., 642 description="Human-readable error message describing what went wrong and how to fix it", 643 ) 644 645 646 # ============================================================================ 647 # Pool Models 648 # ============================================================================ 649 650 class PoolCapacitySpec(BaseModel): 651 """ 652 Capacity configuration that controls the size of the resource pool. 653 """ 654 buffer_max: int = Field( 655 ..., 656 alias="bufferMax", 657 ge=0, 658 description="Maximum number of nodes kept in the warm buffer.", 659 ) 660 buffer_min: int = Field( 661 ..., 662 alias="bufferMin", 663 ge=0, 664 description="Minimum number of nodes that must remain in the buffer.", 665 ) 666 pool_max: int = Field( 667 ..., 668 alias="poolMax", 669 ge=0, 670 description="Maximum total number of nodes allowed in the entire pool.", 671 ) 672 pool_min: int = Field( 673 ..., 674 alias="poolMin", 675 ge=0, 676 description="Minimum total size of the pool.", 677 ) 678 679 class Config: 680 populate_by_name = True 681 682 683 class CreatePoolRequest(BaseModel): 684 """ 685 Request to create a new pre-warmed resource pool. 686 687 A Pool manages a set of pre-warmed pods that can be rapidly allocated 688 to sandboxes, reducing cold-start latency. 689 """ 690 name: str = Field( 691 ..., 692 description="Unique name for the pool (must be a valid Kubernetes resource name).", 693 pattern=r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", 694 max_length=253, 695 ) 696 template: Dict = Field( 697 ..., 698 description=( 699 "Kubernetes PodTemplateSpec defining the pod configuration for pre-warmed nodes. " 700 "Follows the same schema as spec.template in a Kubernetes Deployment." 701 ), 702 ) 703 capacity_spec: PoolCapacitySpec = Field( 704 ..., 705 alias="capacitySpec", 706 description="Capacity configuration controlling pool size and buffer behavior.", 707 ) 708 709 class Config: 710 populate_by_name = True 711 712 713 class UpdatePoolRequest(BaseModel): 714 """ 715 Request to update an existing pool's capacity configuration. 716 717 Only capacity settings can be updated after pool creation. 718 Updating the pod template requires recreating the pool. 719 """ 720 capacity_spec: PoolCapacitySpec = Field( 721 ..., 722 alias="capacitySpec", 723 description="New capacity configuration for the pool.", 724 ) 725 726 class Config: 727 populate_by_name = True 728 729 730 class PoolStatus(BaseModel): 731 """ 732 Observed runtime state of a pool. 733 """ 734 total: int = Field(..., description="Total number of nodes in the pool.") 735 allocated: int = Field(..., description="Number of nodes currently allocated to sandboxes.") 736 available: int = Field(..., description="Number of nodes currently available in the pool.") 737 revision: str = Field(..., description="Latest revision identifier of the pool.") 738 739 740 class PoolResponse(BaseModel): 741 """ 742 Full representation of a Pool resource. 743 """ 744 name: str = Field(..., description="Unique pool name.") 745 capacity_spec: PoolCapacitySpec = Field( 746 ..., 747 alias="capacitySpec", 748 description="Capacity configuration of the pool.", 749 ) 750 status: Optional[PoolStatus] = Field( 751 None, 752 description="Observed runtime state of the pool. May be absent if not yet reconciled.", 753 ) 754 created_at: Optional[datetime] = Field( 755 None, 756 alias="createdAt", 757 description="Pool creation timestamp.", 758 ) 759 760 class Config: 761 populate_by_name = True 762 763 764 class ListPoolsResponse(BaseModel): 765 """ 766 Collection of pools. 767 """ 768 items: List[PoolResponse] = Field(..., description="List of pools.")