diff --git a/README.rst b/README.rst index f78c171..d85afe4 100644 --- a/README.rst +++ b/README.rst @@ -40,12 +40,23 @@ your test suite as it runs, as ordinary environment variables:: Host and Port Mapping --------------------- -tox-docker runs docker with the "publish all ports" option. Any port the -container exposes will be made available to your test suite via environment -variables of the form ``___PORT``. -For instance, for the PostgreSQL container, there will be an environment -variable ``POSTGRES_5432_TCP_PORT`` whose value is the ephemeral port number -that docker has bound the container's port 5432 to. +By default, tox-docker runs the container with the "publish all ports" option. +You may also specify port publishing in ``tox.ini``, in a new section like:: + + [docker:redis:5.0-alpine] + ports = 5432:5432/tcp + +The image name -- everything after the ``docker:`` in the section header -- +must *exactly* match the image name used in your testenv's ``docker`` setting. +Published ports are separated by a newline and are in the format +``:/``. + +Any port the container exposes will be made available to your test suite via +environment variables of the form +``___PORT``. For instance, for the +PostgreSQL container, there will be an environment variable +``POSTGRES_5432_TCP_PORT`` whose value is the ephemeral port number that docker +has bound the container's port 5432 to. Likewise, exposed UDP ports will have environment variables like ``TELEGRAF_8092_UDP_PORT`` Since it's not possible to check whether UDP port diff --git a/test_ports.py b/test_ports.py new file mode 100644 index 0000000..095bfa1 --- /dev/null +++ b/test_ports.py @@ -0,0 +1,18 @@ +import unittest + +import docker + + +class ToxDockerPortTest(unittest.TestCase): + + def test_it_exposes_only_specified_port(self): + client = docker.from_env(version="auto") + mysql_container = None + for container in client.containers.list() + if "mysql" in containers.attrs["Config"]["Image"]: + mysql_container = container + break + + self.assertIsNotNone(mysql_container, "could not find mysql container") + self.assertIsNotNone(mysql_container.attrs["NetworkSettings"]["Ports"].get("3306/tcp")) + self.assertIsNone(mysql_container.attrs["NetworkSettings"]["Ports"].get("33060/tcp")) diff --git a/tox.ini b/tox.ini index da12773..0404e05 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = integration,registry,healthcheck-builtin,healthcheck-custom +envlist = integration,registry,healthcheck-builtin,healthcheck-custom,ports [testenv] # commands_pre/_post only work in tox 3.4+, but at least in some @@ -46,3 +46,11 @@ healthcheck_interval = 1 healthcheck_timeout = 1 healthcheck_retries = 30 healthcheck_start_period = 0.5 + +[testenv:ports] +docker = mysql:5.7 +deps = pytest +commands = py.test [] test_ports.py + +[docker:mysql:5.7] +ports = 3306:3306/tcp diff --git a/tox_docker.py b/tox_docker.py index 09e6c93..ed8f915 100644 --- a/tox_docker.py +++ b/tox_docker.py @@ -100,11 +100,25 @@ def getint(reader, key): "healthcheck_timeout": gettime(reader, "healthcheck_timeout"), "healthcheck_retries": getint(reader, "healthcheck_retries"), "healthcheck_start_period": gettime(reader, "healthcheck_start_period"), + "ports": reader.getlist("ports"), } config._docker_image_configs = image_configs +def _validate_port(port_line): + host_port, _, container_port_proto = port_line.partition(":") + host_port = int(host_port) + + container_port, _, protocol = container_port_proto.partition("/") + container_port = int(container_port) + + if protocol.lower() not in ("tcp", "udp"): + raise ValueError("protocol is not tcp or udp") + + return (host_port, container_port_proto) + + @hookimpl def tox_runtest_pre(venv): envconfig = venv.envconfig @@ -163,12 +177,20 @@ def tox_runtest_pre(venv): else: healthcheck = None + ports = {} + for port_mapping in image_config.get("ports", []): + host_port, container_port_proto = _validate_port(port_mapping) + existing_ports = set(ports.get(container_port_proto, [])) + existing_ports.add(host_port) + ports[container_port_proto] = list(existing_ports) + action.setactivity("docker", "run {!r}".format(image)) with action: container = docker.containers.run( image, detach=True, - publish_all_ports=True, + publish_all_ports=len(ports) == 0, + ports=ports, environment=environment, healthcheck=healthcheck, ) @@ -195,6 +217,10 @@ def tox_runtest_pre(venv): name, _, tag = image.partition(":") gateway_ip = _get_gateway_ip(container) for containerport, hostports in container.attrs["NetworkSettings"]["Ports"].items(): + if hostports is None: + # The port is exposed by the container, but not published. + continue + for spec in hostports: if spec["HostIp"] == "0.0.0.0": hostport = spec["HostPort"]