Spaces:
Paused
Paused
File size: 6,709 Bytes
9792ea7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | # -*- coding: utf-8 -*-
"""Docker container :class:`BackendBase` implementation.
Wraps the ``aiodocker`` container APIs (``exec``, ``get_archive``,
``put_archive``) into the three backend primitives (``exec_shell``,
``read_file``, ``write_file``) so that builtin tools (Bash, Read,
Write, Edit, Grep, Glob) can operate inside a Docker container
transparently. All derived filesystem helpers (``file_exists``,
``is_dir``, ``list_dir``, ``stat_mtime``, ``delete_path``) are
inherited from :class:`BackendBase`, which implements them via
``exec_shell``.
"""
from __future__ import annotations
import asyncio
import io
import posixpath
import tarfile
from typing import Any
from ...tool import BackendBase, ExecResult
class DockerBackend(BackendBase):
"""Backend that delegates to a running Docker container.
Only the three abstract primitives (``exec_shell``, ``read_file``,
``write_file``) are implemented here; the derived filesystem helpers
are inherited from :class:`BackendBase`.
Args:
container (`Any`):
An ``aiodocker`` container object (must already be started).
workdir (`str`):
Default working directory for ``exec_shell`` calls inside
the container.
"""
def __init__(self, container: Any, workdir: str) -> None:
"""Initialize the Docker backend.
Args:
container (`Any`):
A started ``aiodocker`` container object.
workdir (`str`):
Default working directory for ``exec_shell`` calls
inside the container.
"""
self._container = container
self._workdir = workdir
# ββ exec βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async def getcwd(self) -> str:
"""Return the container's default working directory.
Overrides the base class default (which would shell out to
``pwd``) with the cached ``workdir`` supplied at construction,
avoiding a per-call container ``exec`` round-trip.
Returns:
`str`:
The container's default working directory.
"""
return self._workdir
async def exec_shell(
self,
command: list[str],
*,
cwd: str | None = None,
timeout: float | None = None,
) -> ExecResult:
"""Run a program directly inside the container.
*command* is an argv list executed via the container ``exec``
API without an intervening shell. Callers needing shell features
wrap their command line as ``["sh", "-c", line]``.
Args:
command (`list[str]`):
Executable path/name followed by its arguments.
cwd (`str | None`, optional):
Working directory inside the container. When ``None``
the backend's default ``workdir`` is used.
timeout (`float | None`, optional):
Maximum number of seconds to wait before returning an
``exit_code`` of ``-1``. When ``None`` the call waits
indefinitely.
Returns:
`ExecResult`:
The captured exit code, stdout, and stderr.
"""
async def _run() -> ExecResult:
exec_obj = await self._container.exec(
cmd=command,
workdir=cwd or self._workdir,
)
stdout_parts: list[bytes] = []
stderr_parts: list[bytes] = []
async with exec_obj.start() as stream:
while True:
msg = await stream.read_out()
if msg is None:
break
if msg.stream == 1:
stdout_parts.append(msg.data)
else:
stderr_parts.append(msg.data)
inspect = await exec_obj.inspect()
code = inspect.get("ExitCode", -1)
if code is None:
code = -1
return ExecResult(
exit_code=int(code),
stdout=b"".join(stdout_parts),
stderr=b"".join(stderr_parts),
)
if timeout is None:
return await _run()
try:
return await asyncio.wait_for(_run(), timeout=timeout)
except asyncio.TimeoutError:
return ExecResult(
exit_code=-1,
stdout=b"",
stderr=b"timed out",
)
# ββ file I/O βββββββββββββββββββββββββββββββββββββββββββββββββββ
async def read_file(self, path: str) -> bytes:
"""Fetch a file from the container via ``get_archive``.
Args:
path (`str`):
Path to the file inside the container.
Returns:
`bytes`:
The raw file contents.
Raises:
`FileNotFoundError`:
If the path does not exist inside the container.
"""
from aiodocker import exceptions as aiodocker_exceptions
try:
tar = await self._container.get_archive(path)
except aiodocker_exceptions.DockerError as exc:
if exc.status == 404:
raise FileNotFoundError(
f"not found in container: {path}",
) from exc
raise
try:
for member in tar.getmembers():
if member.isfile():
f = tar.extractfile(member)
if f:
return f.read()
finally:
tar.close()
raise FileNotFoundError(f"not found in container: {path}")
async def write_file(self, path: str, data: bytes) -> None:
"""Write raw bytes to a file inside the container.
Creates the parent directory first since ``put_archive``
requires it to exist.
Args:
path (`str`):
Destination path inside the container.
data (`bytes`):
The raw bytes to write.
"""
parent = posixpath.dirname(path) or "/"
name = posixpath.basename(path)
await self.exec_shell(["mkdir", "-p", parent])
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
info = tarfile.TarInfo(name=name)
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
await self._container.put_archive(parent, buf.getvalue())
|