master/adapter/docker/runtime.py
2026-04-02 13:41:41 +03:00

126 lines
3.9 KiB
Python

from datetime import datetime
from pathlib import Path
from docker import DockerClient
from docker.errors import DockerException, NotFound
from docker.types import Mount
from adapter.config.model import SandboxConfig
from domain.error import SandboxError, SandboxStartError
from domain.sandbox import SandboxSession, SandboxStatus
from usecase.interface import SandboxRuntime
class DockerSandboxRuntime(SandboxRuntime):
def __init__(
self,
config: SandboxConfig,
client: DockerClient,
) -> None:
self._config = config
self._client = client
def create(
self,
*,
session_id: str,
chat_id: str,
created_at: datetime,
expires_at: datetime,
) -> SandboxSession:
try:
chat_path = self._chat_path(chat_id)
dependencies_path = self._readonly_host_path(
self._config.dependencies_host_path
)
lambda_tools_path = self._readonly_host_path(
self._config.lambda_tools_host_path
)
chat_path.mkdir(parents=True, exist_ok=True)
container = self._client.containers.run(
self._config.image,
detach=True,
labels=self._labels(session_id, chat_id, expires_at),
mounts=self._mounts(chat_path, dependencies_path, lambda_tools_path),
)
except (DockerException, OSError, ValueError) as exc:
raise SandboxStartError(chat_id) from exc
container_id = str(getattr(container, 'id', '')).strip()
if not container_id:
raise SandboxStartError(chat_id)
return SandboxSession(
session_id=session_id,
chat_id=chat_id,
container_id=container_id,
status=SandboxStatus.RUNNING,
created_at=created_at,
expires_at=expires_at,
)
def stop(self, container_id: str) -> None:
try:
container = self._client.containers.get(container_id)
container.stop()
except NotFound:
return
except DockerException as exc:
raise SandboxError('sandbox_stop_failed') from exc
def _labels(
self,
session_id: str,
chat_id: str,
expires_at: datetime,
) -> dict[str, str]:
return {
'session_id': session_id,
'chat_id': chat_id,
'expires_at': expires_at.isoformat(),
}
def _mounts(
self,
chat_path: Path,
dependencies_path: Path,
lambda_tools_path: Path,
) -> list[Mount]:
return [
Mount(
target=self._config.chat_mount_path,
source=str(chat_path),
type='bind',
),
Mount(
target=self._config.dependencies_mount_path,
source=str(dependencies_path),
type='bind',
read_only=True,
),
Mount(
target=self._config.lambda_tools_mount_path,
source=str(lambda_tools_path),
type='bind',
read_only=True,
),
]
def _chat_path(self, chat_id: str) -> Path:
if not chat_id.strip():
raise ValueError('invalid chat path')
chats_root = self._host_path(self._config.chats_root)
chat_path = (chats_root / chat_id).resolve(strict=False)
if not chat_path.is_relative_to(chats_root):
raise ValueError('invalid chat path')
return chat_path
def _readonly_host_path(self, path_value: str) -> Path:
host_path = self._host_path(path_value)
if not host_path.exists():
raise ValueError('invalid host path')
return host_path
def _host_path(self, path_value: str) -> Path:
return Path(path_value).expanduser().resolve(strict=False)