Skip to content

Commit b10d916

Browse files
deeninetyonedee42alexanderankintotallyzen
authoredMar 20, 2024··
fix(core): DinD issues #141, #329 (#368)
Fix #141 - find IP from custom network if the container is not using the default network Close #329 - This seems fixed in the underlying docker libraries. Improve support for Docker in Docker running on a custom network, by attempting to find the right custom network and use it for new containers. This adds support for using testcontainers-python running the GitHub Actions Runner Controller to run self-hosted actions runners on prem, when you run your workflows in containers. --------- Co-authored-by: Dee Moore <dee42moore@gmail.com> Co-authored-by: David Ankin <daveankin@gmail.com> Co-authored-by: Balint Bartha <39852431+totallyzen@users.noreply.github.com>
1 parent d61af38 commit b10d916

File tree

2 files changed

+107
-10
lines changed

2 files changed

+107
-10
lines changed
 

‎core/testcontainers/core/docker_client.py

+51-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
1313
import functools as ft
14+
import ipaddress
1415
import os
1516
import urllib
17+
import urllib.parse
1618
from os.path import exists
1719
from pathlib import Path
1820
from typing import Optional, Union
@@ -34,7 +36,7 @@ class DockerClient:
3436
"""
3537

3638
def __init__(self, **kwargs) -> None:
37-
docker_host = read_tc_properties().get("tc.host")
39+
docker_host = get_docker_host()
3840

3941
if docker_host:
4042
LOGGER.info(f"using host {docker_host}")
@@ -57,6 +59,12 @@ def run(
5759
remove: bool = False,
5860
**kwargs,
5961
) -> Container:
62+
# If the user has specified a network, we'll assume the user knows best
63+
if "network" not in kwargs and not get_docker_host():
64+
# Otherwise we'll try to find the docker host for dind usage.
65+
host_network = self.find_host_network()
66+
if host_network:
67+
kwargs["network"] = host_network
6068
container = self.client.containers.run(
6169
image,
6270
command=command,
@@ -71,6 +79,30 @@ def run(
7179
)
7280
return container
7381

82+
def find_host_network(self) -> Optional[str]:
83+
"""
84+
Try to find the docker host network.
85+
86+
:return: The network name if found, None if not set.
87+
"""
88+
# If we're docker in docker running on a custom network, we need to inherit the
89+
# network settings, so we can access the resulting container.
90+
try:
91+
docker_host = ipaddress.IPv4Address(self.host())
92+
# See if we can find the host on our networks
93+
for network in self.client.networks.list(filters={"type": "custom"}):
94+
if "IPAM" in network.attrs:
95+
for config in network.attrs["IPAM"]["Config"]:
96+
try:
97+
subnet = ipaddress.IPv4Network(config["Subnet"])
98+
except ipaddress.AddressValueError:
99+
continue
100+
if docker_host in subnet:
101+
return network.name
102+
except ipaddress.AddressValueError:
103+
pass
104+
return None
105+
74106
def port(self, container_id: str, port: int) -> int:
75107
"""
76108
Lookup the public-facing port that is NAT-ed to :code:`port`.
@@ -94,14 +126,26 @@ def bridge_ip(self, container_id: str) -> str:
94126
Get the bridge ip address for a container.
95127
"""
96128
container = self.get_container(container_id)
97-
return container["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]
129+
network_name = self.network_name(container_id)
130+
return container["NetworkSettings"]["Networks"][network_name]["IPAddress"]
131+
132+
def network_name(self, container_id: str) -> str:
133+
"""
134+
Get the name of the network this container runs on
135+
"""
136+
container = self.get_container(container_id)
137+
name = container["HostConfig"]["NetworkMode"]
138+
if name == "default":
139+
return "bridge"
140+
return name
98141

99142
def gateway_ip(self, container_id: str) -> str:
100143
"""
101144
Get the gateway ip address for a container.
102145
"""
103146
container = self.get_container(container_id)
104-
return container["NetworkSettings"]["Networks"]["bridge"]["Gateway"]
147+
network_name = self.network_name(container_id)
148+
return container["NetworkSettings"]["Networks"][network_name]["Gateway"]
105149

106150
def host(self) -> str:
107151
"""
@@ -145,3 +189,7 @@ def read_tc_properties() -> dict[str, str]:
145189
tuples = [line.split("=") for line in contents.readlines() if "=" in line]
146190
settings = {**settings, **{item[0].strip(): item[1].strip() for item in tuples}}
147191
return settings
192+
193+
194+
def get_docker_host() -> Optional[str]:
195+
return read_tc_properties().get("tc.host") or os.getenv("DOCKER_HOST")

