Repository: tox-dev/tox-docker

Issue:
support for remote docker hosts
Docker itself could run on a remote machine and docker would use DOCKER_HOST variable to connect to it. Still, based on the fact that no IP address is returned in enviornment variables I suppose that tox-docker would not be able to work in this case.

This is a serious isse as now docker perfectly supports ssh protocol, and user can easily do `DOCKER_HOST=ssh://root@remote`. 

```
File "/Users/ssbarnea/.local/lib/python2.7/site-packages/pluggy/callers.py", line 81, in get_result
_reraise(*ex)  # noqa
File "/Users/ssbarnea/.local/lib/python2.7/site-packages/pluggy/callers.py", line 187, in _multicall
res = hook_impl.function(*args)
File "/Users/ssbarnea/os/tox-docker/tox_docker.py", line 94, in tox_runtest_pre
"Never got answer on port {} from {}".format(containerport, name)
Exception: Never got answer on port 8080/tcp from gerritcodereview/gerrit
tox -e py27  3.33s user 2.75s system 5% cpu 1:44.68 total
```

Comments:

Patch:
diff --git a/README.md b/README.md
index c99702c..efd4f4f 100644
--- a/README.md
+++ b/README.md
@@ -31,17 +31,24 @@ your test suite as it runs, as ordinary environment variables:
         POSTGRES_USER=username
         POSTGRES_DB=dbname
 
-## Port Mapping
+## 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 `<image-basename>_<exposed-port>_<proto>`. For
+variables of the form `<image-basename>_<exposed-port>_<protocol>_PORT`. For
 instance, for the postgresql container, there will be an environment
