diff --git a/docker/__init__.py b/docker/__init__.py index 343766d7..1edc0e1c 100644 --- a/docker/__init__.py +++ b/docker/__init__.py @@ -17,4 +17,4 @@ from .version import version __version__ = version __title__ = 'docker-py' -from .client import Client # flake8: noqa +from .client import Client, AutoVersionClient # flake8: noqa diff --git a/docker/client.py b/docker/client.py index aed6258f..79726aaf 100644 --- a/docker/client.py +++ b/docker/client.py @@ -40,7 +40,7 @@ STREAM_HEADER_SIZE_BYTES = 8 class Client(requests.Session): - def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, + def __init__(self, base_url=None, version=None, timeout=DEFAULT_TIMEOUT_SECONDS, tls=False): super(Client, self).__init__() base_url = utils.parse_host(base_url) @@ -50,15 +50,8 @@ class Client(requests.Session): raise errors.TLSParameterError( 'If using TLS, the base_url argument must begin with ' '"https://".') - if not isinstance(version, six.string_types): - raise errors.DockerException( - 'version parameter must be a string. Found {0}'.format( - type(version).__name__ - ) - ) self.base_url = base_url self.timeout = timeout - self._version = version self._auth_configs = auth.load_config() # Use SSLAdapter for the ability to specify SSL version @@ -69,6 +62,34 @@ class Client(requests.Session): else: self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout)) + # version detection needs to be after unix adapter mounting + if version is None: + self._version = DEFAULT_DOCKER_API_VERSION + elif isinstance(version, six.string_types): + if version.lower() == 'auto': + self._version = self._retrieve_server_version() + else: + self._version = version + else: + raise errors.DockerException( + 'Version parameter must be a string or None. Found {0}'.format( + type(version).__name__ + ) + ) + + def _retrieve_server_version(self): + try: + return self.version(api_version=False)["ApiVersion"] + except KeyError: + raise errors.DockerException( + 'Invalid response from docker daemon: key "ApiVersion"' + ' is missing.' + ) + except Exception as e: + raise errors.DockerException( + 'Error while fetching server API version: {0}'.format(e) + ) + def _set_request_timeout(self, kwargs): """Prepare the kwargs for an HTTP request by inserting the timeout parameter, if not already present.""" @@ -84,8 +105,11 @@ class Client(requests.Session): def _delete(self, url, **kwargs): return self.delete(url, **self._set_request_timeout(kwargs)) - def _url(self, path): - return '{0}/v{1}{2}'.format(self.base_url, self._version, path) + def _url(self, path, versioned_api=True): + if versioned_api: + return '{0}/v{1}{2}'.format(self.base_url, self._version, path) + else: + return '{0}{1}'.format(self.base_url, path) def _raise_for_status(self, response, explanation=None): """Raises stored :class:`APIError`, if one occurred.""" @@ -914,8 +938,9 @@ class Client(requests.Session): u = self._url("/containers/{0}/top".format(container)) return self._result(self._get(u), True) - def version(self): - return self._result(self._get(self._url("/version")), True) + def version(self, api_version=True): + url = self._url("/version", versioned_api=api_version) + return self._result(self._get(url), json=True) def unpause(self, container): if isinstance(container, dict): @@ -934,3 +959,13 @@ class Client(requests.Session): if 'StatusCode' in json_: return json_['StatusCode'] return -1 + + +class AutoVersionClient(Client): + def __init__(self, *args, **kwargs): + if 'version' in kwargs and kwargs['version']: + raise errors.DockerException( + 'Can not specify version for AutoVersionClient' + ) + kwargs['version'] = 'auto' + super(AutoVersionClient, self).__init__(*args, **kwargs) diff --git a/docs/api.md b/docs/api.md index 9e550a96..8172b7b6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -12,7 +12,8 @@ c = Client(base_url='unix://var/run/docker.sock') * base_url (str): Refers to the protocol+hostname+port where the Docker server is hosted. -* version (str): The version of the API the client will use +* version (str): The version of the API the client will use. Specify `'auto'` + to use the API version provided by the server. * timeout (int): The HTTP request timeout, in seconds. * tls (bool or [TLSConfig](tls.md#TLSConfig)): Equivalent CLI options: `docker --tls ...` diff --git a/tests/fake_api.py b/tests/fake_api.py index 6d6d80e6..a6a637a7 100644 --- a/tests/fake_api.py +++ b/tests/fake_api.py @@ -30,6 +30,17 @@ FAKE_PATH = '/path' # for clarity and readability +def get_fake_raw_version(): + status_code = 200 + response = { + "ApiVersion": "1.17", + "GitCommit": "fake-commit", + "GoVersion": "go1.3.3", + "Version": "1.5.0" + } + return status_code, response + + def get_fake_version(): status_code = 200 response = {'GoVersion': '1', 'Version': '1.1.1', @@ -347,6 +358,8 @@ def get_fake_stats(): # Maps real api url to fake response callback prefix = 'http+unix://var/run/docker.sock' fake_responses = { + '{0}/version'.format(prefix): + get_fake_raw_version, '{1}/{0}/version'.format(CURRENT_VERSION, prefix): get_fake_version, '{1}/{0}/info'.format(CURRENT_VERSION, prefix): diff --git a/tests/integration_test.py b/tests/integration_test.py index 0b313e29..32c34a1a 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1415,6 +1415,28 @@ class TestLoadJSONConfig(BaseTestCase): self.assertEqual(cfg.get('Auth'), None) +class TestAutoDetectVersion(unittest.TestCase): + def test_client_init(self): + client = docker.Client(version='auto') + client_version = client._version + api_version = client.version(api_version=False)['ApiVersion'] + self.assertEqual(client_version, api_version) + api_version_2 = client.version()['ApiVersion'] + self.assertEqual(client_version, api_version_2) + client.close() + + def test_auto_client(self): + client = docker.AutoVersionClient() + client_version = client._version + api_version = client.version(api_version=False)['ApiVersion'] + self.assertEqual(client_version, api_version) + api_version_2 = client.version()['ApiVersion'] + self.assertEqual(client_version, api_version_2) + client.close() + with self.assertRaises(docker.errors.DockerException): + docker.AutoVersionClient(version='1.11') + + class TestConnectionTimeout(unittest.TestCase): def setUp(self): self.timeout = 0.5 diff --git a/tests/test.py b/tests/test.py index eca6492c..d6fd0130 100644 --- a/tests/test.py +++ b/tests/test.py @@ -130,7 +130,7 @@ class DockerClientTest(Cleanup, unittest.TestCase): if not six.PY3: self.assertEqual( str(e), - 'version parameter must be a string. Found float' + 'Version parameter must be a string or None. Found float' ) ######################### @@ -147,6 +147,19 @@ class DockerClientTest(Cleanup, unittest.TestCase): timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) + def test_retrieve_server_version(self): + client = docker.Client(version="auto") + self.assertTrue(isinstance(client._version, six.string_types)) + self.assertFalse(client._version == "auto") + + def test_auto_retrieve_server_version(self): + try: + version = self.client.retrieve_server_version() + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.assertTrue(isinstance(version, six.string_types)) + def test_info(self): try: self.client.info()