Skip to content

Commit 8eeff41

Browse files
Merge pull request #85 from tuvshuud/master
Added docker host detection and support for setting container host explicitly with env variable
2 parents 1e4fa30 + 6979ad1 commit 8eeff41

File tree

5 files changed

+75
-15
lines changed

5 files changed

+75
-15
lines changed

testcontainers/core/container.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,33 @@ def __del__(self):
7777
pass
7878

7979
def get_container_host_ip(self) -> str:
80-
# if testcontainers itself runs in docker, get the newly spawned
81-
# container's IP address from the dockder "bridge" network
80+
# infer from docker host
81+
host = self.get_docker_client().host()
82+
if not host:
83+
return "localhost"
84+
85+
# check testcontainers itself runs inside docker container
8286
if inside_container():
83-
return self.get_docker_client().bridge_ip(self._container.id)
84-
return "localhost"
87+
# If newly spawned container's gateway IP address from the docker
88+
# "bridge" network is equal to detected host address, we should use
89+
# container IP address, otherwise fall back to detected host
90+
# address. Even it's inside container, we need to double check,
91+
# because docker host might be set to docker:dind, usually in CI/CD environment
92+
gateway_ip = self.get_docker_client().gateway_ip(self._container.id)
93+
94+
if gateway_ip == host:
95+
return self.get_docker_client().bridge_ip(self._container.id)
96+
return host
8597

8698
def get_exposed_port(self, port) -> str:
99+
mapped_port = self.get_docker_client().port(self._container.id, port)
87100
if inside_container():
88-
return port
89-
else:
90-
return self.get_docker_client().port(self._container.id, port)
101+
gateway_ip = self.get_docker_client().gateway_ip(self._container.id)
102+
host = self.get_docker_client().host()
103+
104+
if gateway_ip == host:
105+
return port
106+
return mapped_port
91107

92108
def with_command(self, command: str) -> 'DockerContainer':
93109
self._command = command

testcontainers/core/docker_client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
13+
import os
14+
import urllib
1315
import docker
1416
from docker.models.containers import Container
17+
from testcontainers.core.utils import inside_container
18+
from testcontainers.core.utils import default_gateway_ip
1519

1620

1721
class DockerClient(object):
@@ -42,3 +46,27 @@ def port(self, container_id, port):
4246
def bridge_ip(self, container_id):
4347
container = self.client.api.containers(filters={'id': container_id})[0]
4448
return container['NetworkSettings']['Networks']['bridge']['IPAddress']
49+
50+
def gateway_ip(self, container_id):
51+
container = self.client.api.containers(filters={'id': container_id})[0]
52+
return container['NetworkSettings']['Networks']['bridge']['Gateway']
53+
54+
def host(self):
55+
# https://github.com/testcontainers/testcontainers-go/blob/dd76d1e39c654433a3d80429690d07abcec04424/docker.go#L644
56+
# if os env TC_HOST is set, use it
57+
host = os.environ.get('TC_HOST')
58+
if host:
59+
return host
60+
try:
61+
url = urllib.parse.urlparse(self.client.api.base_url)
62+
63+
except ValueError:
64+
return None
65+
if 'http' in url.scheme or 'tcp' in url.scheme:
66+
return url.hostname
67+
if 'unix' in url.scheme or 'npipe' in url.scheme:
68+
if inside_container():
69+
ip_address = default_gateway_ip()
70+
if ip_address:
71+
return ip_address
72+
return "localhost"

testcontainers/core/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
import subprocess
34

45
LINUX = "linux"
56
MAC = "mac"
@@ -35,3 +36,21 @@ def inside_container():
3536
https://github.com/docker/docker/blob/a9fa38b1edf30b23cae3eade0be48b3d4b1de14b/daemon/initlayer/setup_unix.go#L25
3637
"""
3738
return os.path.exists('/.dockerenv')
39+
40+
41+
def default_gateway_ip():
42+
"""
43+
Returns gateway IP address of the host that testcontainer process is
44+
running on
45+
46+
https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L27
47+
"""
48+
cmd = ["sh", "-c", "ip route|awk '/default/ { print $3 }'"]
49+
try:
50+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
51+
stderr=subprocess.PIPE)
52+
ip_address = process.communicate()[0]
53+
if ip_address and process.returncode == 0:
54+
return ip_address.decode('utf-8').strip().strip('\n')
55+
except subprocess.SubprocessError:
56+
return None

tests/test_docker_compose.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import pytest
22

33
from testcontainers.compose import DockerCompose
4+
from testcontainers.core.docker_client import DockerClient
45
from testcontainers.core.exceptions import NoSuchPortExposed
5-
from testcontainers.core.utils import inside_container
66

77

88
def test_can_spawn_service_via_compose():
@@ -29,5 +29,5 @@ def test_can_throw_exception_if_no_port_exposed():
2929

3030
def test_compose_wait_for_container_ready():
3131
with DockerCompose("tests") as compose:
32-
host = "host.docker.internal" if inside_container() else "localhost"
33-
compose.wait_for("http://%s:4444/wd/hub" % host)
32+
docker = DockerClient()
33+
compose.wait_for("http://%s:4444/wd/hub" % docker.host())

tests/test_new_docker_api.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from testcontainers import mysql
66

77
from testcontainers.core.generic import GenericContainer
8-
from testcontainers.core.utils import inside_container
98
from importlib import reload
109

1110

@@ -31,10 +30,8 @@ def test_docker_env_variables():
3130
db.with_bind_ports(3306, 32785)
3231
with db:
3332
url = db.get_connection_url()
34-
if inside_container():
35-
assert re.match(r'mysql\+pymysql://demo:test@(\d+\.){3}\d+:3306/custom_db', url)
36-
else:
37-
assert url == 'mysql+pymysql://demo:test@localhost:32785/custom_db'
33+
pattern = r'mysql\+pymysql:\/\/demo:test@[\w,.]+:(3306|32785)\/custom_db'
34+
assert re.match(pattern, url)
3835

3936

4037
def test_docker_kargs():

0 commit comments

Comments
 (0)