Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core)!: add support for tc.host and de-prioritise docker:dind #388

Merged
merged 8 commits into from
Mar 1, 2024
25 changes: 12 additions & 13 deletions core/testcontainers/core/container.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import contextlib
import os
from platform import system
from typing import Optional

Expand Down Expand Up @@ -102,18 +101,18 @@ def get_container_host_ip(self) -> str:
if host == "localnpipe" and system() == "Windows":
return "localhost"

# check testcontainers itself runs inside docker container
if inside_container() and not os.getenv("DOCKER_HOST"):
# If newly spawned container's gateway IP address from the docker
# "bridge" network is equal to detected host address, we should use
# container IP address, otherwise fall back to detected host
# address. Even it's inside container, we need to double check,
# because docker host might be set to docker:dind, usually in CI/CD environment
gateway_ip = self.get_docker_client().gateway_ip(self._container.id)

if gateway_ip == host:
return self.get_docker_client().bridge_ip(self._container.id)
return gateway_ip
# # check testcontainers itself runs inside docker container
# if inside_container() and not os.getenv("DOCKER_HOST") and not host.startswith("http://"):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to do it, to get things working with Testcontainers Cloud within Codespaces (which runs inside a container). I also thought part of this code duplicate what is done in docker_client.py.

# # If newly spawned container's gateway IP address from the docker
# # "bridge" network is equal to detected host address, we should use
# # container IP address, otherwise fall back to detected host
# # address. Even it's inside container, we need to double check,
# # because docker host might be set to docker:dind, usually in CI/CD environment
# gateway_ip = self.get_docker_client().gateway_ip(self._container.id)

# if gateway_ip == host:
# return self.get_docker_client().bridge_ip(self._container.id)
# return gateway_ip
return host

@wait_container_is_ready()
Expand Down
51 changes: 41 additions & 10 deletions core/testcontainers/core/docker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import functools as ft
import os
import urllib
from os.path import exists
from pathlib import Path
from typing import Optional, Union

import docker
Expand All @@ -23,15 +25,8 @@
from .utils import default_gateway_ip, inside_container, setup_logger

LOGGER = setup_logger(__name__)


def _stop_container(container: Container) -> None:
try:
container.stop()
except NotFound:
pass
except Exception as ex:
LOGGER.warning("failed to shut down container %s with image %s: %s", container.id, container.image, ex)
Comment on lines -26 to -34
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private function -> moved to the bottom of this file

TC_FILE = ".testcontainers.properties"
TC_GLOBAL = Path.home() / TC_FILE


class DockerClient:
Expand All @@ -40,7 +35,13 @@ class DockerClient:
"""

def __init__(self, **kwargs) -> None:
self.client = docker.from_env(**kwargs)
docker_host = read_tc_properties().get("tc.host")

if docker_host:
LOGGER.info(f"using host {docker_host}")
self.client = docker.DockerClient(base_url=docker_host)
else:
self.client = docker.from_env(**kwargs)

@ft.wraps(ContainerCollection.run)
def run(
Expand Down Expand Up @@ -123,3 +124,33 @@ def host(self) -> str:
if ip_address:
return ip_address
return "localhost"


@ft.cache
def read_tc_properties() -> dict[str, str]:
"""
Read the .testcontainers.properties for settings. (see the Java implementation for details)
Currently we only support the ~/.testcontainers.properties but may extend to per-project variables later.

:return: the merged properties from the sources.
"""
tc_files = [item for item in [TC_GLOBAL] if exists(item)]
if not tc_files:
return {}
settings = {}

for file in tc_files:
tuples = []
with open(file) as contents:
tuples = [line.split("=") for line in contents.readlines() if "=" in line]
settings = {**settings, **{item[0]: item[1] for item in tuples}}
return settings


def _stop_container(container: Container) -> None:
try:
container.stop()
except NotFound:
pass
except Exception as ex:
LOGGER.warning("failed to shut down container %s with image %s: %s", container.id, container.image, ex)