Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

280 lines
9.1 KiB
Python

# 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.
"""
Shared helpers for container-based sandbox services.
These utilities centralize common parsing, filtering, and transformation logic
so multiple container runtimes (docker, kubernetes, etc.) can reuse them.
"""
from __future__ import annotations
import logging
import re
from datetime import datetime, timezone
from typing import Dict, Optional
from opensandbox_server.api.schema import Endpoint, Sandbox, SandboxFilter
from opensandbox_server.services.constants import (
ALLOWED_EGRESS_ENV_VARS,
EGRESS_ENV_PREFIX,
OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT,
OPEN_SANDBOX_INGRESS_HEADER,
)
from opensandbox_server.config import (
GATEWAY_ROUTE_MODE_HEADER,
GATEWAY_ROUTE_MODE_URI,
GATEWAY_ROUTE_MODE_WILDCARD,
INGRESS_MODE_GATEWAY,
IngressConfig,
)
logger = logging.getLogger(__name__)
MEMORY_PATTERN = re.compile(r"^\s*(\d+)([kmgti]i?|[kmgti]?b)?\s*$", re.IGNORECASE)
MEMORY_MULTIPLIERS: Dict[str, int] = {
"": 1,
"b": 1,
"k": 1_000,
"kb": 1_000,
"ki": 1024,
"m": 1_000_000,
"mb": 1_000_000,
"mi": 1024**2,
"g": 1_000_000_000,
"gb": 1_000_000_000,
"gi": 1024**3,
"t": 1_000_000_000_000,
"tb": 1_000_000_000_000,
"ti": 1024**4,
}
def parse_memory_limit(value: Optional[str]) -> Optional[int]:
"""Convert memory string (e.g., 512Mi) to bytes."""
if not value:
return None
match = MEMORY_PATTERN.match(value)
if not match:
logger.warning("Invalid memory limit format '%s'; ignoring.", value)
return None
amount = int(match.group(1))
unit = (match.group(2) or "").lower()
multiplier = MEMORY_MULTIPLIERS.get(unit)
if not multiplier:
logger.warning("Unsupported memory unit '%s'; ignoring.", unit)
return None
return amount * multiplier
def parse_nano_cpus(value: Optional[str]) -> Optional[int]:
"""Convert CPU string (e.g., 500m, 2) to nano_cpus."""
if not value:
return None
cpu_str = value.strip().lower()
try:
if cpu_str.endswith("m"):
cpus = float(cpu_str[:-1]) / 1000
else:
cpus = float(cpu_str)
except ValueError:
logger.warning("Invalid CPU limit format '%s'; ignoring.", value)
return None
if cpus <= 0:
logger.warning("CPU limit must be positive. Got '%s'. Ignoring.", value)
return None
return int(cpus * 1_000_000_000)
def parse_gpu_request(value: Optional[str]) -> Optional[int]:
"""Convert GPU limit string to a device count.
Accepts a positive integer string (e.g. ``"1"``, ``"2"``) or the literal
``"all"`` to request every available GPU. Returns ``-1`` for ``"all"``
(the sentinel the Docker Engine uses for unbounded device requests),
a positive int for a numeric count, and ``None`` when unset or the
value cannot be parsed.
"""
if not value:
return None
gpu_str = value.strip().lower()
if gpu_str == "all":
return -1
try:
count = int(gpu_str)
except ValueError:
logger.warning("Invalid GPU limit format '%s'; ignoring.", value)
return None
if count <= 0:
logger.warning("GPU limit must be positive. Got '%s'. Ignoring.", value)
return None
return count
def parse_timestamp(timestamp: Optional[str]) -> datetime:
"""
Parse RFC3339 timestamp into timezone-aware datetime. Fallback to now.
Docker often returns RFC3339Nano (up to 9 fractional digits). Python's
datetime.fromisoformat only supports microseconds (6 digits), so we
truncate the fractional part to 6 digits before parsing.
"""
if not timestamp or timestamp == "0001-01-01T00:00:00Z":
return datetime.now(timezone.utc)
normalized = timestamp
if normalized.endswith("Z"):
normalized = normalized[:-1] + "+00:00"
if "." in normalized:
main, rest = normalized.split(".", 1)
tz_sep = None
for sep in ("+", "-"):
pos = rest.find(sep)
if pos != -1:
tz_sep = pos
break
if tz_sep is None:
frac = rest
tz = ""
else:
frac = rest[:tz_sep]
tz = rest[tz_sep:]
frac = frac[:6] # truncate to microseconds precision
normalized = f"{main}.{frac}{tz}" if frac else f"{main}{tz}"
try:
return datetime.fromisoformat(normalized)
except ValueError:
logger.warning("Invalid timestamp '%s'; defaulting to current time.", timestamp)
return datetime.now(timezone.utc)
def normalize_external_endpoint_url(endpoint: str, default_scheme: str = "https") -> str:
"""Normalize host or URL to a full URL with an explicit scheme."""
endpoint = endpoint.strip()
if endpoint.startswith("http://") or endpoint.startswith("https://"):
return endpoint
return f"{default_scheme}://{endpoint}"
def matches_filter(sandbox: Sandbox, filter_: SandboxFilter) -> bool:
"""Apply state/metadata filters to a sandbox instance."""
if not filter_:
return True
if filter_.state:
desired = {state.lower() for state in filter_.state}
current_state = (sandbox.status.state or "").lower()
if current_state not in desired:
return False
if filter_.metadata:
metadata = sandbox.metadata or {}
for key, value in filter_.metadata.items():
if metadata.get(key) != value:
return False
return True
# ============================================================================
# Ingress helpers
# ============================================================================
def format_ingress_endpoint(
ingress_config: Optional[IngressConfig],
sandbox_id: str,
port: int,
expires_b36: Optional[str] = None,
signature: Optional[str] = None,
) -> Optional[Endpoint]:
"""
Build an ingress-based endpoint string for a sandbox.
When *expires_b36* and *signature* are provided, the endpoint embeds a
signed route token (OSEP-0011). Otherwise a plain ingress endpoint is returned.
Returns None when ingress is not in gateway mode.
"""
if not ingress_config or ingress_config.mode != INGRESS_MODE_GATEWAY:
return None
gateway_cfg = ingress_config.gateway
if gateway_cfg is None:
return None
address = gateway_cfg.address
route_mode = gateway_cfg.route.mode
is_signed = expires_b36 is not None and signature is not None
if route_mode == GATEWAY_ROUTE_MODE_WILDCARD:
base = address[2:] if address.startswith("*.") else address
if is_signed:
return Endpoint(endpoint=f"{sandbox_id}-{port}-{expires_b36}-{signature}.{base}")
return Endpoint(endpoint=f"{sandbox_id}-{port}.{base}")
if route_mode == GATEWAY_ROUTE_MODE_URI:
if is_signed:
return Endpoint(endpoint=f"{address}/{sandbox_id}/{port}/{expires_b36}/{signature}")
return Endpoint(endpoint=f"{address}/{sandbox_id}/{port}")
if route_mode == GATEWAY_ROUTE_MODE_HEADER:
if is_signed:
header_value = f"{sandbox_id}-{port}-{expires_b36}-{signature}"
else:
header_value = f"{sandbox_id}-{port}"
return Endpoint(
endpoint=address,
headers={OPEN_SANDBOX_INGRESS_HEADER: header_value},
)
raise RuntimeError(f"Unsupported route mode: {route_mode}")
def split_egress_env(
env: Optional[Dict[str, Optional[str]]],
) -> tuple[Dict[str, Optional[str]], Dict[str, Optional[str]]]:
"""Split request env into (sandbox_env, egress_env) by OPENSANDBOX_EGRESS_ prefix.
Only env vars listed in ALLOWED_EGRESS_ENV_VARS are forwarded to the egress
sidecar. Any other OPENSANDBOX_EGRESS_ key raises ValueError.
"""
if not env:
return {}, {}
sandbox_env: Dict[str, Optional[str]] = {}
egress_env: Dict[str, Optional[str]] = {}
for key, value in env.items():
if key.startswith(EGRESS_ENV_PREFIX):
if key not in ALLOWED_EGRESS_ENV_VARS:
raise ValueError(
f"Environment variable '{key}' is not allowed; "
f"permitted OPENSANDBOX_EGRESS_ keys: {sorted(ALLOWED_EGRESS_ENV_VARS)}"
)
egress_env[key] = value
if key == OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT:
sandbox_env[key] = value
else:
sandbox_env[key] = value
return sandbox_env, egress_env
__all__ = [
"parse_memory_limit",
"parse_nano_cpus",
"parse_gpu_request",
"parse_timestamp",
"normalize_external_endpoint_url",
"format_ingress_endpoint",
"matches_filter",
"split_egress_env",
]