Skip to content

Commit 72c7f86

Browse files
committed
fix running testcontainer inside a container
see testcontainers#475 (comment) for a summary
1 parent 8df7dc9 commit 72c7f86

File tree

3 files changed

+79
-29
lines changed

3 files changed

+79
-29
lines changed

core/testcontainers/core/config.py

+34
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
from dataclasses import dataclass, field
2+
from enum import Enum
23
from logging import warning
34
from os import environ
45
from os.path import exists
56
from pathlib import Path
67
from typing import Optional, Union
78

9+
10+
class ConnectionMode(Enum):
11+
bridge_ip = "bridge_ip"
12+
gateway_ip = "gateway_ip"
13+
docker_host = "docker_host"
14+
15+
@property
16+
def use_mapped_port(self) -> bool:
17+
"""
18+
Return true if we need to use mapped port for this connection
19+
20+
This is true for everything but bridge mode.
21+
"""
22+
if self == self.bridge_ip:
23+
return False
24+
return True
25+
26+
827
MAX_TRIES = int(environ.get("TC_MAX_TRIES", 120))
928
SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1))
1029
TIMEOUT = MAX_TRIES * SLEEP_TIME
@@ -20,6 +39,19 @@
2039
TC_GLOBAL = Path.home() / TC_FILE
2140

2241

42+
def get_user_overwritten_connection_mode() -> Optional[ConnectionMode]:
43+
"""
44+
Return the user overwritten connection mode.
45+
"""
46+
connection_mode: str | None = environ.get("TESTCONTAINERS_CONNECTION_MODE")
47+
if connection_mode:
48+
try:
49+
return ConnectionMode(connection_mode)
50+
except ValueError as e:
51+
raise ValueError(f"Error parsing TESTCONTAINERS_CONNECTION_MODE: {e}") from e
52+
return None
53+
54+
2355
def read_tc_properties() -> dict[str, str]:
2456
"""
2557
Read the .testcontainers.properties for settings. (see the Java implementation for details)
@@ -54,6 +86,8 @@ class TestcontainersConfiguration:
5486
tc_properties: dict[str, str] = field(default_factory=read_tc_properties)
5587
_docker_auth_config: Optional[str] = field(default_factory=lambda: environ.get("DOCKER_AUTH_CONFIG"))
5688
tc_host_override: Optional[str] = TC_HOST_OVERRIDE
89+
connection_mode_override: Optional[ConnectionMode] = None
90+
5791
"""
5892
https://github.com/testcontainers/testcontainers-go/blob/dd76d1e39c654433a3d80429690d07abcec04424/docker.go#L644
5993
if os env TC_HOST is set, use it

core/testcontainers/core/container.py

+18-27
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
import docker.errors
66
from docker import version
77
from docker.types import EndpointConfig
8-
from typing_extensions import Self
8+
from typing_extensions import Self, assert_never
99

10+
from testcontainers.core.config import ConnectionMode
1011
from testcontainers.core.config import testcontainers_config as c
1112
from testcontainers.core.docker_client import DockerClient
1213
from testcontainers.core.exceptions import ContainerStartException
1314
from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID
1415
from testcontainers.core.network import Network
15-
from testcontainers.core.utils import inside_container, is_arm, setup_logger
16+
from testcontainers.core.utils import is_arm, setup_logger
1617
from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs
1718

1819
if TYPE_CHECKING:
@@ -128,33 +129,23 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
128129
self.stop()
129130

130131
def get_container_host_ip(self) -> str:
131-
# infer from docker host
132-
host = self.get_docker_client().host()
133-
134-
# # check testcontainers itself runs inside docker container
135-
# if inside_container() and not os.getenv("DOCKER_HOST") and not host.startswith("http://"):
136-
# # If newly spawned container's gateway IP address from the docker
137-
# # "bridge" network is equal to detected host address, we should use
138-
# # container IP address, otherwise fall back to detected host
139-
# # address. Even it's inside container, we need to double check,
140-
# # because docker host might be set to docker:dind, usually in CI/CD environment
141-
# gateway_ip = self.get_docker_client().gateway_ip(self._container.id)
142-
143-
# if gateway_ip == host:
144-
# return self.get_docker_client().bridge_ip(self._container.id)
145-
# return gateway_ip
146-
return host
132+
connection_mode: ConnectionMode
133+
connection_mode = self.get_docker_client().get_connection_mode()
134+
if connection_mode == ConnectionMode.docker_host:
135+
return self.get_docker_client().host()
136+
elif connection_mode == ConnectionMode.gateway_ip:
137+
return self.get_docker_client().gateway_ip(self._container.id)
138+
elif connection_mode == ConnectionMode.bridge_ip:
139+
return self.get_docker_client().bridge_ip(self._container.id)
140+
else:
141+
# ensure that we covered all possible connection_modes
142+
assert_never(connection_mode)
147143

