Repository: docker/docker-py

Issue:
docker-py should support same URL schemes as Docker
Fig received https://github.com/orchardup/fig/issues/88 because it blindly passes `DOCKER_HOST` to the `base_url` arg to `Client`.

Maybe `base_url` should support things like `tcp://`, as per https://github.com/dotcloud/docker/blob/2723133a69a292abaa021afff98de127bda57b4e/utils/utils.go#L753-L807 . Alternatively, it could be a separate constructor method.



Discussion:
Already in master: see #140

Sorry, I should have been clearer. docker-py supports _some_ of the values of DOCKER_HOST. Here are some things it doesn't support, for example:
- `tcp://`
- `:4243`
- `localhost`
- `localhost:4243`

It should all work if the logic from Docker's `ParseHost` is reproduced.

I'll hold out on making changes for now because I believe `ParseHost` will undergo some changes very soon in the main project, which could invalidate or change the scope of this request. Leaving it open until then.


Resolving patch:
diff --git a/docker/client.py b/docker/client.py
--- a/docker/client.py
+++ b/docker/client.py
@@ -39,16 +39,9 @@ class Client(requests.Session):
     def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
                  timeout=DEFAULT_TIMEOUT_SECONDS):
         super(Client, self).__init__()
-        if base_url is None:
-            base_url = "http+unix://var/run/docker.sock"
-        if 'unix:///' in base_url:
+        base_url = utils.parse_host(base_url)
+        if 'http+unix:///' in base_url:
             base_url = base_url.replace('unix:/', 'unix:')
-        if base_url.startswith('unix:'):
-            base_url = "http+" + base_url
-        if base_url.startswith('tcp:'):
-            base_url = base_url.replace('tcp:', 'http:')
-        if base_url.endswith('/'):
-            base_url = base_url[:-1]
         self.base_url = base_url
         self._version = version
         self._timeout = timeout
diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py
--- a/docker/utils/__init__.py
+++ b/docker/utils/__init__.py
@@ -1,4 +1,4 @@
 from .utils import (
     compare_version, convert_port_bindings, convert_volume_binds,
-    mkbuildcontext, ping, tar, parse_repository_tag
+    mkbuildcontext, ping, tar, parse_repository_tag, parse_host
 ) # flake8: noqa
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -20,6 +20,11 @@
 import requests
 import six
 
+from .. import errors
+
+DEFAULT_HTTP_HOST = "127.0.0.1"
+DEFAULT_UNIX_SOCKET = "http+unix://var/run/docker.sock"
+
 
 def mkbuildcontext(dockerfile):
     f = tempfile.NamedTemporaryFile()
@@ -139,9 +144,71 @@ def parse_repository_tag(repo):
     column_index = repo.rfind(':')
     if column_index < 0:
         return repo, None
-    tag = repo[column_index+1:]
+    tag = repo[column_index + 1:]
     slash_index = tag.find('/')
     if slash_index < 0:
         return repo[:column_index], tag
 
     return repo, None
