|
1 | 1 | import contextlib |
2 | 2 | import sys |
3 | 3 | from os import PathLike |
| 4 | +from pathlib import Path |
4 | 5 | from socket import socket |
5 | 6 | from types import TracebackType |
6 | 7 | from typing import TYPE_CHECKING, Any, Optional, TypedDict, Union, cast |
|
18 | 19 | from testcontainers.core.exceptions import ContainerConnectException, ContainerStartException |
19 | 20 | from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID |
20 | 21 | from testcontainers.core.network import Network |
21 | | -from testcontainers.core.utils import is_arm, setup_logger |
| 22 | +from testcontainers.core.utils import build_tar_file, is_arm, setup_logger |
22 | 23 | from testcontainers.core.wait_strategies import LogMessageWaitStrategy |
23 | 24 | from testcontainers.core.waiting_utils import WaitStrategy |
24 | 25 |
|
@@ -97,6 +98,7 @@ def __init__( |
97 | 98 |
|
98 | 99 | self._kwargs = kwargs |
99 | 100 | self._wait_strategy: Optional[WaitStrategy] = _wait_strategy |
| 101 | + self._copy_to_container: list[tuple[str, Union[bytes, Path]]] = [] |
100 | 102 |
|
101 | 103 | def with_env(self, key: str, value: str) -> Self: |
102 | 104 | self.env[key] = value |
@@ -190,17 +192,21 @@ def start(self) -> Self: |
190 | 192 | else {} |
191 | 193 | ) |
192 | 194 |
|
193 | | - self._container = docker_client.run( |
| 195 | + self._container = docker_client.create( |
194 | 196 | self.image, |
195 | 197 | command=self._command, |
196 | | - detach=True, |
197 | 198 | environment=self.env, |
198 | 199 | ports=cast("dict[int, Optional[int]]", self.ports), |
199 | 200 | name=self._name, |
200 | 201 | volumes=self.volumes, |
201 | 202 | **{**network_kwargs, **self._kwargs}, |
202 | 203 | ) |
203 | 204 |
|
| 205 | + for target, source in self._copy_to_container: |
| 206 | + self._container.put_archive("/", build_tar_file(target, source)) |
| 207 | + |
| 208 | + docker_client.start(self._container) |
| 209 | + |
204 | 210 | if self._wait_strategy is not None: |
205 | 211 | self._wait_strategy.wait_until_ready(self) |
206 | 212 |
|
@@ -270,6 +276,27 @@ def with_volume_mapping(self, host: Union[str, PathLike[str]], container: str, m |
270 | 276 | self.volumes[str(host)] = mapping |
271 | 277 | return self |
272 | 278 |
|
| 279 | + def with_copy_to(self, target: str, source: Union[bytes, str, PathLike[str]]) -> Self: |
| 280 | + """ |
| 281 | + Copy a file, directory, or raw bytes into the container at startup. |
| 282 | +
|
| 283 | + :param target: Absolute path inside the container where the data should be placed. |
| 284 | + :param source: Either ``bytes``/``bytearray`` (raw file content) or a path |
| 285 | + (``str`` / :class:`pathlib.Path`) to a local file or directory. |
| 286 | +
|
| 287 | + :doctest: |
| 288 | +
|
| 289 | + >>> from testcontainers.core.container import DockerContainer |
| 290 | + >>> container = DockerContainer("alpine") |
| 291 | + >>> container = container.with_copy_to("/tmp/hello.txt", b"hello world") |
| 292 | +
|
| 293 | + """ |
| 294 | + if isinstance(source, (bytes, bytearray)): |
| 295 | + self._copy_to_container.append((target, bytes(source))) |
| 296 | + else: |
| 297 | + self._copy_to_container.append((target, Path(source))) |
| 298 | + return self |
| 299 | + |
273 | 300 | def get_wrapped_container(self) -> "Container": |
274 | 301 | return self._container |
275 | 302 |
|
@@ -301,6 +328,13 @@ def exec(self, command: Union[str, list[str]]) -> ExecResult: |
301 | 328 | raise ContainerStartException("Container should be started before executing a command") |
302 | 329 | return self._container.exec_run(command) |
303 | 330 |
|
| 331 | + def wait(self) -> int: |
| 332 | + """Wait for the container to stop and return its exit code.""" |
| 333 | + if not self._container: |
| 334 | + raise ContainerStartException("Container should be started before waiting") |
| 335 | + result = self._container.wait() |
| 336 | + return int(result["StatusCode"]) |
| 337 | + |
304 | 338 | def _configure(self) -> None: |
305 | 339 | # placeholder if subclasses want to define this and use the default start method |
306 | 340 | pass |
|
0 commit comments