148144
@wait_container_is_ready()
149-
def get_exposed_port(self, port: int) -> str:
150-
mapped_port = self.get_docker_client().port(self._container.id, port)
151-
if inside_container():
152-
gateway_ip = self.get_docker_client().gateway_ip(self._container.id)
153-
host = self.get_docker_client().host()
154-
155-
if gateway_ip == host:
156-
return port
157-
return mapped_port
145+
def get_exposed_port(self, port: int) -> int:
146+
if self.get_docker_client().get_connection_mode().use_mapped_port:
147+
return self.get_docker_client().port(self._container.id, port)
148+
return port
158149

159150
def with_command(self, command: str) -> Self:
160151
self._command = command

core/testcontainers/core/docker_client.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import importlib.metadata
1515
import ipaddress
1616
import os
17+
import socket
1718
import urllib
1819
import urllib.parse
1920
from collections.abc import Iterable
@@ -25,6 +26,7 @@
2526
from typing_extensions import ParamSpec
2627

2728
from testcontainers.core.auth import DockerAuthInfo, parse_docker_auth_config
29+
from testcontainers.core.config import ConnectionMode
2830
from testcontainers.core.config import testcontainers_config as c
2931
from testcontainers.core.labels import SESSION_ID, create_labels
3032
from testcontainers.core.utils import default_gateway_ip, inside_container, is_windows, setup_logger
@@ -128,7 +130,8 @@ def find_host_network(self) -> Optional[str]:
128130
# If we're docker in docker running on a custom network, we need to inherit the
129131
# network settings, so we can access the resulting container.
130132
try:
131-
docker_host = ipaddress.IPv4Address(self.host())
133+
host_ip = socket.gethostbyname(self.host())
134+
docker_host = ipaddress.IPv4Address(host_ip)
132135
# See if we can find the host on our networks
133136
for network in self.client.networks.list(filters={"type": "custom"}):
134137
if "IPAM" in network.attrs:
@@ -139,7 +142,7 @@ def find_host_network(self) -> Optional[str]:
139142
continue
140143
if docker_host in subnet:
141144
return network.name
142-
except ipaddress.AddressValueError:
145+
except (ipaddress.AddressValueError, OSError):
143146
pass
144147
return None
145148

@@ -187,6 +190,28 @@ def gateway_ip(self, container_id: str) -> str:
187190
network_name = self.network_name(container_id)
188191
return container["NetworkSettings"]["Networks"][network_name]["Gateway"]
189192

193+
def get_connection_mode(self) -> ConnectionMode:
194+
"""
195+
Determine the connection mode.
196+
197+
See https://github.com/testcontainers/testcontainers-python/issues/475#issuecomment-2407250970
198+
"""
199+
if c.connection_mode_override:
200+
return c.connection_mode_override
201+
localhosts = {"localhost", "127.0.0.1", "::1"}
202+
if not inside_container() or self.host() not in localhosts:
203+
# if running not inside a container or with a non-local docker client,
204+
# connect ot the docker host per default
205+
return ConnectionMode.docker_host
206+
elif self.find_host_network():
207+
# a host network could be determined, indicator for DooD,
208+
# so we should connect to the bridge_ip as the container we run in
209+
# and the one we started are connected to the same network
210+
# that might have no access to either docker_host or the gateway
211+
return ConnectionMode.bridge_ip
212+
# default for DinD
213+
return ConnectionMode.gateway_ip
214+
190215
def host(self) -> str:
191216
"""
192217
Get the hostname or ip address of the docker host.

0 commit comments

Comments
 (0)