+
+
+# Based on utils.go:ParseHost http://tinyurl.com/nkahcfh
+# fd:// protocol unsupported (for obvious reasons)
+# Added support for http and https
+# Protocol translation: tcp -> http, unix -> http+unix
+def parse_host(addr):
+    proto = "http+unix"
+    host = DEFAULT_HTTP_HOST
+    port = None
+    if not addr or addr.strip() == 'unix://':
+        return DEFAULT_UNIX_SOCKET
+
+    addr = addr.strip()
+    if addr.startswith('http://'):
+        addr = addr.replace('http://', 'tcp://')
+    if addr.startswith('http+unix://'):
+        addr = addr.replace('http+unix://', 'unix://')
+
+    if addr == 'tcp://':
+        raise errors.DockerException("Invalid bind address format: %s" % addr)
+    elif addr.startswith('unix://'):
+        addr = addr[7:]
+    elif addr.startswith('tcp://'):
+        proto = "http"
+        addr = addr[6:]
+    elif addr.startswith('https://'):
+        proto = "https"
+        addr = addr[8:]
+    elif addr.startswith('fd://'):
+        raise errors.DockerException("fd protocol is not implemented")
+    else:
+        if "://" in addr:
+            raise errors.DockerException(
+                "Invalid bind address protocol: %s" % addr
+            )
+        proto = "http"
+
+    if proto != "http+unix" and ":" in addr:
+        host_parts = addr.split(':')
+        if len(host_parts) != 2:
+            raise errors.DockerException(
+                "Invalid bind address format: %s" % addr
+            )
+        if host_parts[0]:
+            host = host_parts[0]
+
+        try:
+            port = int(host_parts[1])
+        except Exception:
+            raise errors.DockerException(
+                "Invalid port: %s", addr
+            )
+
+    elif proto in ("http", "https") and ':' not in addr:
+        raise errors.DockerException("Bind address needs a port: %s" % addr)
+    else:
+        host = addr
+
+    if proto == "http+unix":
+        return "%s://%s" % (proto, host)
+    return "%s://%s:%d" % (proto, host, port)


Test patch:
diff --git a/tests/test.py b/tests/test.py
--- a/tests/test.py
+++ b/tests/test.py
@@ -746,14 +746,14 @@ def test_url_compatibility_http_unix_triple_slash(self):
         assert c.base_url == "http+unix://socket"
 
     def test_url_compatibility_http(self):
-        c = docker.Client(base_url="http://hostname")
+        c = docker.Client(base_url="http://hostname:1234")
 
-        assert c.base_url == "http://hostname"
+        assert c.base_url == "http://hostname:1234"
 
     def test_url_compatibility_tcp(self):
-        c = docker.Client(base_url="tcp://hostname")
+        c = docker.Client(base_url="tcp://hostname:1234")
 
-        assert c.base_url == "http://hostname"
+        assert c.base_url == "http://hostname:1234"
 
     def test_logs(self):
         try:
diff --git a/tests/utils_test.py b/tests/utils_test.py
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -1,9 +1,11 @@
 import unittest
 
-from docker.utils import parse_repository_tag
+from docker.errors import DockerException
+from docker.utils import parse_repository_tag, parse_host
 
 
 class UtilsTest(unittest.TestCase):
+    longMessage = True
 
     def test_parse_repository_tag(self):
         self.assertEqual(parse_repository_tag("root"),
@@ -19,6 +21,37 @@ def test_parse_repository_tag(self):
         self.assertEqual(parse_repository_tag("url:5000/repo:tag"),
                          ("url:5000/repo", "tag"))
 
+    def test_parse_host(self):
+        invalid_hosts = [
+            '0.0.0.0',
+            'tcp://',
+            'udp://127.0.0.1',
+            'udp://127.0.0.1:2375',
+        ]
+
+        valid_hosts = {
+            '0.0.0.1:5555': 'http://0.0.0.1:5555',
+            ':6666': 'http://127.0.0.1:6666',
+            'tcp://:7777': 'http://127.0.0.1:7777',
+            'http://:7777': 'http://127.0.0.1:7777',
+            'https://kokia.jp:2375': 'https://kokia.jp:2375',
+            '': 'http+unix://var/run/docker.sock',
+            None: 'http+unix://var/run/docker.sock',
+            'unix:///var/run/docker.sock': 'http+unix:///var/run/docker.sock',
+            'unix://': 'http+unix://var/run/docker.sock'
+        }
+
+        for host in invalid_hosts:
+            try:
+                parsed = parse_host(host)
+                self.fail('Expected to fail but success: %s -> %s' % (
+                    host, parsed
+                ))
+            except DockerException:
+                pass
+
+        for host, expected in valid_hosts.items():
+            self.assertEqual(parse_host(host), expected, msg=host)
 
 if __name__ == '__main__':
     unittest.main()