‎core/tests/test_docker_in_docker.py

+56-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
1-
import pytest
2-
1+
import time
2+
import socket
33
from testcontainers.core.container import DockerContainer
44
from testcontainers.core.docker_client import DockerClient
55
from testcontainers.core.waiting_utils import wait_for_logs
66

77

8-
@pytest.mark.xfail(reason="https://github.com/docker/docker-py/issues/2717")
8+
def _wait_for_dind_return_ip(client, dind):
9+
# get ip address for DOCKER_HOST
10+
# avoiding DockerContainer class here to prevent code changes affecting the test
11+
docker_host_ip = client.bridge_ip(dind.id)
12+
# Wait for startup
13+
timeout = 10
14+
start_wait = time.perf_counter()
15+
while True:
16+
try:
17+
with socket.create_connection((docker_host_ip, 2375), timeout=timeout):
18+
break
19+
except ConnectionRefusedError:
20+
if time.perf_counter() - start_wait > timeout:
21+
raise RuntimeError("Docker in docker took longer than 10 seconds to start")
22+
time.sleep(0.01)
23+
return docker_host_ip
24+
25+
926
def test_wait_for_logs_docker_in_docker():
1027
# real dind isn't possible (AFAIK) in CI
1128
# forwarding the socket to a container port is at least somewhat the same
@@ -18,21 +35,53 @@ def test_wait_for_logs_docker_in_docker():
1835
)
1936

2037
not_really_dind.start()
38+
docker_host_ip = _wait_for_dind_return_ip(client, not_really_dind)
39+
docker_host = f"tcp://{docker_host_ip}:2375"
2140

22-
# get ip address for DOCKER_HOST
23-
# avoiding DockerContainer class here to prevent code changes affecting the test
24-
specs = client.get_container(not_really_dind.id)
25-
docker_host_ip = specs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]
41+
with DockerContainer(
42+
image="hello-world",
43+
docker_client_kw={"environment": {"DOCKER_HOST": docker_host, "DOCKER_CERT_PATH": "", "DOCKER_TLS_VERIFY": ""}},
44+
) as container:
45+
assert container.get_container_host_ip() == docker_host_ip
46+
wait_for_logs(container, "Hello from Docker!")
47+
stdout, stderr = container.get_logs()
48+
assert stdout, "There should be something on stdout"
49+
50+
not_really_dind.stop()
51+
not_really_dind.remove()
52+
53+
54+
def test_dind_inherits_network():
55+
client = DockerClient()
56+
try:
57+
custom_network = client.client.networks.create("custom_network", driver="bridge", check_duplicate=True)
58+
except Exception:
59+
custom_network = client.client.networks.list(names=["custom_network"])[0]
60+
not_really_dind = client.run(
61+
image="alpine/socat",
62+
command="tcp-listen:2375,fork,reuseaddr unix-connect:/var/run/docker.sock",
63+
volumes={"/var/run/docker.sock": {"bind": "/var/run/docker.sock"}},
64+
detach=True,
65+
)
66+
67+
not_really_dind.start()
68+
69+
docker_host_ip = _wait_for_dind_return_ip(client, not_really_dind)
2670
docker_host = f"tcp://{docker_host_ip}:2375"
2771

2872
with DockerContainer(
2973
image="hello-world",
3074
docker_client_kw={"environment": {"DOCKER_HOST": docker_host, "DOCKER_CERT_PATH": "", "DOCKER_TLS_VERIFY": ""}},
3175
) as container:
3276
assert container.get_container_host_ip() == docker_host_ip
77+
# Check the gateways are the same, so they can talk to each other
78+
assert container.get_docker_client().gateway_ip(container.get_wrapped_container().id) == client.gateway_ip(
79+
not_really_dind.id
80+
)
3381
wait_for_logs(container, "Hello from Docker!")
3482
stdout, stderr = container.get_logs()
3583
assert stdout, "There should be something on stdout"
3684

3785
not_really_dind.stop()
3886
not_really_dind.remove()
87+
custom_network.remove()

0 commit comments

Comments
 (0)
Please sign in to comment.