-variable `POSTGRES_5432_TCP` whose value is the ephemeral port number that
-docker has bound the container's port 5432 to.
+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` whose value is the ephemeral port number that docker has
-bound. NB! Since it's not possible to check whether UDP port is open it's
-just mapping to environment variable without any checks that service up and
-running.
+`TELEGRAF_8092_UDP_PORT` Since it's not possible to check whether UDP port
+is open it's just mapping to environment variable without any checks that
+service up and running.
+
+The host name for each service is also exposed via environment as
+`<image-basename>_HOST`, which is `POSTGRES_HOST` and `TELEGRAF_HOST` for
+the two examples above.
+
+*Deprecation Note:* In older versions of tox-docker, the port was exposed as
+`<image-basename>-<exposed-port>-<protocol>`. This additional environment
+variable is deprecated, but will be supported until tox-docker 2.0.
diff --git a/tox.ini b/tox.ini
index cafee7d..f20bc70 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,18 @@
 [tox]
-envlist = py27
+envlist = integration,registry
 
-[testenv]
+[testenv:integration]
 docker =
     nginx:1.13-alpine
-    telegraf:1.8-alpine
+    ksdn117/tcp-udp-test
 dockerenv =
     ENV_VAR=env-var-value
 deps = pytest
 commands = py.test [] test_integration.py
+
+[testenv:registry]
+docker = docker.io/library/nginx:1.13-alpine
+dockerenv =
+    ENV_VAR=env-var-value
+deps = pytest
+commands = py.test [] test_registry.py
diff --git a/tox_docker.py b/tox_docker.py
index a120a61..8095735 100644
--- a/tox_docker.py
+++ b/tox_docker.py
@@ -7,6 +7,28 @@ from docker.errors import ImageNotFound
 import docker as docker_module
 
 
+def escape_env_var(varname):
+    """
+    Convert a string to a form suitable for use as an environment variable.
+
+    The result will be all uppercase, and will have all invalid characters
+    replaced by an underscore.
+
+    The result will match the following regex: [a-zA-Z_][a-zA-Z0-9_]*
+
+    Example:
+        "my.private.registry/cat/image" will become
+        "MY_PRIVATE_REGISTRY_CAT_IMAGE"
+    """
+    varname = list(varname.upper())
+    if not varname[0].isalpha():
+        varname[0] = '_'
+    for i, c in enumerate(varname):
+        if not c.isalnum() and c != '_':
+            varname[i] = '_'
+    return "".join(varname)
+
+
 def _newaction(venv, message):
     try:
         # tox 3.7 and later
@@ -62,20 +84,33 @@ def tox_runtest_pre(venv):
         conf._docker_containers.append(container)
 
         container.reload()
+        gateway_ip = container.attrs["NetworkSettings"]["Gateway"] or "0.0.0.0"
         for containerport, hostports in container.attrs["NetworkSettings"]["Ports"].items():
-            hostport = None
+
             for spec in hostports:
                 if spec["HostIp"] == "0.0.0.0":
                     hostport = spec["HostPort"]
                     break
-
-            if not hostport:
+            else:
                 continue
 
-            envvar = "{}_{}".format(
-                name.upper(),
-                containerport.replace("/", "_").upper(),
-            )
+            envvar = escape_env_var("{}_HOST".format(
+                name,
+            ))
+            venv.envconfig.setenv[envvar] = gateway_ip
+
+            envvar = escape_env_var("{}_{}_PORT".format(
+                name,
+                containerport,
+            ))
+            venv.envconfig.setenv[envvar] = hostport
+
+            # TODO: remove in 2.0
+            _, proto = containerport.split("/")
+            envvar = escape_env_var("{}_{}".format(
+                name,
+                containerport,
+            ))
             venv.envconfig.setenv[envvar] = hostport
 
             _, proto = containerport.split("/")
@@ -88,7 +123,7 @@ def tox_runtest_pre(venv):
             while (time.time() - start) < 30:
                 try:
                     sock = socket.create_connection(
-                        address=("0.0.0.0", int(hostport)),
+                        address=(gateway_ip, int(hostport)),
                         timeout=0.1,
                     )
                 except socket.error:


Test patch:
diff --git a/test_integration.py b/test_integration.py
index 2c672fe..4a0be70 100644
--- a/test_integration.py
+++ b/test_integration.py
@@ -1,6 +1,10 @@
 import os
 import unittest
-import urllib2
+
+try:
+    from urllib.request import urlopen
+except ImportError:
+    from urllib2 import urlopen
 
 
 class ToxDockerIntegrationTest(unittest.TestCase):
@@ -12,13 +16,30 @@ class ToxDockerIntegrationTest(unittest.TestCase):
 
     def test_it_sets_automatic_env_vars(self):
         # the nginx image we use exposes port 80
+        self.assertIn("NGINX_HOST", os.environ)
         self.assertIn("NGINX_80_TCP", os.environ)
-        # the telegraf image we use exposes UDP port 8092
-        self.assertIn("TELEGRAF_8092_UDP", os.environ)
+        self.assertIn("NGINX_80_TCP_PORT", os.environ)
+        self.assertEqual(
+            os.environ["NGINX_80_TCP_PORT"],
+            os.environ["NGINX_80_TCP"],
+        )
+
+        # the test image we use exposes TCP port 1234 and UDP port 5678
+        self.assertIn("KSDN117_TCP_UDP_TEST_1234_TCP", os.environ)
+        self.assertIn("KSDN117_TCP_UDP_TEST_1234_TCP_PORT", os.environ)
+        self.assertEqual(
+            os.environ["KSDN117_TCP_UDP_TEST_1234_TCP_PORT"],
+            os.environ["KSDN117_TCP_UDP_TEST_1234_TCP"],
+        )
+        self.assertIn("KSDN117_TCP_UDP_TEST_5678_UDP_PORT", os.environ)
+        self.assertEqual(
+            os.environ["KSDN117_TCP_UDP_TEST_5678_UDP_PORT"],
+            os.environ["KSDN117_TCP_UDP_TEST_5678_UDP"],
+        )
 
     def test_it_exposes_the_port(self):
         # the nginx image we use exposes port 80
-        url = "http://127.0.0.1:{port}/".format(port=os.environ["NGINX_80_TCP"])
-        response = urllib2.urlopen(url)
+        url = "http://{host}:{port}/".format(host=os.environ["NGINX_HOST"], port=os.environ["NGINX_80_TCP"])
+        response = urlopen(url)
         self.assertEqual(200, response.getcode())
-        self.assertIn("Thank you for using nginx.", response.read())
+        self.assertIn("Thank you for using nginx.", str(response.read()))
diff --git a/test_registry.py b/test_registry.py
new file mode 100644
index 0000000..4884f36
--- /dev/null
+++ b/test_registry.py
@@ -0,0 +1,18 @@
+import os
+import unittest
+
+from tox_docker import escape_env_var
+
+
+class ToxDockerRegistryTest(unittest.TestCase):
+
+    def test_it_sets_automatic_env_vars(self):
+        # the nginx image we use exposes port 80
+        self.assertIn("DOCKER_IO_LIBRARY_NGINX_HOST", os.environ)
+        self.assertIn("DOCKER_IO_LIBRARY_NGINX_80_TCP", os.environ)
+
+    def test_escape_env_var(self):
+        self.assertEqual(
+            escape_env_var("my.private.registry/cat/image"),
+            "MY_PRIVATE_REGISTRY_CAT_IMAGE",
+        )