ref #5: [feat] add docker impl
This commit is contained in:
parent
3448266c1d
commit
87c789b7fe
6 changed files with 316 additions and 1 deletions
134
adapter/docker/runtime.py
Normal file
134
adapter/docker/runtime.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
from collections.abc import Callable
|
||||
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
|
||||
|
||||
type NowFactory = Callable[[datetime], datetime]
|
||||
|
||||
|
||||
class DockerSandboxRuntime(SandboxRuntime):
|
||||
def __init__(
|
||||
self,
|
||||
config: SandboxConfig,
|
||||
client: DockerClient,
|
||||
now: NowFactory | None = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._client = client
|
||||
self._now = _current_time if now is None else now
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
session_id: str,
|
||||
chat_id: str,
|
||||
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=self._now(expires_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)
|
||||
|
||||
|
||||
def _current_time(expires_at: datetime) -> datetime:
|
||||
return datetime.now(tz=expires_at.tzinfo)
|
||||
Loading…
Add table
Add a link
Reference in a new issue