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
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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,
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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(
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion sdks/sandbox/python/src/opensandbox/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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``).
Expand Down Expand Up @@ -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
)
Comment on lines +478 to +480
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve old create_sandbox service signatures

Sandbox.create now always forwards resource_requests= even when the caller did not set it. Any existing Sandboxes implementation that still uses the previous signature (without resource_requests and without a catch-all **kwargs) will now raise TypeError: got an unexpected keyword argument 'resource_requests', breaking integrations that were previously compatible when platform was unset.

Useful? React with 👍 / 👎.

else:
response = await sandbox_service.create_sandbox(
*create_args,
platform=platform,
resource_requests=resource_requests,
)
sandbox_id = response.id

Expand Down
3 changes: 3 additions & 0 deletions sdks/sandbox/python/src/opensandbox/services/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions sdks/sandbox/python/tests/test_sandbox_business_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ async def create_sandbox(
network_policy,
_extensions,
_volumes,
**kwargs,
):
assert isinstance(network_policy, NetworkPolicy)
return _CreateResponse()
Expand Down
9 changes: 9 additions & 0 deletions server/opensandbox_server/api/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)]
Expand Down Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions server/opensandbox_server/services/k8s/batchsandbox_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -594,34 +596,37 @@ 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
"""
# Convert env dict to V1EnvVar list and inject EXECD path
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep custom workload providers backward compatible

KubernetesSandboxService now unconditionally passes resource_requests= into create_workload. Providers registered through the documented register_provider extension point that still implement the old create_workload signature will fail sandbox creation with an unexpected-keyword TypeError, even when the API request omits resourceRequests (because None is still sent).

Useful? React with 👍 / 👎.

)

logger.info(
Expand Down
3 changes: 3 additions & 0 deletions server/opensandbox_server/services/k8s/workload_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.)
Expand Down
7 changes: 7 additions & 0 deletions specs/sandbox-lifecycle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down