From 9689015d8861addc647064d407e058f85716dbd7 Mon Sep 17 00:00:00 2001 From: gaochunhui <2411512182@qq.com> Date: Wed, 8 Apr 2026 18:14:46 +0800 Subject: [PATCH 1/2] feat: add resource_requests for Burstable QoS support Add optional resourceRequests field to allow requests < limits, enabling Kubernetes Burstable QoS for better resource utilization. - OpenAPI spec: add resourceRequests to CreateSandboxRequest - SDK: Sandbox.create() accepts resource_requests param - Server: extract and pass resource_requests through to providers - Providers: use separate requests in V1ResourceRequirements - Backward compatible: defaults to limits when omitted Co-Authored-By: Claude Opus 4.6 --- .../converter/sandbox_model_converter.py | 7 +++++++ .../opensandbox/adapters/sandboxes_adapter.py | 2 ++ .../models/create_sandbox_request.py | 19 +++++++++++++++++++ .../sandbox/python/src/opensandbox/sandbox.py | 9 ++++++++- .../src/opensandbox/services/sandbox.py | 3 +++ .../tests/test_sandbox_business_logic.py | 1 + server/opensandbox_server/api/schema.py | 9 +++++++++ .../services/k8s/agent_sandbox_provider.py | 8 +++++++- .../services/k8s/batchsandbox_provider.py | 15 ++++++++++----- .../services/k8s/kubernetes_service.py | 6 ++++++ .../services/k8s/workload_provider.py | 3 +++ specs/sandbox-lifecycle.yml | 7 +++++++ 12 files changed, 82 insertions(+), 7 deletions(-) diff --git a/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py b/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py index 81a268df..1a8c9c71 100644 --- a/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py +++ b/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py @@ -148,6 +148,7 @@ def to_api_create_sandbox_request( network_policy: NetworkPolicy | None, extensions: dict[str, str], volumes: list[Volume] | None, + resource_requests: dict[str, str] | None = None, ) -> CreateSandboxRequest: """Convert domain parameters to API CreateSandboxRequest.""" from opensandbox.api.lifecycle.models.create_sandbox_request import ( @@ -243,6 +244,11 @@ def to_api_create_sandbox_request( SandboxModelConverter.to_api_volume(v) for v in volumes ] + # Convert resource requests dict to API model + api_resource_requests = UNSET + if resource_requests is not None: + api_resource_requests = ResourceLimits.from_dict(resource_requests) + request = CreateSandboxRequest( image=SandboxModelConverter.to_api_image_spec(spec), entrypoint=entrypoint, @@ -253,6 +259,7 @@ def to_api_create_sandbox_request( network_policy=api_network_policy, extensions=api_extensions, volumes=api_volumes, + resource_requests=api_resource_requests, ) if timeout is not None: request.timeout = int(timeout.total_seconds()) diff --git a/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py b/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py index c6a70ade..1869d827 100644 --- a/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py +++ b/sdks/sandbox/python/src/opensandbox/adapters/sandboxes_adapter.py @@ -121,6 +121,7 @@ async def create_sandbox( extensions: dict[str, str], volumes: list[Volume] | None, platform: PlatformSpec | None = None, + resource_requests: dict[str, str] | None = None, ) -> SandboxCreateResponse: """Create a new sandbox instance with the specified configuration.""" logger.info(f"Creating sandbox with image: {spec.image}") @@ -139,6 +140,7 @@ async def create_sandbox( network_policy=network_policy, extensions=extensions, volumes=volumes, + resource_requests=resource_requests, ) client = await self._get_client() diff --git a/sdks/sandbox/python/src/opensandbox/api/lifecycle/models/create_sandbox_request.py b/sdks/sandbox/python/src/opensandbox/api/lifecycle/models/create_sandbox_request.py index 4a091b5f..a863a5ac 100644 --- a/sdks/sandbox/python/src/opensandbox/api/lifecycle/models/create_sandbox_request.py +++ b/sdks/sandbox/python/src/opensandbox/api/lifecycle/models/create_sandbox_request.py @@ -57,6 +57,10 @@ class CreateSandboxRequest: New resource types can be added without API changes. Example: {'cpu': '500m', 'memory': '512Mi', 'gpu': '1'}. + resource_requests (ResourceLimits | Unset): Optional resource requests for the sandbox instance + (guaranteed resources). + If omitted, defaults to resourceLimits (Guaranteed QoS). + When specified, enables Burstable QoS with requests < limits. entrypoint (list[str]): The command to execute as the sandbox's entry process (required). Explicitly specifies the user's expected main process, allowing the sandbox management @@ -126,6 +130,7 @@ class CreateSandboxRequest: network_policy: NetworkPolicy | Unset = UNSET volumes: list[Volume] | Unset = UNSET extensions: CreateSandboxRequestExtensions | Unset = UNSET + resource_requests: ResourceLimits | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -168,6 +173,10 @@ def to_dict(self) -> dict[str, Any]: if not isinstance(self.extensions, Unset): extensions = self.extensions.to_dict() + resource_requests: dict[str, Any] | Unset = UNSET + if not isinstance(self.resource_requests, Unset): + resource_requests = self.resource_requests.to_dict() + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -191,6 +200,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["volumes"] = volumes if extensions is not UNSET: field_dict["extensions"] = extensions + if resource_requests is not UNSET: + field_dict["resourceRequests"] = resource_requests return field_dict @@ -265,6 +276,13 @@ def _parse_timeout(data: object) -> int | None | Unset: else: extensions = CreateSandboxRequestExtensions.from_dict(_extensions) + _resource_requests = d.pop("resourceRequests", UNSET) + resource_requests: ResourceLimits | Unset + if isinstance(_resource_requests, Unset): + resource_requests = UNSET + else: + resource_requests = ResourceLimits.from_dict(_resource_requests) + create_sandbox_request = cls( image=image, resource_limits=resource_limits, @@ -276,6 +294,7 @@ def _parse_timeout(data: object) -> int | None | Unset: network_policy=network_policy, volumes=volumes, extensions=extensions, + resource_requests=resource_requests, ) create_sandbox_request.additional_properties = d diff --git a/sdks/sandbox/python/src/opensandbox/sandbox.py b/sdks/sandbox/python/src/opensandbox/sandbox.py index b47838d8..a2ef00da 100644 --- a/sdks/sandbox/python/src/opensandbox/sandbox.py +++ b/sdks/sandbox/python/src/opensandbox/sandbox.py @@ -400,6 +400,7 @@ async def create( env: dict[str, str] | None = None, metadata: dict[str, str] | None = None, resource: dict[str, str] | None = None, + resource_requests: dict[str, str] | None = None, platform: PlatformSpec | None = None, network_policy: NetworkPolicy | None = None, extensions: dict[str, str] | None = None, @@ -420,6 +421,9 @@ async def create( env: Environment variables for the sandbox metadata: Custom metadata for the sandbox resource: Resource limits (CPU, memory, etc.) + resource_requests: Optional resource requests (guaranteed resources). + If omitted, defaults to resource (Guaranteed QoS). + When specified, enables Burstable QoS with requests < limits. network_policy: Optional outbound network policy (egress). extensions: Opaque extension parameters passed through to the server as-is. Prefer namespaced keys (e.g. ``storage.id``). @@ -471,11 +475,14 @@ async def create( volumes, ) if platform is None: - response = await sandbox_service.create_sandbox(*create_args) + response = await sandbox_service.create_sandbox( + *create_args, resource_requests=resource_requests + ) else: response = await sandbox_service.create_sandbox( *create_args, platform=platform, + resource_requests=resource_requests, ) sandbox_id = response.id diff --git a/sdks/sandbox/python/src/opensandbox/services/sandbox.py b/sdks/sandbox/python/src/opensandbox/services/sandbox.py index 53944a17..713c6d9a 100644 --- a/sdks/sandbox/python/src/opensandbox/services/sandbox.py +++ b/sdks/sandbox/python/src/opensandbox/services/sandbox.py @@ -56,6 +56,7 @@ async def create_sandbox( extensions: dict[str, str], volumes: list[Volume] | None, platform: PlatformSpec | None = None, + resource_requests: dict[str, str] | None = None, ) -> SandboxCreateResponse: """ Create a new sandbox with the specified configuration. @@ -71,6 +72,8 @@ async def create_sandbox( extensions: Opaque extension parameters passed through to the server as-is. Prefer namespaced keys (e.g. ``storage.id``). volumes: Optional list of volume mounts for persistent storage. + resource_requests: Optional resource requests (guaranteed resources). + If omitted, defaults to resource limits (Guaranteed QoS). Returns: Sandbox create response diff --git a/sdks/sandbox/python/tests/test_sandbox_business_logic.py b/sdks/sandbox/python/tests/test_sandbox_business_logic.py index aa05f053..da5ca7c5 100644 --- a/sdks/sandbox/python/tests/test_sandbox_business_logic.py +++ b/sdks/sandbox/python/tests/test_sandbox_business_logic.py @@ -307,6 +307,7 @@ async def create_sandbox( network_policy, _extensions, _volumes, + **kwargs, ): assert isinstance(network_policy, NetworkPolicy) return _CreateResponse() diff --git a/server/opensandbox_server/api/schema.py b/server/opensandbox_server/api/schema.py index 831703cd..23291d30 100644 --- a/server/opensandbox_server/api/schema.py +++ b/server/opensandbox_server/api/schema.py @@ -353,6 +353,15 @@ class CreateSandboxRequest(BaseModel): alias="resourceLimits", description="Runtime resource constraints for the sandbox instance", ) + resource_requests: Optional[ResourceLimits] = Field( + None, + alias="resourceRequests", + description=( + "Optional resource requests (guaranteed resources). " + "Defaults to resourceLimits if omitted (Guaranteed QoS). " + "When specified, enables Burstable QoS with requests < limits." + ), + ) env: Optional[Dict[str, Optional[str]]] = Field( None, description="Environment variables to inject into the sandbox runtime", diff --git a/server/opensandbox_server/services/k8s/agent_sandbox_provider.py b/server/opensandbox_server/services/k8s/agent_sandbox_provider.py index 3e32151e..a538a822 100644 --- a/server/opensandbox_server/services/k8s/agent_sandbox_provider.py +++ b/server/opensandbox_server/services/k8s/agent_sandbox_provider.py @@ -141,6 +141,7 @@ def create_workload( annotations: Optional[Dict[str, str]] = None, egress_auth_token: Optional[str] = None, egress_mode: str = EGRESS_MODE_DNS, + resource_requests: Optional[Dict[str, str]] = None, ) -> Dict[str, Any]: """Create an agent-sandbox Sandbox CRD workload.""" if self.runtime_class: @@ -160,6 +161,7 @@ def create_workload( egress_image=egress_image, egress_auth_token=egress_auth_token, egress_mode=egress_mode, + resource_requests=resource_requests, ) # Add user-specified volumes if provided @@ -248,6 +250,7 @@ def _build_pod_spec( egress_image: Optional[str] = None, egress_auth_token: Optional[str] = None, egress_mode: str = EGRESS_MODE_DNS, + resource_requests: Optional[Dict[str, str]] = None, ) -> Dict[str, Any]: """Build pod spec dict for the Sandbox CRD.""" disable_ipv6_for_egress = network_policy is not None and egress_image is not None @@ -261,6 +264,7 @@ def _build_pod_spec( resource_limits=resource_limits, include_execd_volume=True, has_network_policy=network_policy is not None, + resource_requests=resource_requests, ) containers = [self._container_to_dict(main_container)] @@ -340,15 +344,17 @@ def _build_main_container( resource_limits: Dict[str, str], include_execd_volume: bool, has_network_policy: bool = False, + resource_requests: Optional[Dict[str, str]] = None, ) -> V1Container: env_vars = [V1EnvVar(name=k, value=v) for k, v in env.items()] env_vars.append(V1EnvVar(name="EXECD", value="/opt/opensandbox/bin/execd")) resources = None if resource_limits: + requests = resource_requests if resource_requests else resource_limits resources = V1ResourceRequirements( limits=resource_limits, - requests=resource_limits, + requests=requests, ) wrapped_command = ["/opt/opensandbox/bin/bootstrap.sh"] + entrypoint diff --git a/server/opensandbox_server/services/k8s/batchsandbox_provider.py b/server/opensandbox_server/services/k8s/batchsandbox_provider.py index cbec25d9..881af38d 100644 --- a/server/opensandbox_server/services/k8s/batchsandbox_provider.py +++ b/server/opensandbox_server/services/k8s/batchsandbox_provider.py @@ -120,6 +120,7 @@ def create_workload( annotations: Optional[Dict[str, str]] = None, egress_auth_token: Optional[str] = None, egress_mode: str = EGRESS_MODE_DNS, + resource_requests: Optional[Dict[str, str]] = None, ) -> Dict[str, Any]: """ Create a BatchSandbox workload. @@ -203,6 +204,7 @@ def create_workload( env=env, resource_limits=resource_limits, has_network_policy=network_policy is not None, + resource_requests=resource_requests, ) # Build containers list @@ -594,20 +596,22 @@ def _build_main_container( env: Dict[str, str], resource_limits: Dict[str, str], has_network_policy: bool = False, + resource_requests: Optional[Dict[str, str]] = None, ) -> V1Container: """ Build main container spec with execd support. - + The container will use bootstrap script to start execd in background, then execute user's command. - + Args: image_spec: Container image specification entrypoint: Container entrypoint command env: Environment variables resource_limits: Resource limits has_network_policy: Whether network policy is enabled for this sandbox - + resource_requests: Optional resource requests. If None, defaults to resource_limits. + Returns: V1Container: Main container spec """ @@ -615,13 +619,14 @@ def _build_main_container( env_vars = [V1EnvVar(name=k, value=v) for k, v in env.items()] # Add EXECD environment variable to specify execd binary path env_vars.append(V1EnvVar(name="EXECD", value="/opt/opensandbox/bin/execd")) - + # Build resource requirements resources = None if resource_limits: + requests = resource_requests if resource_requests else resource_limits resources = V1ResourceRequirements( limits=resource_limits, - requests=resource_limits, # Set requests = limits for guaranteed QoS + requests=requests, ) # Wrap entrypoint with bootstrap script to start execd diff --git a/server/opensandbox_server/services/k8s/kubernetes_service.py b/server/opensandbox_server/services/k8s/kubernetes_service.py index dba17916..bdcd17f9 100644 --- a/server/opensandbox_server/services/k8s/kubernetes_service.py +++ b/server/opensandbox_server/services/k8s/kubernetes_service.py @@ -337,6 +337,11 @@ async def create_sandbox(self, request: CreateSandboxRequest) -> CreateSandboxRe resource_limits = {} if request.resource_limits and request.resource_limits.root: resource_limits = request.resource_limits.root + + # Extract resource requests (optional, defaults to limits for Guaranteed QoS) + resource_requests = None + if request.resource_requests and request.resource_requests.root: + resource_requests = request.resource_requests.root try: egress_mode = ( @@ -379,6 +384,7 @@ async def create_sandbox(self, request: CreateSandboxRequest) -> CreateSandboxRe egress_mode=egress_mode, volumes=request.volumes, platform=request.platform, + resource_requests=resource_requests, ) logger.info( diff --git a/server/opensandbox_server/services/k8s/workload_provider.py b/server/opensandbox_server/services/k8s/workload_provider.py index e51f6606..2ef63630 100644 --- a/server/opensandbox_server/services/k8s/workload_provider.py +++ b/server/opensandbox_server/services/k8s/workload_provider.py @@ -52,6 +52,7 @@ def create_workload( annotations: Optional[Dict[str, str]] = None, egress_auth_token: Optional[str] = None, egress_mode: str = EGRESS_MODE_DNS, + resource_requests: Optional[Dict[str, str]] = None, ) -> Dict[str, Any]: """ Create a new workload resource. @@ -73,6 +74,8 @@ def create_workload( egress_image: Optional egress sidecar image. Required when network_policy is provided. egress_mode: Sidecar ``OPENSANDBOX_EGRESS_MODE`` (from app ``[egress].mode`` when using network policy). volumes: Optional list of volume mounts for the sandbox. + resource_requests: Optional resource requests (guaranteed resources). + If None, defaults to resource_limits (Guaranteed QoS). Returns: Dict containing workload metadata (name, uid, etc.) diff --git a/specs/sandbox-lifecycle.yml b/specs/sandbox-lifecycle.yml index b2fdc010..b85fa315 100644 --- a/specs/sandbox-lifecycle.yml +++ b/specs/sandbox-lifecycle.yml @@ -766,6 +766,13 @@ components: Runtime resource constraints for the sandbox instance. SDK clients should provide sensible defaults (e.g., cpu: "500m", memory: "512Mi"). + resourceRequests: + $ref: '#/components/schemas/ResourceLimits' + description: | + Optional resource requests for the sandbox instance (guaranteed resources). + If omitted, defaults to resourceLimits (Guaranteed QoS). + When specified, enables Burstable QoS with requests < limits. + env: type: object additionalProperties: From 7439f4c1f3bcff5957b04a6c4cc4e053ae7888c5 Mon Sep 17 00:00:00 2001 From: gaochunhui <2411512182@qq.com> Date: Thu, 9 Apr 2026 10:41:16 +0800 Subject: [PATCH 2/2] feat: add resource_requests for Burstable QoS support --- sdks/sandbox/python/src/opensandbox/sandbox.py | 18 ++++++++---------- .../tests/test_sandbox_business_logic.py | 1 - .../services/k8s/kubernetes_service.py | 5 ++++- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sdks/sandbox/python/src/opensandbox/sandbox.py b/sdks/sandbox/python/src/opensandbox/sandbox.py index a2ef00da..67a389c3 100644 --- a/sdks/sandbox/python/src/opensandbox/sandbox.py +++ b/sdks/sandbox/python/src/opensandbox/sandbox.py @@ -474,16 +474,14 @@ async def create( extensions, volumes, ) - if platform is None: - response = await sandbox_service.create_sandbox( - *create_args, resource_requests=resource_requests - ) - else: - response = await sandbox_service.create_sandbox( - *create_args, - platform=platform, - resource_requests=resource_requests, - ) + create_kwargs: dict = {} + if platform is not None: + create_kwargs["platform"] = platform + if resource_requests is not None: + create_kwargs["resource_requests"] = resource_requests + response = await sandbox_service.create_sandbox( + *create_args, **create_kwargs + ) sandbox_id = response.id execd_endpoint = await sandbox_service.get_sandbox_endpoint( diff --git a/sdks/sandbox/python/tests/test_sandbox_business_logic.py b/sdks/sandbox/python/tests/test_sandbox_business_logic.py index da5ca7c5..aa05f053 100644 --- a/sdks/sandbox/python/tests/test_sandbox_business_logic.py +++ b/sdks/sandbox/python/tests/test_sandbox_business_logic.py @@ -307,7 +307,6 @@ async def create_sandbox( network_policy, _extensions, _volumes, - **kwargs, ): assert isinstance(network_policy, NetworkPolicy) return _CreateResponse() diff --git a/server/opensandbox_server/services/k8s/kubernetes_service.py b/server/opensandbox_server/services/k8s/kubernetes_service.py index bdcd17f9..1414ff4d 100644 --- a/server/opensandbox_server/services/k8s/kubernetes_service.py +++ b/server/opensandbox_server/services/k8s/kubernetes_service.py @@ -366,6 +366,9 @@ async def create_sandbox(self, request: CreateSandboxRequest) -> CreateSandboxRe ) # Create workload + create_kwargs: dict = {} + if resource_requests is not None: + create_kwargs["resource_requests"] = resource_requests workload_info = self.workload_provider.create_workload( sandbox_id=sandbox_id, namespace=self.namespace, @@ -384,7 +387,7 @@ async def create_sandbox(self, request: CreateSandboxRequest) -> CreateSandboxRe egress_mode=egress_mode, volumes=request.volumes, platform=request.platform, - resource_requests=resource_requests, + **create_kwargs, ) logger.info(