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

843 lines
33 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.
"""
BatchSandbox-based workload provider implementation.
"""
import logging
import json
import shlex
from datetime import datetime
from typing import Dict, List, Any, Optional
from opensandbox_server.config import (
AppConfig,
DEFAULT_EGRESS_DISABLE_IPV6,
EGRESS_MODE_DNS,
INGRESS_MODE_GATEWAY,
)
from opensandbox_server.extensions.keys import BOOTSTRAP_EXECD_ISOLATION_KEY
from opensandbox_server.services.constants import OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT
from opensandbox_server.services.helpers import format_ingress_endpoint
from opensandbox_server.api.schema import Endpoint, ImageSpec, NetworkPolicy, PlatformSpec, Volume
from opensandbox_server.services.k8s.image_pull_secret_helper import (
build_image_pull_secret,
build_image_pull_secret_name,
)
from opensandbox_server.services.k8s.batchsandbox_template import BatchSandboxTemplateManager
from opensandbox_server.services.k8s.client import K8sClient
from opensandbox_server.services.k8s.egress_helper import apply_egress_to_spec
from opensandbox_server.services.validators import ensure_egress_runtime_compatible
from opensandbox_server.services.k8s.provider_common import (
DEFAULT_ENTRYPOINT,
_build_execd_init_container,
_build_main_container,
_container_to_dict,
_extract_platform_unschedulable_message_from_pod,
_workload_platform_constraint_scope,
)
from opensandbox_server.services.k8s.windows_profile import (
apply_windows_profile_arch_selector,
apply_windows_profile_overrides,
is_windows_profile,
validate_windows_profile_resource_limits,
)
from opensandbox_server.services.k8s.volume_helper import apply_volumes_to_pod_spec
from opensandbox_server.services.k8s.workload_provider import WorkloadProvider
from opensandbox_server.services.runtime_resolver import SecureRuntimeResolver
logger = logging.getLogger(__name__)
class BatchSandboxProvider(WorkloadProvider):
"""Workload provider for BatchSandbox CRDs."""
def __init__(
self,
k8s_client: K8sClient,
app_config: Optional[AppConfig] = None,
):
self.k8s_client = k8s_client
self.ingress_config = app_config.ingress if app_config else None
k8s_config = app_config.kubernetes if app_config else None
template_file_path = k8s_config.batchsandbox_template_file if k8s_config else None
if template_file_path:
logger.info(f"Using BatchSandbox template file: {template_file_path}")
self.execd_init_resources = k8s_config.execd_init_resources if k8s_config else None
self.image_pull_policy = k8s_config.image_pull_policy if k8s_config else "IfNotPresent"
self.resolver = SecureRuntimeResolver(app_config) if app_config else None
self.runtime_class = (
self.resolver.get_k8s_runtime_class() if self.resolver else None
)
self.group = "sandbox.opensandbox.io"
self.version = "v1alpha1"
self.plural = "batchsandboxes"
self.template_manager = BatchSandboxTemplateManager(template_file_path)
self.egress_disable_ipv6 = (
bool(app_config.egress.disable_ipv6)
if app_config and app_config.egress is not None
else DEFAULT_EGRESS_DISABLE_IPV6
)
def supports_image_auth(self) -> bool:
"""BatchSandbox supports per-request image pull auth."""
return True
def create_workload(
self,
sandbox_id: str,
namespace: str,
image_spec: ImageSpec,
entrypoint: List[str],
env: Dict[str, str],
resource_limits: Dict[str, str],
labels: Dict[str, str],
expires_at: Optional[datetime],
execd_image: str,
extensions: Optional[Dict[str, str]] = None,
network_policy: Optional[NetworkPolicy] = None,
egress_image: Optional[str] = None,
volumes: Optional[List[Volume]] = None,
platform: Optional[PlatformSpec] = None,
annotations: Optional[Dict[str, str]] = None,
egress_auth_token: Optional[str] = None,
egress_mode: str = EGRESS_MODE_DNS,
credential_proxy_enabled: bool = False,
resource_requests: Optional[Dict[str, str]] = None,
egress_env: Optional[Dict[str, Optional[str]]] = None,
) -> Dict[str, Any]:
"""Create a BatchSandbox in template mode or pool mode."""
extensions = extensions or {}
windows_profile = is_windows_profile(platform)
if self.runtime_class:
logger.info(f"Using Kubernetes RuntimeClass '{self.runtime_class}' for sandbox {sandbox_id}")
if extensions.get("poolRef"):
if platform is not None:
raise ValueError(
"platform is not supported together with extensions.poolRef yet. "
"Pool-level platform modeling is not available in this iteration."
)
if volumes:
raise ValueError(
"Pool mode does not support volumes. "
"Remove 'volumes' from request or use template mode."
)
return self._create_workload_from_pool(
batchsandbox_name=sandbox_id,
namespace=namespace,
labels=labels,
pool_ref=extensions["poolRef"],
expires_at=expires_at,
entrypoint=entrypoint,
env=env,
annotations=annotations,
)
extra_volumes, extra_mounts = self._extract_template_pod_extras()
if windows_profile:
validate_windows_profile_resource_limits(resource_limits)
disable_ipv6_for_egress = (
network_policy is not None
and egress_image is not None
and self.egress_disable_ipv6
)
init_container = _build_execd_init_container(
execd_image,
self.execd_init_resources,
disable_ipv6_for_egress=disable_ipv6_for_egress,
)
main_env = dict(env)
if credential_proxy_enabled:
main_env[OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT] = "true"
main_container = _build_main_container(
image_spec=image_spec,
entrypoint=entrypoint,
env=main_env,
resource_limits=resource_limits,
has_network_policy=network_policy is not None,
isolation_enabled=(extensions or {}).get(BOOTSTRAP_EXECD_ISOLATION_KEY) == "enable",
image_pull_policy=self.image_pull_policy,
resource_requests=resource_requests or None,
)
containers = [_container_to_dict(main_container)]
pod_volumes = [
{
"name": "opensandbox-bin",
"emptyDir": {}
}
]
if (extensions or {}).get(BOOTSTRAP_EXECD_ISOLATION_KEY) == "enable":
pod_volumes.append({
"name": "isolation-upper",
"emptyDir": {}
})
pod_spec = {
"automountServiceAccountToken": False,
"initContainers": [_container_to_dict(init_container)],
"containers": containers,
"volumes": pod_volumes,
}
if windows_profile:
apply_windows_profile_overrides(
pod_spec=pod_spec,
entrypoint=entrypoint,
env=env,
resource_limits=resource_limits,
disable_ipv6_for_egress=disable_ipv6_for_egress,
resource_requests=resource_requests or None,
)
template = self.template_manager.get_base_template()
template_spec = (
template.get("spec", {})
.get("template", {})
.get("spec", {})
)
apply_windows_profile_arch_selector(
pod_spec=pod_spec,
template_spec=template_spec if isinstance(template_spec, dict) else {},
platform=platform,
)
else:
self._apply_platform_node_selector(pod_spec, platform)
containers = pod_spec.get("containers", [])
if self.runtime_class:
pod_spec["runtimeClassName"] = self.runtime_class
if image_spec.auth:
secret_name = build_image_pull_secret_name(sandbox_id)
pod_spec["imagePullSecrets"] = [{"name": secret_name}]
apply_egress_to_spec(
containers=containers,
network_policy=network_policy,
egress_image=egress_image,
egress_auth_token=egress_auth_token,
egress_mode=egress_mode,
credential_proxy_enabled=credential_proxy_enabled,
extra_env=egress_env,
)
if volumes:
apply_volumes_to_pod_spec(pod_spec, volumes)
spec: Dict[str, Any] = {
"replicas": 1,
"template": {
"metadata": {
"labels": labels,
"annotations": annotations or {},
},
"spec": pod_spec,
},
}
runtime_manifest = {
"apiVersion": f"{self.group}/{self.version}",
"kind": "BatchSandbox",
"metadata": {
"name": sandbox_id,
"namespace": namespace,
"labels": labels,
},
"spec": spec,
}
if annotations:
runtime_manifest["metadata"]["annotations"] = annotations
batchsandbox = self.template_manager.merge_with_runtime_values(runtime_manifest)
if expires_at is None:
batchsandbox["spec"].pop("expireTime", None)
else:
batchsandbox["spec"]["expireTime"] = expires_at.isoformat()
self._merge_pod_spec_extras(batchsandbox, extra_volumes, extra_mounts)
merged_pod_spec = batchsandbox.get("spec", {}).get("template", {}).get("spec", {})
ensure_egress_runtime_compatible(
network_policy,
effective_runtime_class=merged_pod_spec.get("runtimeClassName"),
)
if platform is not None and not windows_profile:
WorkloadProvider.ensure_platform_compatible_with_affinity(merged_pod_spec, platform)
created = self.k8s_client.create_custom_object(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
body=batchsandbox,
)
if image_spec.auth:
secret = build_image_pull_secret(
sandbox_id=sandbox_id,
image_uri=image_spec.uri,
auth=image_spec.auth,
owner_uid=created["metadata"]["uid"],
owner_api_version=f"{self.group}/{self.version}",
owner_kind="BatchSandbox",
)
try:
self.k8s_client.create_secret(namespace=namespace, body=secret)
logger.info(f"Created imagePullSecret for sandbox {sandbox_id}")
except Exception:
logger.warning(f"Failed to create imagePullSecret for sandbox {sandbox_id}, rolling back BatchSandbox")
try:
self.k8s_client.delete_custom_object(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
name=sandbox_id,
grace_period_seconds=0,
)
except Exception as del_exc:
logger.warning(f"Failed to rollback BatchSandbox {sandbox_id}: {del_exc}")
raise
return {
"name": created["metadata"]["name"],
"uid": created["metadata"]["uid"],
"apiVersion": f"{self.group}/{self.version}",
"kind": "BatchSandbox",
}
def _apply_platform_node_selector(
self,
pod_spec: Dict[str, Any],
platform: Optional[PlatformSpec],
) -> None:
if platform is None:
return
template = self.template_manager.get_base_template()
template_spec = (
template.get("spec", {})
.get("template", {})
.get("spec", {})
)
WorkloadProvider.apply_platform_node_selector(
pod_spec=pod_spec,
template_spec=template_spec if isinstance(template_spec, dict) else {},
platform=platform,
)
def _create_workload_from_pool(
self,
batchsandbox_name: str,
namespace: str,
labels: Dict[str, str],
pool_ref: str,
expires_at: Optional[datetime],
entrypoint: List[str],
env: Dict[str, str],
annotations: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""Create a BatchSandbox by referencing an existing pool."""
entrypoint = entrypoint or DEFAULT_ENTRYPOINT
spec: Dict[str, Any] = {
"replicas": 1,
"poolRef": pool_ref,
}
needs_task_template = env or entrypoint != DEFAULT_ENTRYPOINT
if needs_task_template:
spec["taskTemplate"] = self._build_task_template(entrypoint, env)
if expires_at is not None:
spec["expireTime"] = expires_at.isoformat()
runtime_manifest = {
"apiVersion": f"{self.group}/{self.version}",
"kind": "BatchSandbox",
"metadata": {
"name": batchsandbox_name,
"namespace": namespace,
"labels": labels,
},
"spec": spec,
}
if annotations:
runtime_manifest["metadata"]["annotations"] = annotations
created = self.k8s_client.create_custom_object(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
body=runtime_manifest,
)
return {
"name": created["metadata"]["name"],
"uid": created["metadata"]["uid"],
"apiVersion": f"{self.group}/{self.version}",
"kind": "BatchSandbox",
}
def _extract_template_pod_extras(self) -> tuple[list[Dict[str, Any]], list[Dict[str, Any]]]:
"""Extract extra template volumes and mounts for runtime merge."""
template = self.template_manager.get_base_template()
spec = template.get("spec", {}) if isinstance(template, dict) else {}
template_spec = spec.get("template", {}).get("spec", {})
extra_volumes = template_spec.get("volumes", []) or []
extra_mounts: list[Dict[str, Any]] = []
containers = template_spec.get("containers", []) or []
if containers:
target = None
for container in containers:
if container.get("name") == "sandbox":
target = container
break
if target is None:
target = containers[0]
extra_mounts = target.get("volumeMounts", []) or []
if not isinstance(extra_volumes, list):
extra_volumes = []
if not isinstance(extra_mounts, list):
extra_mounts = []
return extra_volumes, extra_mounts
def _merge_pod_spec_extras(
self,
batchsandbox: Dict[str, Any],
extra_volumes: list[Dict[str, Any]],
extra_mounts: list[Dict[str, Any]],
) -> None:
"""Merge template-provided volumes and mounts into runtime pod spec."""
try:
spec = batchsandbox["spec"]["template"]["spec"]
except KeyError:
return
volumes = spec.get("volumes", []) or []
if isinstance(volumes, list) and extra_volumes:
existing = {v.get("name") for v in volumes if isinstance(v, dict)}
for vol in extra_volumes:
if not isinstance(vol, dict):
continue
name = vol.get("name")
if not name or name in existing:
continue
volumes.append(vol)
existing.add(name)
spec["volumes"] = volumes
containers = spec.get("containers", []) or []
if not containers or not isinstance(containers, list):
return
main_container = containers[0]
mounts = main_container.get("volumeMounts", []) or []
if isinstance(mounts, list) and extra_mounts:
existing = {m.get("name") for m in mounts if isinstance(m, dict)}
for mnt in extra_mounts:
if not isinstance(mnt, dict):
continue
name = mnt.get("name")
if not name or name in existing:
continue
mounts.append(mnt)
existing.add(name)
main_container["volumeMounts"] = mounts
def _build_task_template(
self,
entrypoint: List[str],
env: Dict[str, str],
) -> Dict[str, Any]:
"""Build pool taskTemplate with shell-escaped bootstrap command."""
escaped_entrypoint = ' '.join(shlex.quote(arg) for arg in entrypoint)
user_process_cmd = f"/opt/opensandbox/bootstrap.sh {escaped_entrypoint} &"
wrapped_command = ["/bin/sh", "-c", user_process_cmd]
env_list = [{"name": k, "value": v} for k, v in env.items()] if env else []
return {
"spec": {
"process": {
"command": wrapped_command,
"env": env_list,
}
}
}
def get_workload(self, sandbox_id: str, namespace: str) -> Optional[Dict[str, Any]]:
"""Get BatchSandbox by sandbox ID."""
workload = self.k8s_client.get_custom_object(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
name=sandbox_id,
)
if workload:
return workload
legacy_name = self.legacy_resource_name(sandbox_id)
if legacy_name != sandbox_id:
return self.k8s_client.get_custom_object(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
name=legacy_name,
)
return None
def delete_workload(self, sandbox_id: str, namespace: str) -> None:
"""Delete BatchSandbox workload."""
batchsandbox = self.get_workload(sandbox_id, namespace)
if not batchsandbox:
raise Exception(f"BatchSandbox for sandbox {sandbox_id} not found")
self.k8s_client.delete_custom_object(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
name=batchsandbox["metadata"]["name"],
grace_period_seconds=0,
)
def list_workloads(self, namespace: str, label_selector: str) -> List[Dict[str, Any]]:
"""List BatchSandboxes matching label selector."""
return self.k8s_client.list_custom_objects(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
label_selector=label_selector,
)
def patch_workload(self, sandbox_id: str, namespace: str, spec_patch: Dict[str, Any]) -> Dict[str, Any]:
"""Patch BatchSandbox spec (e.g., spec.pause for pause/resume)."""
batchsandbox = self.get_workload(sandbox_id, namespace)
if not batchsandbox:
return None
return self.k8s_client.patch_custom_object(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
name=batchsandbox["metadata"]["name"],
body=spec_patch,
)
@staticmethod
def _has_true_condition(conditions: List[Dict[str, Any]], condition_type: str) -> bool:
for cond in conditions:
if cond.get("type") == condition_type and cond.get("status") == "True":
return True
return False
@staticmethod
def _first_true_condition_message(conditions: List[Dict[str, Any]], condition_types: List[str]) -> Optional[str]:
for condition_type in condition_types:
for cond in conditions:
if cond.get("type") == condition_type and cond.get("status") == "True":
message = cond.get("message")
if isinstance(message, str) and message.strip():
return message
return None
def _patch_pause_with_retry_bridge(self, sandbox_id: str, namespace: str, target: Optional[bool]) -> None:
self.patch_workload(sandbox_id, namespace, {"spec": {"pause": None}})
try:
self.patch_workload(sandbox_id, namespace, {"spec": {"pause": target}})
return
except Exception as exc:
current = self.get_workload(sandbox_id, namespace)
current_pause = None if not current else current.get("spec", {}).get("pause")
if current is not None and current_pause == target:
logger.warning(
"BatchSandbox %s retry bridge target patch raised %s but read-back confirmed spec.pause=%s",
sandbox_id,
type(exc).__name__,
target,
)
return
logger.warning(
"BatchSandbox %s retry bridge target patch raised %s and current spec.pause=%s; retrying target patch once",
sandbox_id,
type(exc).__name__,
current_pause,
)
retried = self.patch_workload(sandbox_id, namespace, {"spec": {"pause": target}})
if retried is None:
raise exc
def update_expiration(self, sandbox_id: str, namespace: str, expires_at: datetime) -> None:
"""Update BatchSandbox `spec.expireTime`."""
batchsandbox = self.get_workload(sandbox_id, namespace)
if not batchsandbox:
raise Exception(f"BatchSandbox for sandbox {sandbox_id} not found")
body = {
"spec": {
"expireTime": expires_at.isoformat()
}
}
self.k8s_client.patch_custom_object(
group=self.group,
version=self.version,
namespace=namespace,
plural=self.plural,
name=batchsandbox["metadata"]["name"],
body=body,
)
def pause_sandbox(self, sandbox_id: str, namespace: str) -> None:
"""Pause a BatchSandbox by patching spec.pause=true.
Validates that the current status.phase allows pause:
- Succeed: allowed (fresh pause)
- Succeed + PauseFailed=True: allowed (retry after failure, server internally patches nil->true)
- Pausing/Resuming: not allowed (operation in progress)
- Paused: not allowed (already paused)
- Failed: not allowed (sandbox unavailable)
- Failed + PauseFailed=True: not allowed (sandbox unavailable)
"""
batchsandbox = self.get_workload(sandbox_id, namespace)
if not batchsandbox:
raise ValueError(f"Sandbox '{sandbox_id}' not found")
status = batchsandbox.get("status", {})
phase = status.get("phase", "")
conditions = status.get("conditions", [])
pause_failed = self._has_true_condition(conditions, "PauseFailed")
if phase == "Succeed":
pass
elif phase == "Pausing":
raise ValueError(f"Cannot pause: operation in progress (phase={phase})")
elif phase == "Resuming":
raise ValueError(f"Cannot pause: operation in progress (phase={phase})")
elif phase == "Paused":
raise ValueError("Sandbox is already paused")
elif phase == "Failed":
if pause_failed:
raise ValueError("Cannot pause: sandbox is not available (pause caused pod loss)")
else:
raise ValueError("Cannot pause: sandbox is not available")
elif phase == "Pending":
raise ValueError(f"Cannot pause: sandbox is being created (phase={phase})")
else:
raise ValueError(f"Cannot pause sandbox in phase {phase}")
if pause_failed:
self._patch_pause_with_retry_bridge(sandbox_id, namespace, True)
logger.info("Patched BatchSandbox %s retry bridge spec.pause=nil->true", sandbox_id)
else:
self.patch_workload(sandbox_id, namespace, {"spec": {"pause": True}})
logger.info("Patched BatchSandbox %s spec.pause=true", sandbox_id)
def resume_sandbox(self, sandbox_id: str, namespace: str) -> None:
"""Resume a BatchSandbox by patching spec.pause=false.
Validates that the current status.phase allows resume:
- Paused: allowed (fresh resume)
- Paused + ResumeFailed=True: allowed (retry after failure, server internally patches nil->false)
- Resuming/Pausing: not allowed (operation in progress)
- Succeed: not allowed (not paused)
- Failed: not allowed (sandbox unavailable)
"""
batchsandbox = self.get_workload(sandbox_id, namespace)
if not batchsandbox:
raise ValueError(f"Sandbox '{sandbox_id}' not found")
status = batchsandbox.get("status", {})
phase = status.get("phase", "")
conditions = status.get("conditions", [])
resume_failed = self._has_true_condition(conditions, "ResumeFailed")
# Allow resume when Paused (or Paused with ResumeFailed for retry)
if phase == "Paused":
# Always allowed, even if ResumeFailed=True (retry scenario)
pass
elif phase == "Resuming":
raise ValueError(f"Cannot resume: operation in progress (phase={phase})")
elif phase == "Pausing":
raise ValueError(f"Cannot resume: operation in progress (phase={phase})")
elif phase == "Succeed":
raise ValueError(f"Cannot resume sandbox in phase {phase}, expected Paused")
elif phase == "Failed":
if resume_failed:
raise ValueError("Cannot resume: sandbox is not available (resume caused pod start failure)")
else:
raise ValueError("Cannot resume: sandbox is not available")
elif phase == "Pending":
raise ValueError(f"Cannot resume: sandbox is being created (phase={phase})")
else:
raise ValueError(f"Cannot resume sandbox in phase {phase}, expected Paused")
if resume_failed:
self._patch_pause_with_retry_bridge(sandbox_id, namespace, False)
logger.info("Patched BatchSandbox %s retry bridge spec.pause=nil->false", sandbox_id)
else:
self.patch_workload(sandbox_id, namespace, {"spec": {"pause": False}})
logger.info("Patched BatchSandbox %s spec.pause=false", sandbox_id)
def get_expiration(self, workload: Dict[str, Any]) -> Optional[datetime]:
"""Parse expiration timestamp from `spec.expireTime`."""
spec = workload.get("spec", {})
expire_time_str = spec.get("expireTime")
if not expire_time_str:
return None
try:
return datetime.fromisoformat(expire_time_str.replace('Z', '+00:00'))
except (ValueError, TypeError) as e:
logger.warning(f"Invalid expireTime format: {expire_time_str}, error: {e}")
return None
def _parse_pod_ip(self, workload: Dict[str, Any]) -> Optional[str]:
"""Parse first pod IP from endpoints annotation."""
annotations = workload.get("metadata", {}).get("annotations", {})
endpoints_str = annotations.get("sandbox.opensandbox.io/endpoints")
if not endpoints_str:
return None
try:
endpoints = json.loads(endpoints_str)
if endpoints and len(endpoints) > 0:
return endpoints[0]
except (json.JSONDecodeError, IndexError, TypeError):
pass
return None
def _platform_unschedulable_message_from_selector(self, workload: Dict[str, Any]) -> Optional[str]:
workload_has_platform_constraints, workload_has_non_platform_constraints = _workload_platform_constraint_scope(
workload,
"template",
self.analyze_platform_constraints_in_pod_spec,
)
if not workload_has_platform_constraints:
return None
status = workload.get("status", {})
selector = status.get("selector")
namespace = workload.get("metadata", {}).get("namespace")
if not selector or not namespace:
return None
try:
pods = self.k8s_client.list_pods(
namespace=namespace,
label_selector=selector,
)
except Exception:
return None
for pod in pods:
message = _extract_platform_unschedulable_message_from_pod(
pod,
workload_has_platform_constraints,
workload_has_non_platform_constraints,
self.is_platform_unschedulable,
)
if message:
return message
return None
def get_status(self, workload: Dict[str, Any]) -> Dict[str, Any]:
"""Derive sandbox state from BatchSandbox status and pod readiness."""
status = workload.get("status", {})
creation_timestamp = workload.get("metadata", {}).get("creationTimestamp")
# Phase is authoritative when set (Pausing/Paused/Resuming/Failed)
phase = status.get("phase", "")
failed_message = self._first_true_condition_message(
status.get("conditions", []),
["PodFailed", "ResumeFailed", "PauseFailed"],
)
phase_map = {
"Pending": ("Pending", "CREATING", "Sandbox is being created"),
"Succeed": ("Running", "RUNNING", "Sandbox is running"),
"Running": ("Running", "RUNNING", "Sandbox is running"),
"Pausing": ("Pausing", "PAUSING", "Pausing sandbox"),
"Paused": ("Paused", "PAUSED", "Sandbox is paused"),
"Resuming": ("Resuming", "RESUMING", "Resuming sandbox"),
"Failed": ("Failed", "FAILED", failed_message or "Operation failed"),
}
if phase in phase_map:
state, reason, message = phase_map[phase]
return {
"state": state,
"reason": reason,
"message": message,
"last_transition_at": creation_timestamp,
}
# Fallback: derive from pod state
replicas = status.get("replicas", 0)
ready = status.get("ready", 0)
allocated = status.get("allocated", 0)
pod_ip = self._parse_pod_ip(workload)
if ready == 1 and pod_ip:
state = "Running"
reason = "POD_READY_WITH_IP"
message = f"Pod is ready with IP ({ready}/{replicas} ready)"
elif pod_ip:
state = "Allocated"
reason = "IP_ASSIGNED"
message = f"Pod has IP assigned but not ready ({allocated}/{replicas} allocated, {ready} ready)"
else:
unschedulable_message = self._platform_unschedulable_message_from_selector(workload)
if unschedulable_message:
state = "Failed"
reason = "POD_PLATFORM_UNSCHEDULABLE"
message = unschedulable_message
else:
state = "Pending"
reason = "POD_SCHEDULED" if allocated > 0 else "BATCHSANDBOX_PENDING"
message = (
f"Pod is scheduled but waiting for IP ({allocated}/{replicas} allocated, {ready} ready)"
if allocated > 0
else "BatchSandbox is pending allocation"
)
return {
"state": state,
"reason": reason,
"message": message,
"last_transition_at": creation_timestamp,
}
def get_endpoint_info(self, workload: Dict[str, Any], port: int, sandbox_id: str) -> Optional[Endpoint]:
"""Resolve endpoint using gateway ingress or parsed pod IP."""
if self.ingress_config and self.ingress_config.mode == INGRESS_MODE_GATEWAY:
return format_ingress_endpoint(self.ingress_config, sandbox_id, port)
pod_ip = self._parse_pod_ip(workload)
if not pod_ip:
return None
return Endpoint(endpoint=f"{pod_ip}:{port}")