e0e362d700
SDK Tests / SDK CI (push) Blocked by required conditions
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
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
275 lines
12 KiB
Python
275 lines
12 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.
|
|
|
|
"""
|
|
Runtime preparation mixin for Docker sandboxes.
|
|
|
|
Provides execd archive distribution and bootstrap launcher installation.
|
|
Mixed into DockerSandboxService.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import logging
|
|
import posixpath
|
|
import tarfile
|
|
import time
|
|
from typing import Optional
|
|
from uuid import uuid4
|
|
|
|
from docker.errors import DockerException
|
|
from fastapi import HTTPException, status
|
|
|
|
from opensandbox_server.api.schema import PlatformSpec
|
|
from opensandbox_server.services.constants import SandboxErrorCodes
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
OPENSANDBOX_DIR = "/opt/opensandbox"
|
|
# Use posixpath for container-internal paths so they always use forward slashes,
|
|
# even when the server runs on Windows.
|
|
EXECED_INSTALL_PATH = posixpath.join(OPENSANDBOX_DIR, "execd")
|
|
BOOTSTRAP_PATH = posixpath.join(OPENSANDBOX_DIR, "bootstrap.sh")
|
|
DEFAULT_EXECD_ENVS_PATH = posixpath.join(OPENSANDBOX_DIR, ".env")
|
|
|
|
|
|
class DockerRuntimeMixin:
|
|
"""Mixin providing execd distribution and bootstrap launcher installation."""
|
|
|
|
def _fetch_execd_archive(self, platform: Optional[PlatformSpec] = None) -> bytes:
|
|
"""Fetch (and memoize) the execd archive by effective target platform."""
|
|
cache_key = self._normalize_platform_key(platform)
|
|
if cache_key in self._execd_archive_cache:
|
|
return self._execd_archive_cache[cache_key]
|
|
|
|
with self._execd_archive_lock:
|
|
# Double-check locking to ensure only one thread initializes the cache
|
|
if cache_key in self._execd_archive_cache:
|
|
return self._execd_archive_cache[cache_key]
|
|
|
|
container = None
|
|
docker_platform = None
|
|
if platform is not None:
|
|
docker_platform = f"{platform.os}/{platform.arch}"
|
|
try:
|
|
self._ensure_image_available(
|
|
self.execd_image,
|
|
auth_config=None,
|
|
sandbox_id=f"execd-cache:{cache_key}",
|
|
platform=platform,
|
|
)
|
|
|
|
with self._docker_operation("execd cache create container", "execd-cache"):
|
|
create_kwargs: dict[str, any] = {
|
|
"image": self.execd_image,
|
|
"command": ["tail", "-f", "/dev/null"],
|
|
"name": f"sandbox-execd-{uuid4()}",
|
|
"detach": True,
|
|
"auto_remove": False,
|
|
}
|
|
if docker_platform is not None:
|
|
create_kwargs["platform"] = docker_platform
|
|
container = self.docker_client.containers.create(**create_kwargs)
|
|
with self._docker_operation("execd cache start container", "execd-cache"):
|
|
container.start()
|
|
container.reload()
|
|
logger.info("Created sandbox execd archive for container %s", container.id)
|
|
except TypeError as exc:
|
|
if docker_platform is not None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={
|
|
"code": SandboxErrorCodes.INVALID_PARAMETER,
|
|
"message": (
|
|
"The configured Docker client/daemon does not support "
|
|
f"platform-aware container create for '{docker_platform}'."
|
|
),
|
|
},
|
|
) from exc
|
|
raise
|
|
except DockerException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_START_FAILED,
|
|
"message": f"Failed to start execd container: {str(exc)}",
|
|
},
|
|
) from exc
|
|
|
|
try:
|
|
with self._docker_operation("execd cache read archive", "execd-cache"):
|
|
stream, _ = container.get_archive("/execd")
|
|
data = b"".join(stream)
|
|
# Also cache the full bootstrap.sh while the container is running.
|
|
if cache_key not in self._bootstrap_script_cache:
|
|
with self._docker_operation("execd cache read bootstrap", "execd-cache"):
|
|
bs_stream, _ = container.get_archive("/bootstrap.sh")
|
|
self._bootstrap_script_cache[cache_key] = b"".join(bs_stream)
|
|
# Cache bwrap binary (best-effort — may not exist in older images).
|
|
if cache_key not in self._bwrap_archive_cache:
|
|
try:
|
|
with self._docker_operation("execd cache read bwrap", "execd-cache"):
|
|
bwrap_stream, _ = container.get_archive("/usr/local/bin/bwrap")
|
|
self._bwrap_archive_cache[cache_key] = b"".join(bwrap_stream)
|
|
except DockerException:
|
|
logger.warning("bwrap not found in execd image — isolation will be unavailable, upgrade execd image to v1.1.0+")
|
|
except DockerException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": f"Failed to read execd artifacts: {str(exc)}",
|
|
},
|
|
) from exc
|
|
finally:
|
|
if container:
|
|
try:
|
|
with self._docker_operation("execd cache cleanup container", "execd-cache"):
|
|
container.remove(force=True)
|
|
except DockerException as cleanup_exc:
|
|
logger.warning(
|
|
"Failed to cleanup temporary execd container: %s", cleanup_exc
|
|
)
|
|
|
|
self._execd_archive_cache[cache_key] = data
|
|
logger.info("Dumped execd archive to memory for platform key %s", cache_key)
|
|
return data
|
|
|
|
def _ensure_directory(self, container, path: str, sandbox_id: Optional[str] = None) -> None:
|
|
"""Create a directory within the target container if it does not exist."""
|
|
if not path or path == "/":
|
|
return
|
|
normalized_path = path.rstrip("/")
|
|
if not normalized_path:
|
|
return
|
|
tar_stream = io.BytesIO()
|
|
with tarfile.open(fileobj=tar_stream, mode="w") as tar:
|
|
dir_info = tarfile.TarInfo(name=normalized_path.lstrip("/"))
|
|
dir_info.type = tarfile.DIRTYPE
|
|
dir_info.mode = 0o755
|
|
dir_info.mtime = int(time.time())
|
|
tar.addfile(dir_info)
|
|
tar_stream.seek(0)
|
|
try:
|
|
with self._docker_operation(f"ensure directory {normalized_path}", sandbox_id):
|
|
container.put_archive(path="/", data=tar_stream.getvalue())
|
|
except DockerException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": f"Failed to create directory {path} in sandbox: {str(exc)}",
|
|
},
|
|
) from exc
|
|
|
|
def _copy_execd_to_container(
|
|
self,
|
|
container,
|
|
sandbox_id: str,
|
|
platform: Optional[PlatformSpec] = None,
|
|
) -> None:
|
|
"""Copy execd artifacts from the platform container into the sandbox."""
|
|
archive = self._fetch_execd_archive(platform)
|
|
target_parent = posixpath.dirname(EXECED_INSTALL_PATH.rstrip("/")) or "/"
|
|
self._ensure_directory(container, target_parent, sandbox_id)
|
|
try:
|
|
with self._docker_operation("copy execd archive to sandbox", sandbox_id):
|
|
container.put_archive(path=target_parent, data=archive)
|
|
except DockerException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.EXECD_DISTRIBUTION_FAILED,
|
|
"message": f"Failed to copy execd into sandbox: {str(exc)}",
|
|
},
|
|
) from exc
|
|
|
|
def _install_bootstrap_script(
|
|
self,
|
|
container,
|
|
sandbox_id: str,
|
|
platform: Optional[PlatformSpec] = None,
|
|
) -> None:
|
|
"""Install the full bootstrap.sh from the execd image into the sandbox.
|
|
|
|
Uses the same execd container that _copy_execd_to_container already
|
|
created, so there is no extra container lifecycle overhead.
|
|
"""
|
|
cache_key = self._normalize_platform_key(platform)
|
|
# _copy_execd_to_container is called first and ensures both the execd
|
|
# archive and the bootstrap script are already in the caches.
|
|
archive = self._bootstrap_script_cache.get(cache_key)
|
|
if archive is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.BOOTSTRAP_INSTALL_FAILED,
|
|
"message": "bootstrap.sh not found in execd image cache",
|
|
},
|
|
)
|
|
|
|
script_dir = posixpath.dirname(BOOTSTRAP_PATH)
|
|
self._ensure_directory(container, script_dir, sandbox_id)
|
|
|
|
try:
|
|
with self._docker_operation("install bootstrap script", sandbox_id):
|
|
container.put_archive(path=script_dir, data=archive)
|
|
except DockerException as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.BOOTSTRAP_INSTALL_FAILED,
|
|
"message": f"Failed to install bootstrap launcher: {str(exc)}",
|
|
},
|
|
) from exc
|
|
|
|
def _copy_bwrap_to_container(
|
|
self,
|
|
container,
|
|
sandbox_id: str,
|
|
platform: Optional[PlatformSpec] = None,
|
|
) -> None:
|
|
"""Copy bwrap binary into /opt/opensandbox/bin/ (best-effort).
|
|
|
|
Bwrap may not exist in older execd images, so failure is non-fatal —
|
|
isolation will simply be unavailable at runtime.
|
|
"""
|
|
cache_key = self._normalize_platform_key(platform)
|
|
archive = self._bwrap_archive_cache.get(cache_key)
|
|
if archive is None:
|
|
logger.warning("bwrap archive not cached for %s — isolation will be unavailable, upgrade execd image to v1.1.0+", cache_key)
|
|
return
|
|
|
|
try:
|
|
with self._docker_operation("copy bwrap to sandbox", sandbox_id):
|
|
container.put_archive(path=OPENSANDBOX_DIR, data=archive)
|
|
except DockerException as exc:
|
|
logger.warning(
|
|
"Failed to copy bwrap into sandbox %s: %s (isolation will be unavailable)",
|
|
sandbox_id,
|
|
exc,
|
|
)
|
|
|
|
def _prepare_sandbox_runtime(
|
|
self,
|
|
container,
|
|
sandbox_id: str,
|
|
platform: Optional[PlatformSpec] = None,
|
|
) -> None:
|
|
"""Copy execd artifacts and bootstrap launcher into the sandbox container."""
|
|
self._copy_execd_to_container(container, sandbox_id, platform)
|
|
self._install_bootstrap_script(container, sandbox_id, platform)
|
|
self._copy_bwrap_to_container(container, sandbox_id, platform)
|