diff --git a/docker/client.py b/docker/client.py index 0863bdb3a..1aa9ed6db 100644 --- a/docker/client.py +++ b/docker/client.py @@ -115,8 +115,8 @@ def _container_config(self, image, command, hostname=None, user=None, command = shlex.split(str(command)) if isinstance(environment, dict): environment = [ - (six.text_type('{0}={1}').format(k, v) - for k, v in environment.items()) + six.text_type('{0}={1}').format(k, v) + for k, v in six.iteritems(environment) ] if isinstance(mem_limit, six.string_types): @@ -911,63 +911,7 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None, restart_policy=None, cap_add=None, cap_drop=None, devices=None, extra_hosts=None): - start_config = {} - - if isinstance(container, dict): - container = container.get('Id') - - if isinstance(lxc_conf, dict): - formatted = [] - for k, v in six.iteritems(lxc_conf): - formatted.append({'Key': k, 'Value': str(v)}) - lxc_conf = formatted - - if lxc_conf: - start_config['LxcConf'] = lxc_conf - - if binds: - start_config['Binds'] = utils.convert_volume_binds(binds) - - if port_bindings: - start_config['PortBindings'] = utils.convert_port_bindings( - port_bindings - ) - - if publish_all_ports: - start_config['PublishAllPorts'] = publish_all_ports - - if links: - if isinstance(links, dict): - links = six.iteritems(links) - - formatted_links = [ - '{0}:{1}'.format(k, v) for k, v in sorted(links) - ] - - start_config['Links'] = formatted_links - - if extra_hosts: - if isinstance(extra_hosts, dict): - extra_hosts = six.iteritems(extra_hosts) - - formatted_extra_hosts = [ - '{0}:{1}'.format(k, v) for k, v in sorted(extra_hosts) - ] - - start_config['ExtraHosts'] = formatted_extra_hosts - - if privileged: - start_config['Privileged'] = privileged - - if utils.compare_version('1.10', self._version) >= 0: - if dns is not None: - start_config['Dns'] = dns - if volumes_from is not None: - if isinstance(volumes_from, six.string_types): - volumes_from = volumes_from.split(',') - start_config['VolumesFrom'] = volumes_from - else: - + if utils.compare_version('1.10', self._version) < 0: if dns is not None: raise errors.APIError( 'dns is only supported for API version >= 1.10' @@ -976,23 +920,18 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None, raise errors.APIError( 'volumes_from is only supported for API version >= 1.10' ) - if dns_search: - start_config['DnsSearch'] = dns_search - - if network_mode: - start_config['NetworkMode'] = network_mode - - if restart_policy: - start_config['RestartPolicy'] = restart_policy - - if cap_add: - start_config['CapAdd'] = cap_add - if cap_drop: - start_config['CapDrop'] = cap_drop + start_config = utils.create_host_config( + binds=binds, port_bindings=port_bindings, lxc_conf=lxc_conf, + publish_all_ports=publish_all_ports, links=links, dns=dns, + privileged=privileged, dns_search=dns_search, cap_add=cap_add, + cap_drop=cap_drop, volumes_from=volumes_from, devices=devices, + network_mode=network_mode, restart_policy=restart_policy, + extra_hosts=extra_hosts + ) - if devices: - start_config['Devices'] = utils.parse_devices(devices) + if isinstance(container, dict): + container = container.get('Id') url = self._url("/containers/{0}/start".format(container)) if not start_config: diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 98d59c45e..a554952e4 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -300,12 +300,16 @@ def create_host_config( binds=None, port_bindings=None, lxc_conf=None, publish_all_ports=False, links=None, privileged=False, dns=None, dns_search=None, volumes_from=None, network_mode=None, - restart_policy=None, cap_add=None, cap_drop=None, devices=None + restart_policy=None, cap_add=None, cap_drop=None, devices=None, + extra_hosts=None ): - host_config = { - 'Privileged': privileged, - 'PublishAllPorts': publish_all_ports, - } + host_config = {} + + if privileged: + host_config['Privileged'] = privileged + + if publish_all_ports: + host_config['PublishAllPorts'] = publish_all_ports if dns_search: host_config['DnsSearch'] = dns_search @@ -341,7 +345,14 @@ def create_host_config( port_bindings ) - host_config['PublishAllPorts'] = publish_all_ports + if extra_hosts: + if isinstance(extra_hosts, dict): + extra_hosts = [ + '{0}:{1}'.format(k, v) + for k, v in sorted(six.iteritems(extra_hosts)) + ] + + host_config['ExtraHosts'] = extra_hosts if links: if isinstance(links, dict): @@ -358,6 +369,8 @@ def create_host_config( for k, v in six.iteritems(lxc_conf): formatted.append({'Key': k, 'Value': str(v)}) lxc_conf = formatted - host_config['LxcConf'] = lxc_conf + + if lxc_conf: + host_config['LxcConf'] = lxc_conf return host_config diff --git a/docker/version.py b/docker/version.py index ba6bd5d36..7a0c9fe2d 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1 +1 @@ -version = "0.6.1-dev" +version = "0.7.0" diff --git a/docs/api.md b/docs/api.md index 64d9b3de3..a4e7dd135 100644 --- a/docs/api.md +++ b/docs/api.md @@ -54,7 +54,7 @@ correct value (e.g `gzip`). * nocache (bool): Don't use the cache when set to `True` * rm (bool): Remove intermediate containers * stream (bool): Return a blocking generator you can iterate over to retrieve -build output as it happens + build output as it happens * timeout (int): HTTP timeout * custom_context (bool): Optional if using `fileobj` * encoding (str): The encoding for a stream. Set to `gzip` for compressing @@ -385,6 +385,8 @@ Nearly identical to the `docker login` command, but non-interactive. * email (str): The email for the registry account * registry (str): URL to the registry. Ex:`https://index.docker.io/v1/` * reauth (bool): Whether refresh existing authentication on the docker server. +* dockercfg_path (str): Use a custom path for the .dockercfg file + (default `$HOME/.dockercfg`) **Returns** (dict): The response from the login request @@ -636,6 +638,7 @@ from. Optionally a single string joining container id's with commas `['on-failure', 'always']` * cap_add (list of str): See note above * cap_drop (list of str): See note above +* extra_hosts (dict): custom host-to-IP mappings (host:ip) ```python >>> from docker import Client diff --git a/docs/change_log.md b/docs/change_log.md index e3d5a2256..91c99d3b6 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -1,6 +1,43 @@ Change Log ========== +0.7.0 +----- + +### Breaking changes + +* Passing `dns` or `volumes_from` in `Client.start` with API version < 1.10 + will now raise an exception (previously only triggered a warning) + +### Features + +* Added support for `host_config` in `Client.create_container` +* Added utility method `docker.utils.create_host_config` to help build a + proper `HostConfig` dictionary. +* Added support for the `pull` parameter in `Client.build` +* Added support for the `forcerm` parameter in `Client.build` +* Added support for `extra_hosts` in `Client.start` +* Added support for a custom `timeout` in `Client.wait` +* Added support for custom `.dockercfg` loading in `Client.login` + (`dockercfg_path` argument) + +### Bugfixes + +* Fixed a bug where some output wouldn't be streamed properly in streaming + chunked responses +* Fixed a bug where the `devices` param didn't recognize the proper delimiter +* `Client.login` now properly expands the `registry` URL if provided. +* Fixed a bug where unicode characters in passed for `environment` in + `create_container` would break. + +### Miscellaneous + +* Several unit tests and integration tests improvements. +* `Client` constructor now enforces passing the `version` parameter as a + string. +* Build context files are now ordered by filename when creating the archive + (for consistency with docker mainline behavior) + 0.6.0 ----- * **This version introduces breaking changes!** diff --git a/docs/hostconfig.md b/docs/hostconfig.md index 07a75b178..2d7f10f78 100644 --- a/docs/hostconfig.md +++ b/docs/hostconfig.md @@ -81,6 +81,7 @@ for example: * restart_policy (dict): "Name" param must be one of `['on-failure', 'always']` * cap_add (list of str): Add kernel capabilities * cap_drop (list of str): Drop kernel capabilities +* extra_hosts (dict): custom host-to-IP mappings (host:ip) **Returns** (dict) HostConfig dictionary diff --git a/tests/integration_test.py b/tests/integration_test.py index 92ec8826f..801dd82ee 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -35,6 +35,7 @@ warnings.simplefilter('error') create_host_config = docker.utils.create_host_config +compare_version = docker.utils.compare_version class BaseTestCase(unittest.TestCase): @@ -43,6 +44,9 @@ class BaseTestCase(unittest.TestCase): tmp_folders = [] def setUp(self): + if six.PY2: + self.assertRegex = self.assertRegexpMatches + self.assertCountEqual = self.assertItemsEqual self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=5) self.tmp_imgs = [] self.tmp_containers = [] @@ -62,6 +66,7 @@ def tearDown(self): pass for folder in self.tmp_folders: shutil.rmtree(folder) + self.client.close() ######################### # INFORMATION TESTS # @@ -134,7 +139,7 @@ def runTest(self): self.assertIn('Command', retrieved) self.assertEqual(retrieved['Command'], six.text_type('true')) self.assertIn('Image', retrieved) - self.assertRegexpMatches(retrieved['Image'], r'busybox:.*') + self.assertRegex(retrieved['Image'], r'busybox:.*') self.assertIn('Status', retrieved) ##################### @@ -178,6 +183,8 @@ def runTest(self): logs = self.client.logs(container_id) os.unlink(shared_file) + if six.PY3: + logs = logs.decode('utf-8') self.assertIn(filename, logs) @@ -208,6 +215,8 @@ def runTest(self): logs = self.client.logs(container_id) os.unlink(shared_file) + if six.PY3: + logs = logs.decode('utf-8') self.assertIn(filename, logs) @@ -517,12 +526,12 @@ class TestPort(BaseTestCase): def runTest(self): port_bindings = { - 1111: ('127.0.0.1', '4567'), - 2222: ('127.0.0.1', '4568') + '1111': ('127.0.0.1', '4567'), + '2222': ('127.0.0.1', '4568') } container = self.client.create_container( - 'busybox', ['sleep', '60'], ports=port_bindings.keys(), + 'busybox', ['sleep', '60'], ports=list(port_bindings.keys()), host_config=create_host_config(port_bindings=port_bindings) ) id = container['Id'] @@ -546,12 +555,12 @@ class TestStartWithPortBindings(BaseTestCase): def runTest(self): port_bindings = { - 1111: ('127.0.0.1', '4567'), - 2222: ('127.0.0.1', '4568') + '1111': ('127.0.0.1', '4567'), + '2222': ('127.0.0.1', '4568') } container = self.client.create_container( - 'busybox', ['sleep', '60'], ports=port_bindings.keys() + 'busybox', ['sleep', '60'], ports=list(port_bindings.keys()) ) id = container['Id'] @@ -668,7 +677,7 @@ def runTest(self): self.client.start(container3_id) info = self.client.inspect_container(res2['Id']) - self.assertItemsEqual(info['HostConfig']['VolumesFrom'], vol_names) + self.assertCountEqual(info['HostConfig']['VolumesFrom'], vol_names) class TestCreateContainerWithLinks(BaseTestCase): @@ -713,6 +722,8 @@ def runTest(self): self.assertEqual(self.client.wait(container3_id), 0) logs = self.client.logs(container3_id) + if six.PY3: + logs = logs.decode('utf-8') self.assertIn('{0}_NAME='.format(link_env_prefix1), logs) self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix1), logs) self.assertIn('{0}_NAME='.format(link_env_prefix2), logs) @@ -749,7 +760,7 @@ def runTest(self): self.client.start(container3_id, volumes_from=vol_names) info = self.client.inspect_container(res2['Id']) - self.assertItemsEqual(info['HostConfig']['VolumesFrom'], vol_names) + self.assertCountEqual(info['HostConfig']['VolumesFrom'], vol_names) class TestStartContainerWithLinks(BaseTestCase): @@ -793,6 +804,8 @@ def runTest(self): self.assertEqual(self.client.wait(container3_id), 0) logs = self.client.logs(container3_id) + if six.PY3: + logs = logs.decode('utf-8') self.assertIn('{0}_NAME='.format(link_env_prefix1), logs) self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix1), logs) self.assertIn('{0}_NAME='.format(link_env_prefix2), logs) @@ -939,6 +952,7 @@ def runTest(self): class TestPull(BaseTestCase): def runTest(self): + self.client.close() self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=10) try: self.client.remove_image('busybox') @@ -947,7 +961,7 @@ def runTest(self): res = self.client.pull('busybox') self.assertEqual(type(res), six.text_type) self.assertGreaterEqual( - self.client.images('busybox'), 1 + len(self.client.images('busybox')), 1 ) img_info = self.client.inspect_image('busybox') self.assertIn('Id', img_info) @@ -955,6 +969,7 @@ def runTest(self): class TestPullStream(BaseTestCase): def runTest(self): + self.client.close() self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=10) try: self.client.remove_image('busybox') @@ -962,9 +977,11 @@ def runTest(self): pass stream = self.client.pull('busybox', stream=True) for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') json.loads(chunk) # ensure chunk is a single, valid JSON blob self.assertGreaterEqual( - self.client.images('busybox'), 1 + len(self.client.images('busybox')), 1 ) img_info = self.client.inspect_image('busybox') self.assertIn('Id', img_info) @@ -1013,7 +1030,7 @@ def runTest(self): class TestBuild(BaseTestCase): def runTest(self): - if self.client._version >= 1.8: + if compare_version(self.client._version, '1.8') < 0: return script = io.BytesIO('\n'.join([ 'FROM busybox', @@ -1055,6 +1072,8 @@ def runTest(self): stream = self.client.build(fileobj=script, stream=True) logs = '' for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') json.loads(chunk) # ensure chunk is a single, valid JSON blob logs += chunk self.assertNotEqual(logs, '') @@ -1075,13 +1094,15 @@ def runTest(self): stream = self.client.build(fileobj=script, stream=True) logs = '' for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') logs += chunk self.assertNotEqual(logs, '') class TestBuildWithAuth(BaseTestCase): def runTest(self): - if self.client._version < 1.9: + if compare_version(self.client._version, '1.9') >= 0: return k = 'K4104GON3P4Q6ZUJFZRRC2ZQTBJ5YT0UMZD7TGT7ZVIR8Y05FAH2TJQI6Y90SMIB' @@ -1097,6 +1118,8 @@ def runTest(self): stream = self.client.build(fileobj=script, stream=True) logs = '' for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') logs += chunk self.assertNotEqual(logs, '') @@ -1105,7 +1128,7 @@ def runTest(self): class TestBuildWithDockerignore(Cleanup, BaseTestCase): def runTest(self): - if self.client._version < 1.8: + if compare_version(self.client._version, '1.8') >= 0: return base_dir = tempfile.mkdtemp() @@ -1136,6 +1159,8 @@ def runTest(self): stream = self.client.build(path=base_dir, stream=True) logs = '' for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') logs += chunk self.assertFalse('node_modules' in logs) self.assertTrue('not-ignored' in logs) @@ -1171,16 +1196,17 @@ class TestLoadConfig(BaseTestCase): def runTest(self): folder = tempfile.mkdtemp() self.tmp_folders.append(folder) - f = open(os.path.join(folder, '.dockercfg'), 'w') + cfg_path = os.path.join(folder, '.dockercfg') + f = open(cfg_path, 'w') auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') f.write('auth = {0}\n'.format(auth_)) f.write('email = sakuya@scarlet.net') f.close() - cfg = docker.auth.load_config(folder) + cfg = docker.auth.load_config(cfg_path) self.assertNotEqual(cfg[docker.auth.INDEX_URL], None) cfg = cfg[docker.auth.INDEX_URL] - self.assertEqual(cfg['username'], b'sakuya') - self.assertEqual(cfg['password'], b'izayoi') + self.assertEqual(cfg['username'], 'sakuya') + self.assertEqual(cfg['password'], 'izayoi') self.assertEqual(cfg['email'], 'sakuya@scarlet.net') self.assertEqual(cfg.get('Auth'), None) @@ -1189,17 +1215,18 @@ class TestLoadJSONConfig(BaseTestCase): def runTest(self): folder = tempfile.mkdtemp() self.tmp_folders.append(folder) + cfg_path = os.path.join(folder, '.dockercfg') f = open(os.path.join(folder, '.dockercfg'), 'w') auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') email_ = 'sakuya@scarlet.net' - f.write('{{"{}": {{"auth": "{}", "email": "{}"}}}}\n'.format( + f.write('{{"{0}": {{"auth": "{1}", "email": "{2}"}}}}\n'.format( docker.auth.INDEX_URL, auth_, email_)) f.close() - cfg = docker.auth.load_config(folder) + cfg = docker.auth.load_config(cfg_path) self.assertNotEqual(cfg[docker.auth.INDEX_URL], None) cfg = cfg[docker.auth.INDEX_URL] - self.assertEqual(cfg['username'], b'sakuya') - self.assertEqual(cfg['password'], b'izayoi') + self.assertEqual(cfg['username'], 'sakuya') + self.assertEqual(cfg['password'], 'izayoi') self.assertEqual(cfg['email'], 'sakuya@scarlet.net') self.assertEqual(cfg.get('Auth'), None) @@ -1248,4 +1275,5 @@ def test_resource_warnings(self): if __name__ == '__main__': c = docker.Client(base_url=DEFAULT_BASE_URL) c.pull('busybox') + c.close() unittest.main() diff --git a/tests/test.py b/tests/test.py index 2a22b3c60..bd56d6468 100644 --- a/tests/test.py +++ b/tests/test.py @@ -733,7 +733,6 @@ def test_create_container_with_port_binds(self): args = fake_request.call_args self.assertEqual(args[0][0], url_prefix + 'containers/create') data = json.loads(args[1]['data']) - self.assertEqual(data['HostConfig']['PublishAllPorts'], False) port_bindings = data['HostConfig']['PortBindings'] self.assertTrue('1111/tcp' in port_bindings) self.assertTrue('2222/tcp' in port_bindings) diff --git a/tests/utils_test.py b/tests/utils_test.py index 9023b085f..2708302ea 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -5,7 +5,8 @@ from docker.client import Client from docker.errors import DockerException from docker.utils import ( - parse_repository_tag, parse_host, convert_filters, kwargs_from_env + parse_repository_tag, parse_host, convert_filters, kwargs_from_env, + create_host_config ) @@ -95,6 +96,10 @@ def test_convert_filters(self): for filters, expected in tests: self.assertEqual(convert_filters(filters), expected) + def test_create_host_config(self): + empty_config = create_host_config() + self.assertEqual(empty_config, {}) + if __name__ == '__main__': unittest.main()