Rework Unix connection code so that 'chunked' transfers work

This allows streaming a system to the 'import' command, rather than
having to read the whole thing into memory before sending it.

Previously both the UnixAdapter and the docker.Client objects would
track the 'base URL' of the Docker daemon (socket path in the case of
local Unix-domain socket connections). The Client object would construct
URLs which contained the path to the socket with the path of the Docker
API call appended. The UnixHTTPConnection instance would then remove the
known socket path from the URL.

This relied on all calls going through the HTTPConnection.request()
function, where the URL could be rewritten. In the case of 'chunked'
HTTP POST requests this doesn't happen, so such calls would request
a path still including the socket path and would receive a 404 error.

The client now constructs URLs containing just the path of the desired API
endpoint, and expects the Unix socket transport to know the path to the
Docker daemon's socket.
This commit is contained in:
Sam Thursfield 2014-09-17 14:38:14 +00:00
parent 6a63cb2c96
commit 42b91885a2
4 changed files with 34 additions and 31 deletions

View File

@ -42,31 +42,35 @@ class Client(requests.Session):
def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
timeout=DEFAULT_TIMEOUT_SECONDS, tls=False): timeout=DEFAULT_TIMEOUT_SECONDS, tls=False):
super(Client, self).__init__() super(Client, self).__init__()
base_url = utils.parse_host(base_url)
if 'http+unix:///' in base_url:
base_url = base_url.replace('unix:/', 'unix:')
if tls and not base_url.startswith('https://'): if tls and not base_url.startswith('https://'):
raise errors.TLSParameterError( raise errors.TLSParameterError(
'If using TLS, the base_url argument must begin with ' 'If using TLS, the base_url argument must begin with '
'"https://".') '"https://".')
if not isinstance(version, six.string_types): if not isinstance(version, six.string_types):
raise errors.DockerException( raise errors.DockerException(
'version parameter must be a string. Found {0}'.format( 'version parameter must be a string. Found {0}'.format(
type(version).__name__ type(version).__name__
) )
) )
self.base_url = base_url
self._version = version self._version = version
self._timeout = timeout self._timeout = timeout
self._auth_configs = auth.load_config() self._auth_configs = auth.load_config()
# Use SSLAdapter for the ability to specify SSL version base_url = utils.parse_host(base_url)
if isinstance(tls, TLSConfig): if base_url.startswith('http+unix://'):
tls.configure_client(self) unix_socket_adapter = unixconn.UnixAdapter(base_url, timeout)
elif tls: self.mount('http+docker://', unix_socket_adapter)
self.mount('https://', ssladapter.SSLAdapter()) self.base_url = 'http+docker://localunixsocket'
else: else:
self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout)) # Use SSLAdapter for the ability to specify SSL version
if isinstance(tls, TLSConfig):
tls.configure_client(self)
elif tls:
self.mount('https://', ssladapter.SSLAdapter())
self.base_url = base_url
def _set_request_timeout(self, kwargs): def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout """Prepare the kwargs for an HTTP request by inserting the timeout

View File

@ -41,17 +41,9 @@ class UnixHTTPConnection(httplib.HTTPConnection, object):
def connect(self): def connect(self):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(self.timeout) sock.settimeout(self.timeout)
sock.connect(self.base_url.replace("http+unix:/", "")) sock.connect(self.unix_socket)
self.sock = sock self.sock = sock
def _extract_path(self, url):
# remove the base_url entirely..
return url.replace(self.base_url, "")
def request(self, method, url, **kwargs):
url = self._extract_path(self.unix_socket)
super(UnixHTTPConnection, self).request(method, url, **kwargs)
class UnixHTTPConnectionPool(connectionpool.HTTPConnectionPool): class UnixHTTPConnectionPool(connectionpool.HTTPConnectionPool):
def __init__(self, base_url, socket_path, timeout=60): def __init__(self, base_url, socket_path, timeout=60):
@ -67,23 +59,26 @@ class UnixHTTPConnectionPool(connectionpool.HTTPConnectionPool):
class UnixAdapter(requests.adapters.HTTPAdapter): class UnixAdapter(requests.adapters.HTTPAdapter):
def __init__(self, base_url, timeout=60): def __init__(self, socket_url, timeout=60):
self.base_url = base_url socket_path = socket_url.replace('http+unix://', '')
if not socket_path.startswith('/'):
socket_path = '/' + socket_path
self.socket_path = socket_path
self.timeout = timeout self.timeout = timeout
self.pools = RecentlyUsedContainer(10, self.pools = RecentlyUsedContainer(10,
dispose_func=lambda p: p.close()) dispose_func=lambda p: p.close())
super(UnixAdapter, self).__init__() super(UnixAdapter, self).__init__()
def get_connection(self, socket_path, proxies=None): def get_connection(self, url, proxies=None):
with self.pools.lock: with self.pools.lock:
pool = self.pools.get(socket_path) pool = self.pools.get(url)
if pool: if pool:
return pool return pool
pool = UnixHTTPConnectionPool(self.base_url, pool = UnixHTTPConnectionPool(url,
socket_path, self.socket_path,
self.timeout) self.timeout)
self.pools[socket_path] = pool self.pools[url] = pool
return pool return pool

View File

@ -324,7 +324,7 @@ def post_fake_tag_image():
# Maps real api url to fake response callback # Maps real api url to fake response callback
prefix = 'http+unix://var/run/docker.sock' prefix = 'http+docker://localunixsocket'
fake_responses = { fake_responses = {
'{1}/{0}/version'.format(CURRENT_VERSION, prefix): '{1}/{0}/version'.format(CURRENT_VERSION, prefix):
get_fake_version, get_fake_version,

View File

@ -71,7 +71,7 @@ def fake_resp(url, data=None, **kwargs):
fake_request = mock.Mock(side_effect=fake_resp) fake_request = mock.Mock(side_effect=fake_resp)
url_prefix = 'http+unix://var/run/docker.sock/v{0}/'.format( url_prefix = 'http+docker://localunixsocket/v{0}/'.format(
docker.client.DEFAULT_DOCKER_API_VERSION) docker.client.DEFAULT_DOCKER_API_VERSION)
@ -1267,20 +1267,24 @@ class DockerClientTest(Cleanup, unittest.TestCase):
timeout=None timeout=None
) )
def _socket_path_for_client_session(self, client):
socket_adapter = client.get_adapter('http+docker://')
return socket_adapter.socket_path
def test_url_compatibility_unix(self): def test_url_compatibility_unix(self):
c = docker.Client(base_url="unix://socket") c = docker.Client(base_url="unix://socket")
assert c.base_url == "http+unix://socket" assert self._socket_path_for_client_session(c) == '/socket'
def test_url_compatibility_unix_triple_slash(self): def test_url_compatibility_unix_triple_slash(self):
c = docker.Client(base_url="unix:///socket") c = docker.Client(base_url="unix:///socket")
assert c.base_url == "http+unix://socket" assert self._socket_path_for_client_session(c) == '/socket'
def test_url_compatibility_http_unix_triple_slash(self): def test_url_compatibility_http_unix_triple_slash(self):
c = docker.Client(base_url="http+unix:///socket") c = docker.Client(base_url="http+unix:///socket")
assert c.base_url == "http+unix://socket" assert self._socket_path_for_client_session(c) == '/socket'
def test_url_compatibility_http(self): def test_url_compatibility_http(self):
c = docker.Client(base_url="http://hostname:1234") c = docker.Client(base_url="http://hostname:1234")