126 lines
3.9 KiB
Python
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)
|