|
1 | 1 | import contextlib |
| 2 | +from os import PathLike |
2 | 3 | from socket import socket |
3 | 4 | from typing import TYPE_CHECKING, Optional, Union |
4 | 5 |
|
5 | 6 | import docker.errors |
6 | 7 | from docker import version |
7 | 8 | from docker.types import EndpointConfig |
| 9 | +from dotenv import dotenv_values |
8 | 10 | from typing_extensions import Self, assert_never |
9 | 11 |
|
10 | 12 | from testcontainers.core.config import ConnectionMode |
11 | 13 | from testcontainers.core.config import testcontainers_config as c |
12 | 14 | from testcontainers.core.docker_client import DockerClient |
13 | | -from testcontainers.core.exceptions import ContainerStartException |
| 15 | +from testcontainers.core.exceptions import ContainerConnectException, ContainerStartException |
14 | 16 | from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID |
15 | 17 | from testcontainers.core.network import Network |
16 | 18 | from testcontainers.core.utils import is_arm, setup_logger |
@@ -57,11 +59,44 @@ def with_env(self, key: str, value: str) -> Self: |
57 | 59 | self.env[key] = value |
58 | 60 | return self |
59 | 61 |
|
60 | | - def with_bind_ports(self, container: int, host: Optional[int] = None) -> Self: |
| 62 | + def with_env_file(self, env_file: Union[str, PathLike]) -> Self: |
| 63 | + env_values = dotenv_values(env_file) |
| 64 | + for key, value in env_values.items(): |
| 65 | + self.with_env(key, value) |
| 66 | + return self |
| 67 | + |
| 68 | + def with_bind_ports(self, container: Union[str, int], host: Optional[Union[str, int]] = None) -> Self: |
| 69 | + """ |
| 70 | + Bind container port to host port |
| 71 | +
|
| 72 | + :param container: container port |
| 73 | + :param host: host port |
| 74 | +
|
| 75 | + :doctest: |
| 76 | +
|
| 77 | + >>> from testcontainers.core.container import DockerContainer |
| 78 | + >>> container = DockerContainer("nginx") |
| 79 | + >>> container = container.with_bind_ports("8080/tcp", 8080) |
| 80 | + >>> container = container.with_bind_ports("8081/tcp", 8081) |
| 81 | +
|
| 82 | + """ |
61 | 83 | self.ports[container] = host |
62 | 84 | return self |
63 | 85 |
|
64 | | - def with_exposed_ports(self, *ports: int) -> Self: |
| 86 | + def with_exposed_ports(self, *ports: Union[str, int]) -> Self: |
| 87 | + """ |
| 88 | + Expose ports from the container without binding them to the host. |
| 89 | +
|
| 90 | + :param ports: ports to expose |
| 91 | +
|
| 92 | + :doctest: |
| 93 | +
|
| 94 | + >>> from testcontainers.core.container import DockerContainer |
| 95 | + >>> container = DockerContainer("nginx") |
| 96 | + >>> container = container.with_exposed_ports("8080/tcp", "8081/tcp") |
| 97 | +
|
| 98 | + """ |
| 99 | + |
65 | 100 | for port in ports: |
66 | 101 | self.ports[port] = None |
67 | 102 | return self |
@@ -147,7 +182,7 @@ def get_exposed_port(self, port: int) -> int: |
147 | 182 | return self.get_docker_client().port(self._container.id, port) |
148 | 183 | return port |
149 | 184 |
|
150 | | - def with_command(self, command: str) -> Self: |
| 185 | + def with_command(self, command: Union[str, list[str]]) -> Self: |
151 | 186 | self._command = command |
152 | 187 | return self |
153 | 188 |
|
@@ -220,15 +255,21 @@ def _create_instance(cls) -> "Reaper": |
220 | 255 | .with_env("RYUK_RECONNECTION_TIMEOUT", c.ryuk_reconnection_timeout) |
221 | 256 | .start() |
222 | 257 | ) |
223 | | - wait_for_logs(Reaper._container, r".* Started!") |
| 258 | + wait_for_logs(Reaper._container, r".* Started!", timeout=20, raise_on_exit=True) |
224 | 259 |
|
225 | 260 | container_host = Reaper._container.get_container_host_ip() |
226 | 261 | container_port = int(Reaper._container.get_exposed_port(8080)) |
227 | 262 |
|
| 263 | + if not container_host or not container_port: |
| 264 | + raise ContainerConnectException( |
| 265 | + f"Could not obtain network details for {Reaper._container._container.id}. Host: {container_host} Port: {container_port}" |
| 266 | + ) |
| 267 | + |
228 | 268 | last_connection_exception: Optional[Exception] = None |
229 | 269 | for _ in range(50): |
230 | 270 | try: |
231 | 271 | Reaper._socket = socket() |
| 272 | + Reaper._socket.settimeout(1) |
232 | 273 | Reaper._socket.connect((container_host, container_port)) |
233 | 274 | last_connection_exception = None |
234 | 275 | break |
|
0 commit comments