From 5a4a0941aa6b711dc65c82b38d85f1c847ae89e2 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 26 May 2015 16:16:22 -0700 Subject: [PATCH 01/43] Bumped version (rc) --- docker/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/version.py b/docker/version.py index 7ff5b2a3f1..ddd4f2ad22 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "1.2.3-dev" -version_info = tuple([int(d) for d in version.replace("-dev", "").split(".")]) +version = "1.2.3-rc0" +version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) From 8375865c333a0c6a9573e1e565b103ed31a6469e Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 26 May 2015 16:18:55 -0700 Subject: [PATCH 02/43] Changelog WIP --- docs/change_log.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/change_log.md b/docs/change_log.md index 5bbbc9385b..f334630386 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -1,6 +1,12 @@ Change Log ========== +1.2.3 +----- + +Work in progress. + + 1.2.2 ----- From 829736ae7efffa4c782905d6fb10c6db665971db Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 12 Jun 2015 12:36:59 -0400 Subject: [PATCH 03/43] Allow binds to be specified as a list of strings Signed-off-by: Aanand Prasad --- docker/utils/utils.py | 3 +++ docs/volumes.md | 13 +++++++++++++ tests/test.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index e4a3c9e648..724af46504 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -174,6 +174,9 @@ def convert_port_bindings(port_bindings): def convert_volume_binds(binds): + if isinstance(binds, list): + return binds + result = [] for k, v in binds.items(): if isinstance(v, dict): diff --git a/docs/volumes.md b/docs/volumes.md index de28214005..db421557ae 100644 --- a/docs/volumes.md +++ b/docs/volumes.md @@ -19,3 +19,16 @@ container_id = c.create_container( }) ) ``` + +You can alternatively specify binds as a list. This code is equivalent to the +example above: + +```python +container_id = c.create_container( + 'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'], + host_config=docker.utils.create_host_config(binds=[ + '/home/user1/:/mnt/vol2', + '/var/www:/mnt/vol1:ro', + ]) +) +``` diff --git a/tests/test.py b/tests/test.py index e0a9e34525..97af11eec8 100644 --- a/tests/test.py +++ b/tests/test.py @@ -808,6 +808,36 @@ def test_create_container_with_binds_rw(self): DEFAULT_TIMEOUT_SECONDS ) + def test_create_container_with_binds_list(self): + try: + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + binds=[ + "/tmp:/mnt/1:ro", + "/tmp:/mnt/2", + ], + ) + ) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Binds'] = [ + "/tmp:/mnt/1:ro", + "/tmp:/mnt/2", + ] + self.assertEqual(json.loads(args[1]['data']), expected_payload) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + self.assertEqual( + args[1]['timeout'], + DEFAULT_TIMEOUT_SECONDS + ) + def test_create_container_with_port_binds(self): self.maxDiff = None try: From 7ecd23948f10772212d6d2d1c6da626a1878250a Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 12 Jun 2015 11:31:10 -0700 Subject: [PATCH 04/43] Bump RC --- docker/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/version.py b/docker/version.py index ddd4f2ad22..f47b741004 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "1.2.3-rc0" +version = "1.2.3-rc1" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) From ef35cb3fd37b459d8726fbcf9cf90995d9d51b04 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 Jun 2015 14:44:39 -0700 Subject: [PATCH 05/43] Updated ChangeLog for 1.2.3 --- docs/change_log.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/change_log.md b/docs/change_log.md index f334630386..837655f71f 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -4,8 +4,35 @@ Change Log 1.2.3 ----- -Work in progress. +[List of PRs / issues for this release](https://github.com/docker/docker-py/issues?q=milestone%3A1.2.3+is%3Aclosed) +### Deprecation warning + +* Passing host config in the `Client.start` method is now deprecated. Please use the + `host_config` in `Client.create_container` instead. + +### Features + +* Added support for `privileged` param in `Client.exec_create` + (only available in API >= 1.19) + +### Bugfixes + +* Fixed a bug where the `read_only` param in host_config wasn't handled + properly. +* Fixed a bug in `Client.execute` (this method is still deprecated). +* The `cpuset` param in `Client.create_container` is also passed as + the `CpusetCpus` param (`Cpuset` deprecated in recent versions of the API) +* Fixed an issue with integration tests being run inside a container + (`make integration-test`) +* Fixed a bug where an empty string would be considered a valid container ID + or image ID. +* Fixed a bug in `Client.insert` + + +### Documentation + +* Various fixes 1.2.2 ----- From b36e53fc3c87b59f07d4d45f0903ff46c3bf2360 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Jun 2015 19:50:31 +0200 Subject: [PATCH 06/43] Release 1.2.3 --- docker/version.py | 2 +- docs/change_log.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/version.py b/docker/version.py index f47b741004..d7a9a776a8 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "1.2.3-rc1" +version = "1.2.3" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) diff --git a/docs/change_log.md b/docs/change_log.md index 837655f71f..aac4acb1c5 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -15,6 +15,7 @@ Change Log * Added support for `privileged` param in `Client.exec_create` (only available in API >= 1.19) +* Volume binds can now also be specified as a list of strings. ### Bugfixes From 84f25d41db18479d8347ebd5f65d7c9fdcc14d69 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Jun 2015 19:58:01 +0200 Subject: [PATCH 07/43] Fixed integration test --- tests/integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 266090098d..c9ab1405e1 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1464,7 +1464,7 @@ def test_542(self): self.client.start( self.client.create_container('busybox', ['true']) ) - result = self.client.containers(trunc=True) + result = self.client.containers(all=True, trunc=True) self.assertEqual(len(result[0]['Id']), 12) From 456d0cbcd0b283a56af9acd289c5fefeddd48634 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Jun 2015 20:14:37 +0200 Subject: [PATCH 08/43] Updated site_url value in mkdocs file --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 6f70d1c0b8..107d53f57c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: docker-py Documentation site_description: An API client for Docker written in Python site_favicon: favicon_whale.png -site_url: docker-py.readthedocs.org +site_url: http://docker-py.readthedocs.org repo_url: https://github.com/docker/docker-py/ theme: readthedocs pages: From 136b0726de9ffac82078ee73bc64be5c14300af2 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Jun 2015 20:38:07 +0200 Subject: [PATCH 09/43] Updated mkdocs to use the new pages index format --- mkdocs.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 107d53f57c..8293cbc4b5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,13 +5,13 @@ site_url: http://docker-py.readthedocs.org repo_url: https://github.com/docker/docker-py/ theme: readthedocs pages: -- [index.md, Home] -- [api.md, Client API] -- [port-bindings.md, Port Bindings] -- [volumes.md, Using Volumes] -- [tls.md, Using TLS] -- [host-devices.md, Host devices] -- [hostconfig.md, Host configuration] -- [boot2docker.md, Using with boot2docker] -- [change_log.md, Change Log] -- [contributing.md, Contributing] +- Home: index.md +- Client API: api.md +- Port Bindings: port-bindings.md +- Using Volumes: volumes.md +- Using TLS: tls.md +- Host devices: host-devices.md +- Host configuration: hostconfig.md +- Using with boot2docker: boot2docker.md +- Change Log: change_log.md +- Contributing: contributing.md From b10e6d51a2db199bdd2f05aa49e37b388c40ae0d Mon Sep 17 00:00:00 2001 From: Luke Marsden Date: Wed, 3 Jun 2015 12:00:43 +0100 Subject: [PATCH 10/43] Add volume_driver param to client.create_container - Add appropriate test which also asserts that volume names can be passed through to drivers. - Add new param to docs. Signed-off-by: Luke Marsden --- docker/client.py | 5 +++-- docker/utils/utils.py | 3 ++- docs/api.md | 1 + tests/test.py | 31 +++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docker/client.py b/docker/client.py index b66480676d..0851865b46 100644 --- a/docker/client.py +++ b/docker/client.py @@ -465,7 +465,7 @@ def create_container(self, image, command=None, hostname=None, user=None, network_disabled=False, name=None, entrypoint=None, cpu_shares=None, working_dir=None, domainname=None, memswap_limit=0, cpuset=None, host_config=None, - mac_address=None, labels=None): + mac_address=None, labels=None, volume_driver=None): if isinstance(volumes, six.string_types): volumes = [volumes, ] @@ -479,7 +479,8 @@ def create_container(self, image, command=None, hostname=None, user=None, self._version, image, command, hostname, user, detach, stdin_open, tty, mem_limit, ports, environment, dns, volumes, volumes_from, network_disabled, entrypoint, cpu_shares, working_dir, domainname, - memswap_limit, cpuset, host_config, mac_address, labels + memswap_limit, cpuset, host_config, mac_address, labels, + volume_driver ) return self.create_container_from_config(config, name) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 724af46504..4ecdf6b15a 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -489,7 +489,7 @@ def create_container_config( dns=None, volumes=None, volumes_from=None, network_disabled=False, entrypoint=None, cpu_shares=None, working_dir=None, domainname=None, memswap_limit=0, cpuset=None, host_config=None, mac_address=None, - labels=None + labels=None, volume_driver=None ): if isinstance(command, six.string_types): command = shlex.split(str(command)) @@ -589,4 +589,5 @@ def create_container_config( 'HostConfig': host_config, 'MacAddress': mac_address, 'Labels': labels, + 'VolumeDriver': volume_driver, } diff --git a/docs/api.md b/docs/api.md index 26dfe60813..3b3a144266 100644 --- a/docs/api.md +++ b/docs/api.md @@ -219,6 +219,7 @@ from. Optionally a single string joining container id's with commas * host_config (dict): A [HostConfig](hostconfig.md) dictionary * mac_address (str): The Mac Address to assign the container * labels (dict or list): A dictionary of name-value labels (e.g. `{"label1": "value1", "label2": "value2"}`) or a list of names of labels to set with empty values (e.g. `["label1", "label2"]`) +* volume_driver (str): The name of a volume driver/plugin. **Returns** (dict): A dictionary with an image 'Id' key and a 'Warnings' key. diff --git a/tests/test.py b/tests/test.py index 97af11eec8..f5815f0fea 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1380,6 +1380,37 @@ def test_create_container_with_labels_list(self): args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS ) + def test_create_container_with_named_volume(self): + try: + mount_dest = '/mnt' + volume_name = 'name' + self.client.create_container( + 'busybox', 'true', + host_config=create_host_config( + binds={volume_name: { + "bind": mount_dest, + "ro": False + }}), + volume_driver='foodriver', + ) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['VolumeDriver'] = 'foodriver' + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Binds'] = ["name:/mnt:rw"] + self.assertEqual(json.loads(args[1]['data']), expected_payload) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + self.assertEqual( + args[1]['timeout'], + DEFAULT_TIMEOUT_SECONDS + ) + def test_resize_container(self): try: self.client.resize( From 2766afd060eb73bd9a92f269c652f9bf65127ab8 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Sat, 13 Jun 2015 19:09:50 -0400 Subject: [PATCH 11/43] Add raise_for_status check to push and pull methods as underlying exceptions(such as push already in progress) will be hidden in the stream generator otherwise. --- docker/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/client.py b/docker/client.py index 0851865b46..4607971a81 100644 --- a/docker/client.py +++ b/docker/client.py @@ -914,6 +914,8 @@ def pull(self, repository, tag=None, stream=False, response = self._post(self._url('/images/create'), params=params, headers=headers, stream=stream, timeout=None) + self._raise_for_status(response) + if stream: return self._stream_helper(response) else: @@ -950,6 +952,8 @@ def push(self, repository, tag=None, stream=False, else: response = self._post_json(u, None, stream=stream, params=params) + self._raise_for_status(response) + return stream and self._stream_helper(response) \ or self._result(response) From 84926a88e06cb8c6212376be29f1325beb22e96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20M=C3=B6ller?= Date: Mon, 15 Jun 2015 10:29:00 +0200 Subject: [PATCH 12/43] Add and document a decode parameter for build This makes the build method consistent with the events method and adds some convenience. --- docker/client.py | 5 +++-- docs/api.md | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docker/client.py b/docker/client.py index 4607971a81..e6022508d0 100644 --- a/docker/client.py +++ b/docker/client.py @@ -307,7 +307,8 @@ def attach_socket(self, container, params=None, ws=False): def build(self, path=None, tag=None, quiet=False, fileobj=None, nocache=False, rm=False, stream=False, timeout=None, custom_context=False, encoding=None, pull=True, - forcerm=False, dockerfile=None, container_limits=None): + forcerm=False, dockerfile=None, container_limits=None, + decode=False): remote = context = headers = None container_limits = container_limits or {} if path is None and fileobj is None: @@ -398,7 +399,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, context.close() if stream: - return self._stream_helper(response) + return self._stream_helper(response, decode=decode) else: output = self._result(response) srch = r'Successfully built ([0-9a-f]+)' diff --git a/docs/api.md b/docs/api.md index 3b3a144266..19048e3935 100644 --- a/docs/api.md +++ b/docs/api.md @@ -71,6 +71,8 @@ correct value (e.g `gzip`). - memswap (int): Total memory (memory + swap), -1 to disable swap - cpushares (int): CPU shares (relative weight) - cpusetcpus (str): CPUs in which to allow exection, e.g., `"0-3"`, `"0,1"` +* decode (bool): If set to true, the returned stream will be decoded into dicts on the fly. + False by default. **Returns** (generator): A generator of the build output From 3007548d062dcc3655d74d097edf71b744feb675 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 Jun 2015 14:45:38 -0700 Subject: [PATCH 13/43] Bumped version to 1.3.0 (dev) --- docker/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/version.py b/docker/version.py index d7a9a776a8..88859a6c00 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "1.2.3" +version = "1.3.0-dev" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) From 3a70f89e7d2b30b39103af6534483c830592b85b Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 Jun 2015 15:09:38 -0700 Subject: [PATCH 14/43] small doc fixes --- docs/api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index 19048e3935..4b64147155 100644 --- a/docs/api.md +++ b/docs/api.md @@ -71,10 +71,10 @@ correct value (e.g `gzip`). - memswap (int): Total memory (memory + swap), -1 to disable swap - cpushares (int): CPU shares (relative weight) - cpusetcpus (str): CPUs in which to allow exection, e.g., `"0-3"`, `"0,1"` -* decode (bool): If set to true, the returned stream will be decoded into dicts on the fly. - False by default. +* decode (bool): If set to `True`, the returned stream will be decoded into + dicts on the fly. Default `False`. -**Returns** (generator): A generator of the build output +**Returns** (generator): A generator for the build output ```python >>> from io import BytesIO From dcfefb47802b79710193f8fb588b0a4394599be2 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 Jun 2015 15:17:40 -0700 Subject: [PATCH 15/43] Enforce consistent style for push and pull methods --- docker/client.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docker/client.py b/docker/client.py index e6022508d0..c73e560b7d 100644 --- a/docker/client.py +++ b/docker/client.py @@ -912,15 +912,17 @@ def pull(self, repository, tag=None, stream=False, else: headers['X-Registry-Auth'] = auth.encode_header(auth_config) - response = self._post(self._url('/images/create'), params=params, - headers=headers, stream=stream, timeout=None) + response = self._post( + self._url('/images/create'), params=params, headers=headers, + stream=stream, timeout=None + ) self._raise_for_status(response) if stream: return self._stream_helper(response) - else: - return self._result(response) + + return self._result(response) def push(self, repository, tag=None, stream=False, insecure_registry=False): @@ -948,15 +950,16 @@ def push(self, repository, tag=None, stream=False, if authcfg: headers['X-Registry-Auth'] = auth.encode_header(authcfg) - response = self._post_json(u, None, headers=headers, - stream=stream, params=params) - else: - response = self._post_json(u, None, stream=stream, params=params) + response = self._post_json( + u, None, headers=headers, stream=stream, params=params + ) self._raise_for_status(response) - return stream and self._stream_helper(response) \ - or self._result(response) + if stream: + return self._stream_helper(response) + + return self._result(response) @check_resource def remove_container(self, container, v=False, link=False, force=False): From 65e6d09076043b6a69b931d63065ac5345884537 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 Jun 2015 17:07:11 -0700 Subject: [PATCH 16/43] Only allow volume_driver param if API version >= 1.19 --- docker/utils/utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 4ecdf6b15a..84acca3a66 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -500,10 +500,15 @@ def create_container_config( ] if labels is not None and compare_version('1.18', version) < 0: - raise errors.DockerException( + raise errors.InvalidVersion( 'labels were only introduced in API version 1.18' ) + if volume_driver is not None and compare_version('1.19', version) < 0: + raise errors.InvalidVersion( + 'Volume drivers were only introduced in API version 1.19' + ) + if isinstance(labels, list): labels = dict((lbl, six.text_type('')) for lbl in labels) @@ -557,9 +562,9 @@ def create_container_config( message = ('{0!r} parameter has no effect on create_container().' ' It has been moved to start()') if dns is not None: - raise errors.DockerException(message.format('dns')) + raise errors.InvalidVersion(message.format('dns')) if volumes_from is not None: - raise errors.DockerException(message.format('volumes_from')) + raise errors.InvalidVersion(message.format('volumes_from')) return { 'Hostname': hostname, From df84b68ff2e9718a31d63281f5ce24ac97fbf345 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 Jun 2015 17:07:32 -0700 Subject: [PATCH 17/43] Bumped default API version == 1.19 --- docker/constants.py | 2 +- tests/fake_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/constants.py b/docker/constants.py index 233d9b1717..f99f19226e 100644 --- a/docker/constants.py +++ b/docker/constants.py @@ -1,4 +1,4 @@ -DEFAULT_DOCKER_API_VERSION = '1.18' +DEFAULT_DOCKER_API_VERSION = '1.19' DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 CONTAINER_LIMITS_KEYS = [ diff --git a/tests/fake_api.py b/tests/fake_api.py index 2ee146e453..d201838e71 100644 --- a/tests/fake_api.py +++ b/tests/fake_api.py @@ -14,7 +14,7 @@ import fake_stat -CURRENT_VERSION = 'v1.18' +CURRENT_VERSION = 'v1.19' FAKE_CONTAINER_ID = '3cc2351ab11b' FAKE_IMAGE_ID = 'e9aa60c60128' From e1a8d27baafc4b91c3010d71369568e35f644181 Mon Sep 17 00:00:00 2001 From: Ed Morley Date: Wed, 10 Jun 2015 14:30:41 +0100 Subject: [PATCH 18/43] Docs: Update boot2docker shellinit example to use 'eval' The boot2docker documentation has since changed the recommended way to use shellinit - see boot2docker/boot2docker#786. The command also no longer prints the export lines to the console, so have updated the example output. --- docs/boot2docker.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/boot2docker.md b/docs/boot2docker.md index cb83b45335..43aa558d21 100644 --- a/docs/boot2docker.md +++ b/docs/boot2docker.md @@ -4,13 +4,10 @@ For usage with boot2docker, there is a helper function in the utils package name First run boot2docker in your shell: ```bash -$ $(boot2docker shellinit) +$ eval "$(boot2docker shellinit)" Writing /Users/you/.boot2docker/certs/boot2docker-vm/ca.pem Writing /Users/you/.boot2docker/certs/boot2docker-vm/cert.pem Writing /Users/you/.boot2docker/certs/boot2docker-vm/key.pem -export DOCKER_HOST=tcp://192.168.59.103:2376 -export DOCKER_CERT_PATH=/Users/you/.boot2docker/certs/boot2docker-vm -export DOCKER_TLS_VERIFY=1 ``` You can then instantiate `docker.Client` like this: From 8bee1f44c9a2ce1ce9f6b446129c30f5f4b54503 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 5 Jun 2015 12:55:03 +0100 Subject: [PATCH 19/43] Allow any mode string to be passed into a volume bind Volume binds now take a "mode" key, whose value can be any string. "ro" is still supported. It is an error to specify both "ro" and "mode". Signed-off-by: Aanand Prasad --- docker/utils/utils.py | 15 +++++++++++++- docs/volumes.md | 4 ++-- tests/test.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 84acca3a66..77bd7f3fc4 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -180,8 +180,21 @@ def convert_volume_binds(binds): result = [] for k, v in binds.items(): if isinstance(v, dict): + if 'ro' in v and 'mode' in v: + raise ValueError( + 'Binding cannot contain both "ro" and "mode": {}' + .format(repr(v)) + ) + + if 'ro' in v: + mode = 'ro' if v['ro'] else 'rw' + elif 'mode' in v: + mode = v['mode'] + else: + mode = 'rw' + result.append('{0}:{1}:{2}'.format( - k, v['bind'], 'ro' if v.get('ro', False) else 'rw' + k, v['bind'], mode )) else: result.append('{0}:{1}:rw'.format(k, v)) diff --git a/docs/volumes.md b/docs/volumes.md index db421557ae..16c3228e52 100644 --- a/docs/volumes.md +++ b/docs/volumes.md @@ -10,11 +10,11 @@ container_id = c.create_container( host_config=docker.utils.create_host_config(binds={ '/home/user1/': { 'bind': '/mnt/vol2', - 'ro': False + 'mode': 'rw', }, '/var/www': { 'bind': '/mnt/vol1', - 'ro': True + 'mode': 'ro', } }) ) diff --git a/tests/test.py b/tests/test.py index f5815f0fea..bac959298b 100644 --- a/tests/test.py +++ b/tests/test.py @@ -808,6 +808,53 @@ def test_create_container_with_binds_rw(self): DEFAULT_TIMEOUT_SECONDS ) + def test_create_container_with_binds_mode(self): + try: + mount_dest = '/mnt' + mount_origin = '/tmp' + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + binds={mount_origin: { + "bind": mount_dest, + "mode": "z", + }} + ) + ) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Binds'] = ["/tmp:/mnt:z"] + self.assertEqual(json.loads(args[1]['data']), expected_payload) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + self.assertEqual( + args[1]['timeout'], + DEFAULT_TIMEOUT_SECONDS + ) + + def test_create_container_with_binds_mode_and_ro_error(self): + try: + mount_dest = '/mnt' + mount_origin = '/tmp' + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + binds={mount_origin: { + "bind": mount_dest, + "mode": "z", + "ro": True, + }} + ) + ) + except ValueError: + return + + self.fail('Command should raise ValueError') + def test_create_container_with_binds_list(self): try: self.client.create_container( From bcf4d919a2d7d77573a413a60938e5ac88fba5a4 Mon Sep 17 00:00:00 2001 From: Scott Sanderson Date: Tue, 2 Jun 2015 11:00:00 -0400 Subject: [PATCH 20/43] Use functools.wraps for check_resource decorator. This helps runtime introspection tools like the `help()` builting or IPython's `?` operator correctly find the underlying method instead of the decorator definition. --- docker/utils/decorators.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index 4771da21ee..7ffcce1e7c 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -1,7 +1,10 @@ +from functools import wraps from .. import errors def check_resource(f): + + @wraps(f) def wrapped(self, resource_id=None, *args, **kwargs): if resource_id is None: if kwargs.get('container'): From 1b3a6b44802ea10a1504e151ab17bd9c9cc7fa42 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Jun 2015 21:24:49 +0200 Subject: [PATCH 21/43] Fixed import style --- docker/utils/decorators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index 7ffcce1e7c..a4be50ce5c 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -1,10 +1,11 @@ -from functools import wraps +import functools + from .. import errors def check_resource(f): - @wraps(f) + @functools.wraps(f) def wrapped(self, resource_id=None, *args, **kwargs): if resource_id is None: if kwargs.get('container'): From b08ae792853eb00f22880d97c486d028036b3106 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Thu, 21 May 2015 12:59:38 -0700 Subject: [PATCH 22/43] Update docker-py to use a more portable sense of HOME. This makes docker-py consistent with Docker's newish way of establishing the path to .dockercfg: https://github.com/docker/docker/blob/master/pkg/homedir/homedir.go --- docker/auth/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/auth/auth.py b/docker/auth/auth.py index 373df565e2..a890fcebec 100644 --- a/docker/auth/auth.py +++ b/docker/auth/auth.py @@ -115,7 +115,7 @@ def load_config(config_path=None): conf = {} data = None - config_file = config_path or os.path.join(os.environ.get('HOME', '.'), + config_file = config_path or os.path.join(os.path.expanduser('~'), DOCKER_CONFIG_FILENAME) # if config path doesn't exist return empty config From 9eb9aeca80d09bf33963a7d40658fe78f29eca19 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 25 May 2015 20:28:27 -0700 Subject: [PATCH 23/43] Allow extra_hosts to be a list too The current map syntax does not allow the API equivalent of --add-host foo:1.1.1.1 --add-host foo:2.2.2.2 The above will map one hostname to two IPs. The above is valid in Docker. --- docker/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 77bd7f3fc4..b322806c32 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -450,7 +450,7 @@ def create_host_config( for k, v in sorted(six.iteritems(extra_hosts)) ] - host_config['ExtraHosts'] = extra_hosts + host_config['ExtraHosts'] = extra_hosts if links is not None: if isinstance(links, dict): From 2792bd65c61fb360d32a77b30c097806fec9ef3a Mon Sep 17 00:00:00 2001 From: Giorgos Logiotatidis Date: Mon, 1 Jun 2015 20:21:53 +0300 Subject: [PATCH 24/43] Set default value for pull to False on build(). Fixes 622. --- docker/client.py | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/docker/client.py b/docker/client.py index c73e560b7d..f87e40ca8e 100644 --- a/docker/client.py +++ b/docker/client.py @@ -306,9 +306,8 @@ def attach_socket(self, container, params=None, ws=False): def build(self, path=None, tag=None, quiet=False, fileobj=None, nocache=False, rm=False, stream=False, timeout=None, - custom_context=False, encoding=None, pull=True, - forcerm=False, dockerfile=None, container_limits=None, - decode=False): + custom_context=False, encoding=None, pull=False, + forcerm=False, dockerfile=None, container_limits=None): remote = context = headers = None container_limits = container_limits or {} if path is None and fileobj is None: @@ -399,7 +398,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, context.close() if stream: - return self._stream_helper(response, decode=decode) + return self._stream_helper(response) else: output = self._result(response) srch = r'Successfully built ([0-9a-f]+)' @@ -466,7 +465,7 @@ def create_container(self, image, command=None, hostname=None, user=None, network_disabled=False, name=None, entrypoint=None, cpu_shares=None, working_dir=None, domainname=None, memswap_limit=0, cpuset=None, host_config=None, - mac_address=None, labels=None, volume_driver=None): + mac_address=None, labels=None): if isinstance(volumes, six.string_types): volumes = [volumes, ] @@ -480,8 +479,7 @@ def create_container(self, image, command=None, hostname=None, user=None, self._version, image, command, hostname, user, detach, stdin_open, tty, mem_limit, ports, environment, dns, volumes, volumes_from, network_disabled, entrypoint, cpu_shares, working_dir, domainname, - memswap_limit, cpuset, host_config, mac_address, labels, - volume_driver + memswap_limit, cpuset, host_config, mac_address, labels ) return self.create_container_from_config(config, name) @@ -912,17 +910,13 @@ def pull(self, repository, tag=None, stream=False, else: headers['X-Registry-Auth'] = auth.encode_header(auth_config) - response = self._post( - self._url('/images/create'), params=params, headers=headers, - stream=stream, timeout=None - ) - - self._raise_for_status(response) + response = self._post(self._url('/images/create'), params=params, + headers=headers, stream=stream, timeout=None) if stream: return self._stream_helper(response) - - return self._result(response) + else: + return self._result(response) def push(self, repository, tag=None, stream=False, insecure_registry=False): @@ -950,16 +944,13 @@ def push(self, repository, tag=None, stream=False, if authcfg: headers['X-Registry-Auth'] = auth.encode_header(authcfg) - response = self._post_json( - u, None, headers=headers, stream=stream, params=params - ) - - self._raise_for_status(response) - - if stream: - return self._stream_helper(response) + response = self._post_json(u, None, headers=headers, + stream=stream, params=params) + else: + response = self._post_json(u, None, stream=stream, params=params) - return self._result(response) + return stream and self._stream_helper(response) \ + or self._result(response) @check_resource def remove_container(self, container, v=False, link=False, force=False): From 808d4b6cbae819b44211b1e54bc90e4baa75ce35 Mon Sep 17 00:00:00 2001 From: Giorgos Logiotatidis Date: Mon, 1 Jun 2015 20:22:46 +0300 Subject: [PATCH 25/43] Fix pull parameter for docker server version < 1.7. --- docker/client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/client.py b/docker/client.py index f87e40ca8e..242f95535d 100644 --- a/docker/client.py +++ b/docker/client.py @@ -352,6 +352,12 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, 'dockerfile was only introduced in API version 1.17' ) + # Docker server 1.6 only supports values 1 and 0 for pull + # parameter. This was later fixed to support a wider range of + # values, including true / false. + # See also https://github.com/docker/docker/issues/13631 + pull = 1 if pull else 0 + u = self._url('/build') params = { 't': tag, From 28b0168385d423ecc3868a928f52c114ae8d41d0 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 19 Jun 2015 02:59:35 +0200 Subject: [PATCH 26/43] Added Aanand (@aanand) as a maintainer --- MAINTAINERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MAINTAINERS b/MAINTAINERS index f0bc46b607..14f61963fc 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,3 +1,3 @@ -Joffrey F (@shin-) +Joffrey F (@shin-) Maxime Petazzoni (@mpetazzoni) - +Aanand Prasad (@aanand) From 5e1ce88cf2c9d8e5498a2a04cdc2c19ef900c556 Mon Sep 17 00:00:00 2001 From: Marko Mikulicic Date: Fri, 29 May 2015 00:58:49 +0200 Subject: [PATCH 27/43] Fix pinging an unauthenticated v2 registry --- docker/utils/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index b322806c32..f79f7c26e2 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -129,7 +129,9 @@ def ping(url): except Exception: return False else: - return res.status_code < 400 + # We don't send yet auth headers + # and a v2 registry will respond with status 401 + return res.status_code == 401 or res.status_code < 400 def _convert_port_binding(binding): From 538b0ceb46e47b5aa2f01b036dc87ec4d6f6fc6e Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 19 Jun 2015 22:42:43 +0200 Subject: [PATCH 28/43] Support 401 status for v2 registry endpoint --- docker/utils/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index f79f7c26e2..e4e665f3f4 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -120,10 +120,10 @@ def compare_version(v1, v2): def ping_registry(url): - return ping(url + '/v2/') or ping(url + '/v1/_ping') + return ping(url + '/v2/', [401]) or ping(url + '/v1/_ping') -def ping(url): +def ping(url, valid_4xx_statuses=None): try: res = requests.get(url, timeout=3) except Exception: @@ -131,7 +131,10 @@ def ping(url): else: # We don't send yet auth headers # and a v2 registry will respond with status 401 - return res.status_code == 401 or res.status_code < 400 + return ( + res.status_code < 400 or + (valid_4xx_statuses and res.status_code in valid_4xx_statuses) + ) def _convert_port_binding(binding): From 571cdcfa118ebea15bd63299efe13d5d69f00168 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 19 Jun 2015 23:09:51 +0200 Subject: [PATCH 29/43] Updated websocket-client dependency to latest version (now supports python 3) --- docker/client.py | 9 ++------- requirements.txt | 2 +- requirements3.txt | 2 -- setup.py | 2 +- 4 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 requirements3.txt diff --git a/docker/client.py b/docker/client.py index 242f95535d..c8335d96ea 100644 --- a/docker/client.py +++ b/docker/client.py @@ -23,6 +23,8 @@ import requests import requests.exceptions import six +import websocket + from . import constants from . import errors @@ -33,10 +35,6 @@ from .tls import TLSConfig -if not six.PY3: - import websocket - - class Client(requests.Session): def __init__(self, base_url=None, version=None, timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False): @@ -154,9 +152,6 @@ def _attach_params(self, override=None): @check_resource def _attach_websocket(self, container, params=None): - if six.PY3: - raise NotImplementedError("This method is not currently supported " - "under python 3") url = self._url("/containers/{0}/attach/ws".format(container)) req = requests.Request("POST", url, params=self._attach_params(params)) full_url = req.prepare().url diff --git a/requirements.txt b/requirements.txt index a1c3aa55d5..b23ea488ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.5.3 six>=1.3.0 -websocket-client==0.11.0 +websocket-client==0.32.0 diff --git a/requirements3.txt b/requirements3.txt deleted file mode 100644 index 9666476df4..0000000000 --- a/requirements3.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests==2.5.3 -six>=1.3.0 diff --git a/setup.py b/setup.py index 6000e6fd6e..485d33c77a 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ ] if sys.version_info[0] < 3: - requirements.append('websocket-client >= 0.11.0') + requirements.append('websocket-client >= 0.32.0') exec(open('docker/version.py').read()) From a89527cbd4c87445e553d972c8f8e96a6a3752c6 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 19 Jun 2015 23:16:54 +0200 Subject: [PATCH 30/43] Simplified tox config --- tox.ini | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/tox.ini b/tox.ini index 35b9bd6fd8..10b9df935e 100644 --- a/tox.ini +++ b/tox.ini @@ -2,24 +2,6 @@ envlist = py26, py27, py32, py33, py34, flake8 skipsdist=True -[testenv:py26] -usedevelop=True -commands = - {envbindir}/coverage run -p tests/test.py - {envbindir}/coverage run -p tests/utils_test.py -deps = - -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt - -[testenv:py27] -usedevelop=True -commands = - {envbindir}/coverage run -p tests/test.py - {envbindir}/coverage run -p tests/utils_test.py -deps = - -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt - [testenv] usedevelop=True commands = @@ -29,8 +11,8 @@ commands = {envbindir}/coverage report {envbindir}/coverage html deps = - -r{toxinidir}/requirements3.txt -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt [testenv:flake8] commands = flake8 docker tests From b4df4b95949e1c97c42390bf17846022cfcdbccf Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 24 Jun 2015 22:58:56 +0200 Subject: [PATCH 31/43] Fix stop timeout bug --- docker/client.py | 2 +- tests/integration_test.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docker/client.py b/docker/client.py index c8335d96ea..a939fb5ddd 100644 --- a/docker/client.py +++ b/docker/client.py @@ -1095,7 +1095,7 @@ def stop(self, container, timeout=10): url = self._url("/containers/{0}/stop".format(container)) res = self._post(url, params=params, - timeout=(timeout + self.timeout)) + timeout=(timeout + (self.timeout or 0))) self._raise_for_status(res) @check_resource diff --git a/tests/integration_test.py b/tests/integration_test.py index c9ab1405e1..4b9869e28a 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1467,6 +1467,11 @@ def test_542(self): result = self.client.containers(all=True, trunc=True) self.assertEqual(len(result[0]['Id']), 12) + def test_649(self): + self.client.timeout = None + ctnr = self.client.create_container('busybox', ['sleep', '2']) + self.client.start(ctnr) + self.client.stop(ctnr) if __name__ == '__main__': c = docker.Client(base_url=DEFAULT_BASE_URL) From 99d7c731c23be15c700f02c32b55d4793bddf16e Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Wed, 20 May 2015 15:33:21 -0700 Subject: [PATCH 32/43] Move image/container ID resolution to @check_resource decorator. --- docker/client.py | 51 -------------------------------------- docker/utils/decorators.py | 2 ++ tests/test.py | 38 ++++++++++++++-------------- 3 files changed, 22 insertions(+), 69 deletions(-) diff --git a/docker/client.py b/docker/client.py index a939fb5ddd..878125686b 100644 --- a/docker/client.py +++ b/docker/client.py @@ -246,8 +246,6 @@ def api_version(self): @check_resource def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): - if isinstance(container, dict): - container = container.get('Id') params = { 'logs': logs and 1 or 0, 'stdout': stdout and 1 or 0, @@ -292,9 +290,6 @@ def attach_socket(self, container, params=None, ws=False): if ws: return self._attach_websocket(container, params) - if isinstance(container, dict): - container = container.get('Id') - u = self._url("/containers/{0}/attach".format(container)) return self._get_raw_response_socket(self.post( u, None, params=self._attach_params(params), stream=True)) @@ -411,8 +406,6 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, @check_resource def commit(self, container, repository=None, tag=None, message=None, author=None, conf=None): - if isinstance(container, dict): - container = container.get('Id') params = { 'container': container, 'repo': repository, @@ -449,8 +442,6 @@ def containers(self, quiet=False, all=False, trunc=False, latest=False, @check_resource def copy(self, container, resource): - if isinstance(container, dict): - container = container.get('Id') res = self._post_json( self._url("/containers/{0}/copy".format(container)), data={"Resource": resource}, @@ -494,8 +485,6 @@ def create_container_from_config(self, config, name=None): @check_resource def diff(self, container): - if isinstance(container, dict): - container = container.get('Id') return self._result(self._get(self._url("/containers/{0}/changes". format(container))), True) @@ -540,8 +529,6 @@ def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False, raise errors.InvalidVersion( 'Privileged exec is not supported in API < 1.19' ) - if isinstance(container, dict): - container = container.get('Id') if isinstance(cmd, six.string_types): cmd = shlex.split(str(cmd)) @@ -606,8 +593,6 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False): @check_resource def export(self, container): - if isinstance(container, dict): - container = container.get('Id') res = self._get(self._url("/containers/{0}/export".format(container)), stream=True) self._raise_for_status(res) @@ -745,16 +730,12 @@ def insert(self, image, url, path): @check_resource def inspect_container(self, container): - if isinstance(container, dict): - container = container.get('Id') return self._result( self._get(self._url("/containers/{0}/json".format(container))), True) @check_resource def inspect_image(self, image): - if isinstance(image, dict): - image = image.get('Id') return self._result( self._get(self._url("/images/{0}/json".format(image))), True @@ -762,8 +743,6 @@ def inspect_image(self, image): @check_resource def kill(self, container, signal=None): - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/kill".format(container)) params = {} if signal is not None: @@ -811,8 +790,6 @@ def login(self, username, password=None, email=None, registry=None, @check_resource def logs(self, container, stdout=True, stderr=True, stream=False, timestamps=False, tail='all'): - if isinstance(container, dict): - container = container.get('Id') if utils.compare_version('1.11', self._version) >= 0: params = {'stderr': stderr and 1 or 0, 'stdout': stdout and 1 or 0, @@ -845,8 +822,6 @@ def logs(self, container, stdout=True, stderr=True, stream=False, @check_resource def pause(self, container): - if isinstance(container, dict): - container = container.get('Id') url = self._url('/containers/{0}/pause'.format(container)) res = self._post(url) self._raise_for_status(res) @@ -856,8 +831,6 @@ def ping(self): @check_resource def port(self, container, private_port): - if isinstance(container, dict): - container = container.get('Id') res = self._get(self._url("/containers/{0}/json".format(container))) self._raise_for_status(res) json_ = res.json() @@ -955,8 +928,6 @@ def push(self, repository, tag=None, stream=False, @check_resource def remove_container(self, container, v=False, link=False, force=False): - if isinstance(container, dict): - container = container.get('Id') params = {'v': v, 'link': link, 'force': force} res = self._delete(self._url("/containers/" + container), params=params) @@ -964,8 +935,6 @@ def remove_container(self, container, v=False, link=False, force=False): @check_resource def remove_image(self, image, force=False, noprune=False): - if isinstance(image, dict): - image = image.get('Id') params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/" + image), params=params) self._raise_for_status(res) @@ -976,8 +945,6 @@ def rename(self, container, name): raise errors.InvalidVersion( 'rename was only introduced in API version 1.17' ) - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/rename".format(container)) params = {'name': name} res = self._post(url, params=params) @@ -985,9 +952,6 @@ def rename(self, container, name): @check_resource def resize(self, container, height, width): - if isinstance(container, dict): - container = container.get('Id') - params = {'h': height, 'w': width} url = self._url("/containers/{0}/resize".format(container)) res = self._post(url, params=params) @@ -995,8 +959,6 @@ def resize(self, container, height, width): @check_resource def restart(self, container, timeout=10): - if isinstance(container, dict): - container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/restart".format(container)) res = self._post(url, params=params) @@ -1061,9 +1023,6 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None, ipc_mode=ipc_mode, security_opt=security_opt, ulimits=ulimits ) - if isinstance(container, dict): - container = container.get('Id') - url = self._url("/containers/{0}/start".format(container)) if not start_config: start_config = None @@ -1082,15 +1041,11 @@ def stats(self, container, decode=None): raise errors.InvalidVersion( 'Stats retrieval is not supported in API < 1.17!') - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/stats".format(container)) return self._stream_helper(self._get(url, stream=True), decode=decode) @check_resource def stop(self, container, timeout=10): - if isinstance(container, dict): - container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/stop".format(container)) @@ -1112,8 +1067,6 @@ def tag(self, image, repository, tag=None, force=False): @check_resource def top(self, container): - if isinstance(container, dict): - container = container.get('Id') u = self._url("/containers/{0}/top".format(container)) return self._result(self._get(u), True) @@ -1123,16 +1076,12 @@ def version(self, api_version=True): @check_resource def unpause(self, container): - if isinstance(container, dict): - container = container.get('Id') url = self._url('/containers/{0}/unpause'.format(container)) res = self._post(url) self._raise_for_status(res) @check_resource def wait(self, container, timeout=None): - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/wait".format(container)) res = self._post(url, timeout=timeout) self._raise_for_status(res) diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index a4be50ce5c..d3869e84d6 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -12,6 +12,8 @@ def wrapped(self, resource_id=None, *args, **kwargs): resource_id = kwargs.pop('container') elif kwargs.get('image'): resource_id = kwargs.pop('image') + if isinstance(resource_id, dict): + resource_id = resource_id.get('Id') if not resource_id: raise errors.NullResource( 'image or container param is undefined' diff --git a/tests/test.py b/tests/test.py index bac959298b..40a7e30a3b 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1894,15 +1894,16 @@ def test_inspect_container(self): timeout=DEFAULT_TIMEOUT_SECONDS ) - def test_inspect_container_empty_id(self): - try: - self.client.inspect_container('') - except docker.errors.NullResource as e: - self.assertEqual( - e.args[0], 'image or container param is undefined' - ) - else: - self.fail('Command expected NullResource exception') + def test_inspect_container_undefined_id(self): + for arg in None, '', {True: True}: + try: + self.client.inspect_container(arg) + except docker.errors.NullResource as e: + self.assertEqual( + e.args[0], 'image or container param is undefined' + ) + else: + self.fail('Command expected NullResource exception') def test_container_stats(self): try: @@ -2075,15 +2076,16 @@ def test_inspect_image(self): timeout=DEFAULT_TIMEOUT_SECONDS ) - def test_inspect_image_empty_id(self): - try: - self.client.inspect_image('') - except docker.errors.NullResource as e: - self.assertEqual( - e.args[0], 'image or container param is undefined' - ) - else: - self.fail('Command expected NullResource exception') + def test_inspect_image_undefined_id(self): + for arg in None, '', {True: True}: + try: + self.client.inspect_image(arg) + except docker.errors.NullResource as e: + self.assertEqual( + e.args[0], 'image or container param is undefined' + ) + else: + self.fail('Command expected NullResource exception') def test_insert_image(self): try: From 20ee30eaa49551ac5ca58a70b5e4d32325cd8c90 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 30 Jun 2015 01:49:20 +0200 Subject: [PATCH 33/43] Cleanup --- docker/utils/decorators.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index d3869e84d6..5da3df83fc 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -4,7 +4,6 @@ def check_resource(f): - @functools.wraps(f) def wrapped(self, resource_id=None, *args, **kwargs): if resource_id is None: @@ -12,11 +11,11 @@ def wrapped(self, resource_id=None, *args, **kwargs): resource_id = kwargs.pop('container') elif kwargs.get('image'): resource_id = kwargs.pop('image') - if isinstance(resource_id, dict): - resource_id = resource_id.get('Id') if not resource_id: raise errors.NullResource( 'image or container param is undefined' ) + if isinstance(resource_id, dict): + resource_id = resource_id.get('Id') return f(self, resource_id, *args, **kwargs) return wrapped From 99fb7bb8d139e9567993e37715e4c9f26dd953a8 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 30 Jun 2015 02:50:34 +0200 Subject: [PATCH 34/43] Fix small decorator issue --- docker/utils/decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index 5da3df83fc..3c42fe4b9f 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -11,11 +11,11 @@ def wrapped(self, resource_id=None, *args, **kwargs): resource_id = kwargs.pop('container') elif kwargs.get('image'): resource_id = kwargs.pop('image') + if isinstance(resource_id, dict): + resource_id = resource_id.get('Id') if not resource_id: raise errors.NullResource( 'image or container param is undefined' ) - if isinstance(resource_id, dict): - resource_id = resource_id.get('Id') return f(self, resource_id, *args, **kwargs) return wrapped From 10126b01321c66caefd070b046faf616cedbe9f0 Mon Sep 17 00:00:00 2001 From: Kevin Martin Date: Sun, 28 Jun 2015 23:32:03 -0400 Subject: [PATCH 35/43] Prefer new Docker config location and format. This tries to load Docker authentication info from ~/.docker/config.json before falling back to its legacy location and format at ~/.dockercfg. Resolves https://github.com/docker/docker-py/issues/648 --- docker/auth/auth.py | 56 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/docker/auth/auth.py b/docker/auth/auth.py index a890fcebec..1c29615546 100644 --- a/docker/auth/auth.py +++ b/docker/auth/auth.py @@ -23,7 +23,8 @@ from .. import errors INDEX_URL = 'https://index.docker.io/v1/' -DOCKER_CONFIG_FILENAME = '.dockercfg' +DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json') +LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg' def expand_registry_url(hostname, insecure=False): @@ -107,6 +108,29 @@ def encode_full_header(auth): return encode_header({'configs': auth}) +def parse_auth(entries): + """ + Parses authentication entries + + Args: + entries: Dict of authentication entries. + + Returns: + Authentication registry. + """ + + conf = {} + for registry, entry in six.iteritems(entries): + username, password = decode_auth(entry['auth']) + conf[registry] = { + 'username': username, + 'password': password, + 'email': entry['email'], + 'serveraddress': registry, + } + return conf + + def load_config(config_path=None): """ Loads authentication data from a Docker configuration file in the given @@ -115,26 +139,34 @@ def load_config(config_path=None): conf = {} data = None + # Prefer ~/.docker/config.json. config_file = config_path or os.path.join(os.path.expanduser('~'), DOCKER_CONFIG_FILENAME) + if os.path.exists(config_file): + try: + with open(config_file) as f: + for section, data in six.iteritems(json.load(f)): + if section != 'auths': + continue + return parse_auth(data) + except (IOError, KeyError, ValueError): + # Likely missing new Docker config file or it's in an + # unknown format, continue to attempt to read old location + # and format. + pass + + config_file = config_path or os.path.join(os.path.expanduser('~'), + LEGACY_DOCKER_CONFIG_FILENAME) + # if config path doesn't exist return empty config if not os.path.exists(config_file): return {} - # First try as JSON + # Try reading legacy location as JSON. try: with open(config_file) as f: - conf = {} - for registry, entry in six.iteritems(json.load(f)): - username, password = decode_auth(entry['auth']) - conf[registry] = { - 'username': username, - 'password': password, - 'email': entry['email'], - 'serveraddress': registry, - } - return conf + return parse_auth(json.load(f)) except: pass From 7d4dfcb3b918d04030e72316234dc747361438dd Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 30 Jun 2015 03:00:10 +0200 Subject: [PATCH 36/43] Added git@ as a valid prefix for remote build paths --- docker/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/client.py b/docker/client.py index 878125686b..f467847f6a 100644 --- a/docker/client.py +++ b/docker/client.py @@ -316,7 +316,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, elif fileobj is not None: context = utils.mkbuildcontext(fileobj) elif path.startswith(('http://', 'https://', - 'git://', 'github.com/')): + 'git://', 'github.com/', 'git@')): remote = path elif not os.path.isdir(path): raise TypeError("You must specify a directory to build in path") From e1a60908f080aefcd3f0d2ecb6262f2e7b5f6863 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 19 Jun 2015 02:12:32 +0200 Subject: [PATCH 37/43] Moved mem_limit and memswap_limit to host_config for API version >= 1.19 --- docker/client.py | 114 ++++++++++++++++++++++++++++++++---------- docker/utils/utils.py | 39 ++++++++++++--- 2 files changed, 120 insertions(+), 33 deletions(-) diff --git a/docker/client.py b/docker/client.py index f467847f6a..294d0d3ade 100644 --- a/docker/client.py +++ b/docker/client.py @@ -23,8 +23,6 @@ import requests import requests.exceptions import six -import websocket - from . import constants from . import errors @@ -35,6 +33,10 @@ from .tls import TLSConfig +if not six.PY3: + import websocket + + class Client(requests.Session): def __init__(self, base_url=None, version=None, timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False): @@ -152,6 +154,9 @@ def _attach_params(self, override=None): @check_resource def _attach_websocket(self, container, params=None): + if six.PY3: + raise NotImplementedError("This method is not currently supported " + "under python 3") url = self._url("/containers/{0}/attach/ws".format(container)) req = requests.Request("POST", url, params=self._attach_params(params)) full_url = req.prepare().url @@ -246,6 +251,8 @@ def api_version(self): @check_resource def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): + if isinstance(container, dict): + container = container.get('Id') params = { 'logs': logs and 1 or 0, 'stdout': stdout and 1 or 0, @@ -290,6 +297,9 @@ def attach_socket(self, container, params=None, ws=False): if ws: return self._attach_websocket(container, params) + if isinstance(container, dict): + container = container.get('Id') + u = self._url("/containers/{0}/attach".format(container)) return self._get_raw_response_socket(self.post( u, None, params=self._attach_params(params), stream=True)) @@ -297,7 +307,8 @@ def attach_socket(self, container, params=None, ws=False): def build(self, path=None, tag=None, quiet=False, fileobj=None, nocache=False, rm=False, stream=False, timeout=None, custom_context=False, encoding=None, pull=False, - forcerm=False, dockerfile=None, container_limits=None): + forcerm=False, dockerfile=None, container_limits=None, + decode=False): remote = context = headers = None container_limits = container_limits or {} if path is None and fileobj is None: @@ -316,7 +327,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, elif fileobj is not None: context = utils.mkbuildcontext(fileobj) elif path.startswith(('http://', 'https://', - 'git://', 'github.com/', 'git@')): + 'git://', 'github.com/')): remote = path elif not os.path.isdir(path): raise TypeError("You must specify a directory to build in path") @@ -342,11 +353,8 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, 'dockerfile was only introduced in API version 1.17' ) - # Docker server 1.6 only supports values 1 and 0 for pull - # parameter. This was later fixed to support a wider range of - # values, including true / false. - # See also https://github.com/docker/docker/issues/13631 - pull = 1 if pull else 0 + if utils.compare_version('1.19', self._version) < 0: + pull = 1 if pull else 0 u = self._url('/build') params = { @@ -394,7 +402,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, context.close() if stream: - return self._stream_helper(response) + return self._stream_helper(response, decode=decode) else: output = self._result(response) srch = r'Successfully built ([0-9a-f]+)' @@ -406,6 +414,8 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, @check_resource def commit(self, container, repository=None, tag=None, message=None, author=None, conf=None): + if isinstance(container, dict): + container = container.get('Id') params = { 'container': container, 'repo': repository, @@ -442,6 +452,8 @@ def containers(self, quiet=False, all=False, trunc=False, latest=False, @check_resource def copy(self, container, resource): + if isinstance(container, dict): + container = container.get('Id') res = self._post_json( self._url("/containers/{0}/copy".format(container)), data={"Resource": resource}, @@ -452,12 +464,12 @@ def copy(self, container, resource): def create_container(self, image, command=None, hostname=None, user=None, detach=False, stdin_open=False, tty=False, - mem_limit=0, ports=None, environment=None, dns=None, - volumes=None, volumes_from=None, + mem_limit=None, ports=None, environment=None, + dns=None, volumes=None, volumes_from=None, network_disabled=False, name=None, entrypoint=None, cpu_shares=None, working_dir=None, domainname=None, - memswap_limit=0, cpuset=None, host_config=None, - mac_address=None, labels=None): + memswap_limit=None, cpuset=None, host_config=None, + mac_address=None, labels=None, volume_driver=None): if isinstance(volumes, six.string_types): volumes = [volumes, ] @@ -471,7 +483,8 @@ def create_container(self, image, command=None, hostname=None, user=None, self._version, image, command, hostname, user, detach, stdin_open, tty, mem_limit, ports, environment, dns, volumes, volumes_from, network_disabled, entrypoint, cpu_shares, working_dir, domainname, - memswap_limit, cpuset, host_config, mac_address, labels + memswap_limit, cpuset, host_config, mac_address, labels, + volume_driver ) return self.create_container_from_config(config, name) @@ -485,6 +498,8 @@ def create_container_from_config(self, config, name=None): @check_resource def diff(self, container): + if isinstance(container, dict): + container = container.get('Id') return self._result(self._get(self._url("/containers/{0}/changes". format(container))), True) @@ -529,6 +544,8 @@ def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False, raise errors.InvalidVersion( 'Privileged exec is not supported in API < 1.19' ) + if isinstance(container, dict): + container = container.get('Id') if isinstance(cmd, six.string_types): cmd = shlex.split(str(cmd)) @@ -593,6 +610,8 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False): @check_resource def export(self, container): + if isinstance(container, dict): + container = container.get('Id') res = self._get(self._url("/containers/{0}/export".format(container)), stream=True) self._raise_for_status(res) @@ -730,12 +749,16 @@ def insert(self, image, url, path): @check_resource def inspect_container(self, container): + if isinstance(container, dict): + container = container.get('Id') return self._result( self._get(self._url("/containers/{0}/json".format(container))), True) @check_resource def inspect_image(self, image): + if isinstance(image, dict): + image = image.get('Id') return self._result( self._get(self._url("/images/{0}/json".format(image))), True @@ -743,6 +766,8 @@ def inspect_image(self, image): @check_resource def kill(self, container, signal=None): + if isinstance(container, dict): + container = container.get('Id') url = self._url("/containers/{0}/kill".format(container)) params = {} if signal is not None: @@ -790,6 +815,8 @@ def login(self, username, password=None, email=None, registry=None, @check_resource def logs(self, container, stdout=True, stderr=True, stream=False, timestamps=False, tail='all'): + if isinstance(container, dict): + container = container.get('Id') if utils.compare_version('1.11', self._version) >= 0: params = {'stderr': stderr and 1 or 0, 'stdout': stdout and 1 or 0, @@ -822,6 +849,8 @@ def logs(self, container, stdout=True, stderr=True, stream=False, @check_resource def pause(self, container): + if isinstance(container, dict): + container = container.get('Id') url = self._url('/containers/{0}/pause'.format(container)) res = self._post(url) self._raise_for_status(res) @@ -831,6 +860,8 @@ def ping(self): @check_resource def port(self, container, private_port): + if isinstance(container, dict): + container = container.get('Id') res = self._get(self._url("/containers/{0}/json".format(container))) self._raise_for_status(res) json_ = res.json() @@ -884,13 +915,17 @@ def pull(self, repository, tag=None, stream=False, else: headers['X-Registry-Auth'] = auth.encode_header(auth_config) - response = self._post(self._url('/images/create'), params=params, - headers=headers, stream=stream, timeout=None) + response = self._post( + self._url('/images/create'), params=params, headers=headers, + stream=stream, timeout=None + ) + + self._raise_for_status(response) if stream: return self._stream_helper(response) - else: - return self._result(response) + + return self._result(response) def push(self, repository, tag=None, stream=False, insecure_registry=False): @@ -918,16 +953,21 @@ def push(self, repository, tag=None, stream=False, if authcfg: headers['X-Registry-Auth'] = auth.encode_header(authcfg) - response = self._post_json(u, None, headers=headers, - stream=stream, params=params) - else: - response = self._post_json(u, None, stream=stream, params=params) + response = self._post_json( + u, None, headers=headers, stream=stream, params=params + ) + + self._raise_for_status(response) - return stream and self._stream_helper(response) \ - or self._result(response) + if stream: + return self._stream_helper(response) + + return self._result(response) @check_resource def remove_container(self, container, v=False, link=False, force=False): + if isinstance(container, dict): + container = container.get('Id') params = {'v': v, 'link': link, 'force': force} res = self._delete(self._url("/containers/" + container), params=params) @@ -935,6 +975,8 @@ def remove_container(self, container, v=False, link=False, force=False): @check_resource def remove_image(self, image, force=False, noprune=False): + if isinstance(image, dict): + image = image.get('Id') params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/" + image), params=params) self._raise_for_status(res) @@ -945,6 +987,8 @@ def rename(self, container, name): raise errors.InvalidVersion( 'rename was only introduced in API version 1.17' ) + if isinstance(container, dict): + container = container.get('Id') url = self._url("/containers/{0}/rename".format(container)) params = {'name': name} res = self._post(url, params=params) @@ -952,6 +996,9 @@ def rename(self, container, name): @check_resource def resize(self, container, height, width): + if isinstance(container, dict): + container = container.get('Id') + params = {'h': height, 'w': width} url = self._url("/containers/{0}/resize".format(container)) res = self._post(url, params=params) @@ -959,6 +1006,8 @@ def resize(self, container, height, width): @check_resource def restart(self, container, timeout=10): + if isinstance(container, dict): + container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/restart".format(container)) res = self._post(url, params=params) @@ -1023,6 +1072,9 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None, ipc_mode=ipc_mode, security_opt=security_opt, ulimits=ulimits ) + if isinstance(container, dict): + container = container.get('Id') + url = self._url("/containers/{0}/start".format(container)) if not start_config: start_config = None @@ -1041,16 +1093,20 @@ def stats(self, container, decode=None): raise errors.InvalidVersion( 'Stats retrieval is not supported in API < 1.17!') + if isinstance(container, dict): + container = container.get('Id') url = self._url("/containers/{0}/stats".format(container)) return self._stream_helper(self._get(url, stream=True), decode=decode) @check_resource def stop(self, container, timeout=10): + if isinstance(container, dict): + container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/stop".format(container)) res = self._post(url, params=params, - timeout=(timeout + (self.timeout or 0))) + timeout=(timeout + self.timeout)) self._raise_for_status(res) @check_resource @@ -1067,6 +1123,8 @@ def tag(self, image, repository, tag=None, force=False): @check_resource def top(self, container): + if isinstance(container, dict): + container = container.get('Id') u = self._url("/containers/{0}/top".format(container)) return self._result(self._get(u), True) @@ -1076,12 +1134,16 @@ def version(self, api_version=True): @check_resource def unpause(self, container): + if isinstance(container, dict): + container = container.get('Id') url = self._url('/containers/{0}/unpause'.format(container)) res = self._post(url) self._raise_for_status(res) @check_resource def wait(self, container, timeout=None): + if isinstance(container, dict): + container = container.get('Id') url = self._url("/containers/{0}/wait".format(container)) res = self._post(url, timeout=timeout) self._raise_for_status(res) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index e4e665f3f4..175a7e0ff6 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -383,10 +383,21 @@ def create_host_config( dns=None, dns_search=None, volumes_from=None, network_mode=None, restart_policy=None, cap_add=None, cap_drop=None, devices=None, extra_hosts=None, read_only=None, pid_mode=None, ipc_mode=None, - security_opt=None, ulimits=None, log_config=None + security_opt=None, ulimits=None, log_config=None, mem_limit=None, + memswap_limit=None ): host_config = {} + if mem_limit is not None: + if isinstance(mem_limit, six.string_types): + mem_limit = parse_bytes(mem_limit) + host_config['Memory'] = mem_limit + + if memswap_limit is not None: + if isinstance(memswap_limit, six.string_types): + memswap_limit = parse_bytes(memswap_limit) + host_config['MemorySwap'] = memswap_limit + if pid_mode not in (None, 'host'): raise errors.DockerException( 'Invalid value for pid param: {0}'.format(pid_mode) @@ -503,10 +514,10 @@ def create_host_config( def create_container_config( version, image, command, hostname=None, user=None, detach=False, - stdin_open=False, tty=False, mem_limit=0, ports=None, environment=None, + stdin_open=False, tty=False, mem_limit=None, ports=None, environment=None, dns=None, volumes=None, volumes_from=None, network_disabled=False, entrypoint=None, cpu_shares=None, working_dir=None, domainname=None, - memswap_limit=0, cpuset=None, host_config=None, mac_address=None, + memswap_limit=None, cpuset=None, host_config=None, mac_address=None, labels=None, volume_driver=None ): if isinstance(command, six.string_types): @@ -522,10 +533,24 @@ def create_container_config( 'labels were only introduced in API version 1.18' ) - if volume_driver is not None and compare_version('1.19', version) < 0: - raise errors.InvalidVersion( - 'Volume drivers were only introduced in API version 1.19' - ) + if compare_version('1.19', version) < 0: + if volume_driver is not None: + raise errors.InvalidVersion( + 'Volume drivers were only introduced in API version 1.19' + ) + mem_limit = mem_limit if mem_limit is not None else 0 + memswap_limit = memswap_limit if memswap_limit is not None else 0 + else: + if mem_limit is not None: + raise errors.InvalidVersion( + 'mem_limit has been moved to host_config in API version 1.19' + ) + + if memswap_limit is not None: + raise errors.InvalidVersion( + 'memswap_limit has been moved to host_config in API ' + 'version 1.19' + ) if isinstance(labels, list): labels = dict((lbl, six.text_type('')) for lbl in labels) From d1c64c701efe92282a9b4a888c313003ff6bf9a2 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 19 Jun 2015 02:13:12 +0200 Subject: [PATCH 38/43] Updated tests for mem_limit changes --- tests/test.py | 109 ++++++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/tests/test.py b/tests/test.py index 40a7e30a3b..2e3b652b0b 100644 --- a/tests/test.py +++ b/tests/test.py @@ -124,11 +124,10 @@ def base_create_payload(self, img='busybox', cmd=None): if not cmd: cmd = ['true'] return {"Tty": False, "Image": img, "Cmd": cmd, - "AttachStdin": False, "Memory": 0, + "AttachStdin": False, "AttachStderr": True, "AttachStdout": True, "StdinOnce": False, "OpenStdin": False, "NetworkDisabled": False, - "MemorySwap": 0 } def test_ctor(self): @@ -337,11 +336,10 @@ def test_create_container(self): self.assertEqual(json.loads(args[1]['data']), json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["true"], - "AttachStdin": false, "Memory": 0, + "AttachStdin": false, "AttachStderr": true, "AttachStdout": true, "StdinOnce": false, - "OpenStdin": false, "NetworkDisabled": false, - "MemorySwap": 0}''')) + "OpenStdin": false, "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -361,12 +359,11 @@ def test_create_container_with_binds(self): json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls", "/mnt"], "AttachStdin": false, - "Volumes": {"/mnt": {}}, "Memory": 0, + "Volumes": {"/mnt": {}}, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, - "NetworkDisabled": false, - "MemorySwap": 0}''')) + "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -386,12 +383,11 @@ def test_create_container_with_volume_string(self): json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls", "/mnt"], "AttachStdin": false, - "Volumes": {"/mnt": {}}, "Memory": 0, + "Volumes": {"/mnt": {}}, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, - "NetworkDisabled": false, - "MemorySwap": 0}''')) + "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -409,7 +405,7 @@ def test_create_container_with_ports(self): json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls"], "AttachStdin": false, - "Memory": 0, "ExposedPorts": { + "ExposedPorts": { "1111/tcp": {}, "2222/udp": {}, "3333/tcp": {} @@ -417,8 +413,7 @@ def test_create_container_with_ports(self): "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, - "NetworkDisabled": false, - "MemorySwap": 0}''')) + "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -436,13 +431,11 @@ def test_create_container_with_entrypoint(self): json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["hello"], "AttachStdin": false, - "Memory": 0, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, "NetworkDisabled": false, - "Entrypoint": "cowsay", - "MemorySwap": 0}''')) + "Entrypoint": "cowsay"}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -460,13 +453,11 @@ def test_create_container_with_cpu_shares(self): json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls"], "AttachStdin": false, - "Memory": 0, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, "NetworkDisabled": false, - "CpuShares": 5, - "MemorySwap": 0}''')) + "CpuShares": 5}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -484,14 +475,12 @@ def test_create_container_with_cpuset(self): json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls"], "AttachStdin": false, - "Memory": 0, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, "NetworkDisabled": false, "Cpuset": "0,1", - "CpusetCpus": "0,1", - "MemorySwap": 0}''')) + "CpusetCpus": "0,1"}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -509,13 +498,11 @@ def test_create_container_with_working_dir(self): json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls"], "AttachStdin": false, - "Memory": 0, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, "NetworkDisabled": false, - "WorkingDir": "/root", - "MemorySwap": 0}''')) + "WorkingDir": "/root"}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -531,11 +518,10 @@ def test_create_container_with_stdin_open(self): self.assertEqual(json.loads(args[1]['data']), json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["true"], - "AttachStdin": true, "Memory": 0, + "AttachStdin": true, "AttachStderr": true, "AttachStdout": true, "StdinOnce": true, - "OpenStdin": true, "NetworkDisabled": false, - "MemorySwap": 0}''')) + "OpenStdin": true, "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -581,78 +567,95 @@ def test_create_named_container(self): self.assertEqual(json.loads(args[1]['data']), json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["true"], - "AttachStdin": false, "Memory": 0, + "AttachStdin": false, "AttachStderr": true, "AttachStdout": true, "StdinOnce": false, - "OpenStdin": false, "NetworkDisabled": false, - "MemorySwap": 0}''')) + "OpenStdin": false, "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) self.assertEqual(args[1]['params'], {'name': 'marisa-kirisame'}) def test_create_container_with_mem_limit_as_int(self): try: - self.client.create_container('busybox', 'true', - mem_limit=128.0) + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit=128.0 + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0) + self.assertEqual(data['HostConfig']['Memory'], 128.0) def test_create_container_with_mem_limit_as_string(self): try: - self.client.create_container('busybox', 'true', - mem_limit='128') + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit='128' + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0) + self.assertEqual(data['HostConfig']['Memory'], 128.0) def test_create_container_with_mem_limit_as_string_with_k_unit(self): try: - self.client.create_container('busybox', 'true', - mem_limit='128k') + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit='128k' + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0 * 1024) + self.assertEqual(data['HostConfig']['Memory'], 128.0 * 1024) def test_create_container_with_mem_limit_as_string_with_m_unit(self): try: - self.client.create_container('busybox', 'true', - mem_limit='128m') + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit='128m' + ) + ) + except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0 * 1024 * 1024) + self.assertEqual(data['HostConfig']['Memory'], 128.0 * 1024 * 1024) def test_create_container_with_mem_limit_as_string_with_g_unit(self): try: - self.client.create_container('busybox', 'true', - mem_limit='128g') + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit='128g' + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0 * 1024 * 1024 * 1024) + self.assertEqual( + data['HostConfig']['Memory'], 128.0 * 1024 * 1024 * 1024 + ) def test_create_container_with_mem_limit_as_string_with_wrong_value(self): - self.assertRaises(docker.errors.DockerException, - self.client.create_container, - 'busybox', 'true', mem_limit='128p') + self.assertRaises( + docker.errors.DockerException, create_host_config, mem_limit='128p' + ) - self.assertRaises(docker.errors.DockerException, - self.client.create_container, - 'busybox', 'true', mem_limit='1f28') + self.assertRaises( + docker.errors.DockerException, create_host_config, mem_limit='1f28' + ) def test_start_container(self): try: From c6a7ef1fcaaf76bc7a758e9ccc6173e0a1be67fc Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 24 Jun 2015 22:29:11 +0200 Subject: [PATCH 39/43] Fix Unix socket adapter bug with double slash in path + regression test --- docker/client.py | 15 ++++++++++++--- tests/integration_test.py | 9 +++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docker/client.py b/docker/client.py index 294d0d3ade..e59c14d9e7 100644 --- a/docker/client.py +++ b/docker/client.py @@ -54,15 +54,16 @@ def __init__(self, base_url=None, version=None, base_url = utils.parse_host(base_url) if base_url.startswith('http+unix://'): - unix_socket_adapter = unixconn.UnixAdapter(base_url, timeout) - self.mount('http+docker://', unix_socket_adapter) + self._adapter = unixconn.UnixAdapter(base_url, timeout) + self.mount('http+docker://', self._adapter) self.base_url = 'http+docker://localunixsocket' else: # 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._adapter = ssladapter.SSLAdapter() + self.mount('https://', self._adapter) self.base_url = base_url # version detection needs to be after unix adapter mounting @@ -248,6 +249,14 @@ def _multiplexed_response_stream_helper(self, response): def api_version(self): return self._version + def get_adapter(self, url): + try: + return super(Client, self).get_adapter(url) + except requests.exceptions.InvalidSchema as e: + if self._adapter: + return self._adapter + raise e + @check_resource def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): diff --git a/tests/integration_test.py b/tests/integration_test.py index 4b9869e28a..ac4a871753 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -872,8 +872,8 @@ def runTest(self): id = container['Id'] self.client.start(id) self.tmp_containers.append(id) - socket = self.client.attach_socket(container, ws=False) - self.assertTrue(socket.fileno() > -1) + sock = self.client.attach_socket(container, ws=False) + self.assertTrue(sock.fileno() > -1) class TestPauseUnpauseContainer(BaseTestCase): @@ -1467,12 +1467,17 @@ def test_542(self): result = self.client.containers(all=True, trunc=True) self.assertEqual(len(result[0]['Id']), 12) + def test_647(self): + with self.assertRaises(docker.errors.APIError): + self.client.inspect_image('gensokyo.jp//kirisame') + def test_649(self): self.client.timeout = None ctnr = self.client.create_container('busybox', ['sleep', '2']) self.client.start(ctnr) self.client.stop(ctnr) + if __name__ == '__main__': c = docker.Client(base_url=DEFAULT_BASE_URL) c.pull('busybox') From f2160f8a5fdf980a4a5bd6a9ec5f948500b515af Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 24 Jun 2015 22:29:11 +0200 Subject: [PATCH 40/43] Fix adapter bug + regression test --- tests/integration_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index ac4a871753..2a639e2c3d 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1477,7 +1477,6 @@ def test_649(self): self.client.start(ctnr) self.client.stop(ctnr) - if __name__ == '__main__': c = docker.Client(base_url=DEFAULT_BASE_URL) c.pull('busybox') From 445ac86e8e9f3d6e0bf5d23e86715ebca28986e4 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 24 Jun 2015 22:49:16 +0200 Subject: [PATCH 41/43] ClientBase class to extract utility methods and constructor and sanitize Client class --- docker/client.py | 292 +------------------------------------- docker/clientbase.py | 235 ++++++++++++++++++++++++++++++ tests/integration_test.py | 1 + 3 files changed, 243 insertions(+), 285 deletions(-) create mode 100644 docker/clientbase.py diff --git a/docker/client.py b/docker/client.py index e59c14d9e7..bb12e000ac 100644 --- a/docker/client.py +++ b/docker/client.py @@ -12,256 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import os import re import shlex -import struct import warnings from datetime import datetime -import requests -import requests.exceptions import six +from . import clientbase from . import constants from . import errors from .auth import auth -from .unixconn import unixconn -from .ssladapter import ssladapter from .utils import utils, check_resource -from .tls import TLSConfig -if not six.PY3: - import websocket - - -class Client(requests.Session): - def __init__(self, base_url=None, version=None, - timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False): - super(Client, self).__init__() - - if tls and not base_url.startswith('https://'): - raise errors.TLSParameterError( - 'If using TLS, the base_url argument must begin with ' - '"https://".') - - self.base_url = base_url - self.timeout = timeout - - self._auth_configs = auth.load_config() - - base_url = utils.parse_host(base_url) - if base_url.startswith('http+unix://'): - self._adapter = unixconn.UnixAdapter(base_url, timeout) - self.mount('http+docker://', self._adapter) - self.base_url = 'http+docker://localunixsocket' - else: - # Use SSLAdapter for the ability to specify SSL version - if isinstance(tls, TLSConfig): - tls.configure_client(self) - elif tls: - self._adapter = ssladapter.SSLAdapter() - self.mount('https://', self._adapter) - self.base_url = base_url - - # version detection needs to be after unix adapter mounting - if version is None: - self._version = constants.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.""" - kwargs.setdefault('timeout', self.timeout) - return kwargs - - def _post(self, url, **kwargs): - return self.post(url, **self._set_request_timeout(kwargs)) - - def _get(self, url, **kwargs): - return self.get(url, **self._set_request_timeout(kwargs)) - - def _delete(self, url, **kwargs): - return self.delete(url, **self._set_request_timeout(kwargs)) - - 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.""" - try: - response.raise_for_status() - except requests.exceptions.HTTPError as e: - raise errors.APIError(e, response, explanation=explanation) - - def _result(self, response, json=False, binary=False): - assert not (json and binary) - self._raise_for_status(response) - - if json: - return response.json() - if binary: - return response.content - return response.text - - def _post_json(self, url, data, **kwargs): - # Go <1.1 can't unserialize null to a string - # so we do this disgusting thing here. - data2 = {} - if data is not None: - for k, v in six.iteritems(data): - if v is not None: - data2[k] = v - - if 'headers' not in kwargs: - kwargs['headers'] = {} - kwargs['headers']['Content-Type'] = 'application/json' - return self._post(url, data=json.dumps(data2), **kwargs) - - def _attach_params(self, override=None): - return override or { - 'stdout': 1, - 'stderr': 1, - 'stream': 1 - } - - @check_resource - def _attach_websocket(self, container, params=None): - if six.PY3: - raise NotImplementedError("This method is not currently supported " - "under python 3") - url = self._url("/containers/{0}/attach/ws".format(container)) - req = requests.Request("POST", url, params=self._attach_params(params)) - full_url = req.prepare().url - full_url = full_url.replace("http://", "ws://", 1) - full_url = full_url.replace("https://", "wss://", 1) - return self._create_websocket_connection(full_url) - - def _create_websocket_connection(self, url): - return websocket.create_connection(url) - - def _get_raw_response_socket(self, response): - self._raise_for_status(response) - if six.PY3: - sock = response.raw._fp.fp.raw - else: - sock = response.raw._fp.fp._sock - try: - # Keep a reference to the response to stop it being garbage - # collected. If the response is garbage collected, it will - # close TLS sockets. - sock._response = response - except AttributeError: - # UNIX sockets can't have attributes set on them, but that's - # fine because we won't be doing TLS over them - pass - - return sock - - def _stream_helper(self, response, decode=False): - """Generator for data coming from a chunked-encoded HTTP response.""" - if response.raw._fp.chunked: - reader = response.raw - while not reader.closed: - # this read call will block until we get a chunk - data = reader.read(1) - if not data: - break - if reader._fp.chunk_left: - data += reader.read(reader._fp.chunk_left) - if decode: - if six.PY3: - data = data.decode('utf-8') - data = json.loads(data) - yield data - else: - # Response isn't chunked, meaning we probably - # encountered an error immediately - yield self._result(response) - - def _multiplexed_buffer_helper(self, response): - """A generator of multiplexed data blocks read from a buffered - response.""" - buf = self._result(response, binary=True) - walker = 0 - while True: - if len(buf[walker:]) < 8: - break - _, length = struct.unpack_from('>BxxxL', buf[walker:]) - start = walker + constants.STREAM_HEADER_SIZE_BYTES - end = start + length - walker = end - yield buf[start:end] - - def _multiplexed_response_stream_helper(self, response): - """A generator of multiplexed data blocks coming from a response - stream.""" - - # Disable timeout on the underlying socket to prevent - # Read timed out(s) for long running processes - socket = self._get_raw_response_socket(response) - if six.PY3: - socket._sock.settimeout(None) - else: - socket.settimeout(None) - - while True: - header = response.raw.read(constants.STREAM_HEADER_SIZE_BYTES) - if not header: - break - _, length = struct.unpack('>BxxxL', header) - if not length: - break - data = response.raw.read(length) - if not data: - break - yield data - - @property - def api_version(self): - return self._version - - def get_adapter(self, url): - try: - return super(Client, self).get_adapter(url) - except requests.exceptions.InvalidSchema as e: - if self._adapter: - return self._adapter - raise e - +class Client(clientbase.ClientBase): @check_resource def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): - if isinstance(container, dict): - container = container.get('Id') params = { 'logs': logs and 1 or 0, 'stdout': stdout and 1 or 0, @@ -306,9 +75,6 @@ def attach_socket(self, container, params=None, ws=False): if ws: return self._attach_websocket(container, params) - if isinstance(container, dict): - container = container.get('Id') - u = self._url("/containers/{0}/attach".format(container)) return self._get_raw_response_socket(self.post( u, None, params=self._attach_params(params), stream=True)) @@ -336,7 +102,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, elif fileobj is not None: context = utils.mkbuildcontext(fileobj) elif path.startswith(('http://', 'https://', - 'git://', 'github.com/')): + 'git://', 'github.com/', 'git@')): remote = path elif not os.path.isdir(path): raise TypeError("You must specify a directory to build in path") @@ -423,8 +189,6 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, @check_resource def commit(self, container, repository=None, tag=None, message=None, author=None, conf=None): - if isinstance(container, dict): - container = container.get('Id') params = { 'container': container, 'repo': repository, @@ -461,8 +225,6 @@ def containers(self, quiet=False, all=False, trunc=False, latest=False, @check_resource def copy(self, container, resource): - if isinstance(container, dict): - container = container.get('Id') res = self._post_json( self._url("/containers/{0}/copy".format(container)), data={"Resource": resource}, @@ -507,8 +269,6 @@ def create_container_from_config(self, config, name=None): @check_resource def diff(self, container): - if isinstance(container, dict): - container = container.get('Id') return self._result(self._get(self._url("/containers/{0}/changes". format(container))), True) @@ -553,8 +313,6 @@ def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False, raise errors.InvalidVersion( 'Privileged exec is not supported in API < 1.19' ) - if isinstance(container, dict): - container = container.get('Id') if isinstance(cmd, six.string_types): cmd = shlex.split(str(cmd)) @@ -619,8 +377,6 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False): @check_resource def export(self, container): - if isinstance(container, dict): - container = container.get('Id') res = self._get(self._url("/containers/{0}/export".format(container)), stream=True) self._raise_for_status(res) @@ -758,25 +514,21 @@ def insert(self, image, url, path): @check_resource def inspect_container(self, container): - if isinstance(container, dict): - container = container.get('Id') return self._result( self._get(self._url("/containers/{0}/json".format(container))), True) @check_resource def inspect_image(self, image): - if isinstance(image, dict): - image = image.get('Id') return self._result( - self._get(self._url("/images/{0}/json".format(image))), + self._get( + self._url("/images/{0}/json".format(image.replace('/', '%2F'))) + ), True ) @check_resource def kill(self, container, signal=None): - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/kill".format(container)) params = {} if signal is not None: @@ -824,8 +576,6 @@ def login(self, username, password=None, email=None, registry=None, @check_resource def logs(self, container, stdout=True, stderr=True, stream=False, timestamps=False, tail='all'): - if isinstance(container, dict): - container = container.get('Id') if utils.compare_version('1.11', self._version) >= 0: params = {'stderr': stderr and 1 or 0, 'stdout': stdout and 1 or 0, @@ -858,8 +608,6 @@ def logs(self, container, stdout=True, stderr=True, stream=False, @check_resource def pause(self, container): - if isinstance(container, dict): - container = container.get('Id') url = self._url('/containers/{0}/pause'.format(container)) res = self._post(url) self._raise_for_status(res) @@ -869,8 +617,6 @@ def ping(self): @check_resource def port(self, container, private_port): - if isinstance(container, dict): - container = container.get('Id') res = self._get(self._url("/containers/{0}/json".format(container))) self._raise_for_status(res) json_ = res.json() @@ -975,8 +721,6 @@ def push(self, repository, tag=None, stream=False, @check_resource def remove_container(self, container, v=False, link=False, force=False): - if isinstance(container, dict): - container = container.get('Id') params = {'v': v, 'link': link, 'force': force} res = self._delete(self._url("/containers/" + container), params=params) @@ -984,8 +728,6 @@ def remove_container(self, container, v=False, link=False, force=False): @check_resource def remove_image(self, image, force=False, noprune=False): - if isinstance(image, dict): - image = image.get('Id') params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/" + image), params=params) self._raise_for_status(res) @@ -996,8 +738,6 @@ def rename(self, container, name): raise errors.InvalidVersion( 'rename was only introduced in API version 1.17' ) - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/rename".format(container)) params = {'name': name} res = self._post(url, params=params) @@ -1005,9 +745,6 @@ def rename(self, container, name): @check_resource def resize(self, container, height, width): - if isinstance(container, dict): - container = container.get('Id') - params = {'h': height, 'w': width} url = self._url("/containers/{0}/resize".format(container)) res = self._post(url, params=params) @@ -1015,8 +752,6 @@ def resize(self, container, height, width): @check_resource def restart(self, container, timeout=10): - if isinstance(container, dict): - container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/restart".format(container)) res = self._post(url, params=params) @@ -1081,9 +816,6 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None, ipc_mode=ipc_mode, security_opt=security_opt, ulimits=ulimits ) - if isinstance(container, dict): - container = container.get('Id') - url = self._url("/containers/{0}/start".format(container)) if not start_config: start_config = None @@ -1102,20 +834,16 @@ def stats(self, container, decode=None): raise errors.InvalidVersion( 'Stats retrieval is not supported in API < 1.17!') - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/stats".format(container)) return self._stream_helper(self._get(url, stream=True), decode=decode) @check_resource def stop(self, container, timeout=10): - if isinstance(container, dict): - container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/stop".format(container)) res = self._post(url, params=params, - timeout=(timeout + self.timeout)) + timeout=(timeout + (self.timeout or 0))) self._raise_for_status(res) @check_resource @@ -1132,8 +860,6 @@ def tag(self, image, repository, tag=None, force=False): @check_resource def top(self, container): - if isinstance(container, dict): - container = container.get('Id') u = self._url("/containers/{0}/top".format(container)) return self._result(self._get(u), True) @@ -1143,16 +869,12 @@ def version(self, api_version=True): @check_resource def unpause(self, container): - if isinstance(container, dict): - container = container.get('Id') url = self._url('/containers/{0}/unpause'.format(container)) res = self._post(url) self._raise_for_status(res) @check_resource def wait(self, container, timeout=None): - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/wait".format(container)) res = self._post(url, timeout=timeout) self._raise_for_status(res) diff --git a/docker/clientbase.py b/docker/clientbase.py new file mode 100644 index 0000000000..e51bf3ec84 --- /dev/null +++ b/docker/clientbase.py @@ -0,0 +1,235 @@ +import json +import struct + +import requests +import requests.exceptions +import six +import websocket + + +from . import constants +from . import errors +from .auth import auth +from .unixconn import unixconn +from .ssladapter import ssladapter +from .utils import utils, check_resource +from .tls import TLSConfig + + +class ClientBase(requests.Session): + def __init__(self, base_url=None, version=None, + timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False): + super(ClientBase, self).__init__() + + if tls and not base_url.startswith('https://'): + raise errors.TLSParameterError( + 'If using TLS, the base_url argument must begin with ' + '"https://".') + + self.base_url = base_url + self.timeout = timeout + + self._auth_configs = auth.load_config() + + base_url = utils.parse_host(base_url) + if base_url.startswith('http+unix://'): + self._custom_adapter = unixconn.UnixAdapter(base_url, timeout) + self.mount('http+docker://', self._custom_adapter) + self.base_url = 'http+docker://localunixsocket' + else: + # Use SSLAdapter for the ability to specify SSL version + if isinstance(tls, TLSConfig): + tls.configure_client(self) + elif tls: + self._custom_adapter = ssladapter.SSLAdapter() + self.mount('https://', self._custom_adapter) + self.base_url = base_url + + # version detection needs to be after unix adapter mounting + if version is None: + self._version = constants.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.""" + kwargs.setdefault('timeout', self.timeout) + return kwargs + + def _post(self, url, **kwargs): + return self.post(url, **self._set_request_timeout(kwargs)) + + def _get(self, url, **kwargs): + return self.get(url, **self._set_request_timeout(kwargs)) + + def _delete(self, url, **kwargs): + return self.delete(url, **self._set_request_timeout(kwargs)) + + 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.""" + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + raise errors.APIError(e, response, explanation=explanation) + + def _result(self, response, json=False, binary=False): + assert not (json and binary) + self._raise_for_status(response) + + if json: + return response.json() + if binary: + return response.content + return response.text + + def _post_json(self, url, data, **kwargs): + # Go <1.1 can't unserialize null to a string + # so we do this disgusting thing here. + data2 = {} + if data is not None: + for k, v in six.iteritems(data): + if v is not None: + data2[k] = v + + if 'headers' not in kwargs: + kwargs['headers'] = {} + kwargs['headers']['Content-Type'] = 'application/json' + return self._post(url, data=json.dumps(data2), **kwargs) + + def _attach_params(self, override=None): + return override or { + 'stdout': 1, + 'stderr': 1, + 'stream': 1 + } + + @check_resource + def _attach_websocket(self, container, params=None): + url = self._url("/containers/{0}/attach/ws".format(container)) + req = requests.Request("POST", url, params=self._attach_params(params)) + full_url = req.prepare().url + full_url = full_url.replace("http://", "ws://", 1) + full_url = full_url.replace("https://", "wss://", 1) + return self._create_websocket_connection(full_url) + + def _create_websocket_connection(self, url): + return websocket.create_connection(url) + + def _get_raw_response_socket(self, response): + self._raise_for_status(response) + if six.PY3: + sock = response.raw._fp.fp.raw + else: + sock = response.raw._fp.fp._sock + try: + # Keep a reference to the response to stop it being garbage + # collected. If the response is garbage collected, it will + # close TLS sockets. + sock._response = response + except AttributeError: + # UNIX sockets can't have attributes set on them, but that's + # fine because we won't be doing TLS over them + pass + + return sock + + def _stream_helper(self, response, decode=False): + """Generator for data coming from a chunked-encoded HTTP response.""" + if response.raw._fp.chunked: + reader = response.raw + while not reader.closed: + # this read call will block until we get a chunk + data = reader.read(1) + if not data: + break + if reader._fp.chunk_left: + data += reader.read(reader._fp.chunk_left) + if decode: + if six.PY3: + data = data.decode('utf-8') + data = json.loads(data) + yield data + else: + # Response isn't chunked, meaning we probably + # encountered an error immediately + yield self._result(response) + + def _multiplexed_buffer_helper(self, response): + """A generator of multiplexed data blocks read from a buffered + response.""" + buf = self._result(response, binary=True) + walker = 0 + while True: + if len(buf[walker:]) < 8: + break + _, length = struct.unpack_from('>BxxxL', buf[walker:]) + start = walker + constants.STREAM_HEADER_SIZE_BYTES + end = start + length + walker = end + yield buf[start:end] + + def _multiplexed_response_stream_helper(self, response): + """A generator of multiplexed data blocks coming from a response + stream.""" + + # Disable timeout on the underlying socket to prevent + # Read timed out(s) for long running processes + socket = self._get_raw_response_socket(response) + if six.PY3: + socket._sock.settimeout(None) + else: + socket.settimeout(None) + + while True: + header = response.raw.read(constants.STREAM_HEADER_SIZE_BYTES) + if not header: + break + _, length = struct.unpack('>BxxxL', header) + if not length: + break + data = response.raw.read(length) + if not data: + break + yield data + + def get_adapter(self, url): + try: + return super(ClientBase, self).get_adapter(url) + except requests.exceptions.InvalidSchema as e: + if self._custom_adapter: + return self._custom_adapter + else: + raise e + + @property + def api_version(self): + return self._version diff --git a/tests/integration_test.py b/tests/integration_test.py index 2a639e2c3d..ac4a871753 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1477,6 +1477,7 @@ def test_649(self): self.client.start(ctnr) self.client.stop(ctnr) + if __name__ == '__main__': c = docker.Client(base_url=DEFAULT_BASE_URL) c.pull('busybox') From a036e1c43b4b662052f22f548293fd3c7d220511 Mon Sep 17 00:00:00 2001 From: Peter Kowalczyk Date: Thu, 2 Jul 2015 10:42:08 -0500 Subject: [PATCH 42/43] Fix missing apostrophes in docs --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 4b64147155..5a3b3222a6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -184,7 +184,7 @@ information on how to create port bindings and volume mappings. The `mem_limit` variable accepts float values (which represent the memory limit of the created container in bytes) or a string with a units identification char -('100000b', 1000k', 128m', '1g'). If a string is specified without a units +('100000b', '1000k', '128m', '1g'). If a string is specified without a units character, bytes are assumed as an intended unit. `volumes_from` and `dns` arguments raise [TypeError]( From 27a158a085e992201e6537517bb0b22912089329 Mon Sep 17 00:00:00 2001 From: Dan O'Reilly Date: Sun, 7 Jun 2015 15:47:22 -0400 Subject: [PATCH 43/43] Fix handling output from tty-enabled containers. Treat output from TTY-enabled containers as raw streams, rather than as multiplexed streams. The docker API docs specify that tty-enabled containers don't multiplex. Also update tests to pass with these changes, and changed the code used to read raw streams to not read line-by-line, and to not skip empty lines. Addresses issue #630 Signed-off-by: Dan O'Reilly --- docker/client.py | 46 +++----------------------------------------- docker/clientbase.py | 40 ++++++++++++++++++++++++++++++++++++++ tests/fake_api.py | 4 ++-- tests/test.py | 46 ++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 87 insertions(+), 49 deletions(-) diff --git a/docker/client.py b/docker/client.py index bb12e000ac..17b7da101c 100644 --- a/docker/client.py +++ b/docker/client.py @@ -40,28 +40,7 @@ def attach(self, container, stdout=True, stderr=True, u = self._url("/containers/{0}/attach".format(container)) response = self._post(u, params=params, stream=stream) - # Stream multi-plexing was only introduced in API v1.6. Anything before - # that needs old-style streaming. - if utils.compare_version('1.6', self._version) < 0: - def stream_result(): - self._raise_for_status(response) - for line in response.iter_lines(chunk_size=1, - decode_unicode=True): - # filter out keep-alive new lines - if line: - yield line - - return stream_result() if stream else \ - self._result(response, binary=True) - - sep = bytes() if six.PY3 else str() - - if stream: - return self._multiplexed_response_stream_helper(response) - else: - return sep.join( - [x for x in self._multiplexed_buffer_helper(response)] - ) + return self._get_result(container, stream, response) @check_resource def attach_socket(self, container, params=None, ws=False): @@ -363,17 +342,7 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False): res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)), data=data, stream=stream) - self._raise_for_status(res) - if stream: - return self._multiplexed_response_stream_helper(res) - elif six.PY3: - return bytes().join( - [x for x in self._multiplexed_buffer_helper(res)] - ) - else: - return str().join( - [x for x in self._multiplexed_buffer_helper(res)] - ) + return self._get_result_tty(stream, res, tty) @check_resource def export(self, container): @@ -588,16 +557,7 @@ def logs(self, container, stdout=True, stderr=True, stream=False, params['tail'] = tail url = self._url("/containers/{0}/logs".format(container)) res = self._get(url, params=params, stream=stream) - if stream: - return self._multiplexed_response_stream_helper(res) - elif six.PY3: - return bytes().join( - [x for x in self._multiplexed_buffer_helper(res)] - ) - else: - return str().join( - [x for x in self._multiplexed_buffer_helper(res)] - ) + return self._get_result(container, stream, res) return self.attach( container, stdout=stdout, diff --git a/docker/clientbase.py b/docker/clientbase.py index e51bf3ec84..c1ae8137a2 100644 --- a/docker/clientbase.py +++ b/docker/clientbase.py @@ -221,6 +221,46 @@ def _multiplexed_response_stream_helper(self, response): break yield data + def _stream_raw_result_old(self, response): + ''' Stream raw output for API versions below 1.6 ''' + self._raise_for_status(response) + for line in response.iter_lines(chunk_size=1, + decode_unicode=True): + # filter out keep-alive new lines + if line: + yield line + + def _stream_raw_result(self, response): + ''' Stream result for TTY-enabled container above API 1.6 ''' + self._raise_for_status(response) + for out in response.iter_content(chunk_size=1, decode_unicode=True): + yield out + + def _get_result(self, container, stream, res): + cont = self.inspect_container(container) + return self._get_result_tty(stream, res, cont['Config']['Tty']) + + def _get_result_tty(self, stream, res, is_tty): + # Stream multi-plexing was only introduced in API v1.6. Anything + # before that needs old-style streaming. + if utils.compare_version('1.6', self._version) < 0: + return self._stream_raw_result_old(res) + + # We should also use raw streaming (without keep-alives) + # if we're dealing with a tty-enabled container. + if is_tty: + return self._stream_raw_result(res) if stream else \ + self._result(res, binary=True) + + self._raise_for_status(res) + sep = six.binary_type() + if stream: + return self._multiplexed_response_stream_helper(res) + else: + return sep.join( + [x for x in self._multiplexed_buffer_helper(res)] + ) + def get_adapter(self, url): try: return super(ClientBase, self).get_adapter(url) diff --git a/tests/fake_api.py b/tests/fake_api.py index d201838e71..199b4f6498 100644 --- a/tests/fake_api.py +++ b/tests/fake_api.py @@ -129,11 +129,11 @@ def post_fake_create_container(): return status_code, response -def get_fake_inspect_container(): +def get_fake_inspect_container(tty=False): status_code = 200 response = { 'Id': FAKE_CONTAINER_ID, - 'Config': {'Privileged': True}, + 'Config': {'Privileged': True, 'Tty': tty}, 'ID': FAKE_CONTAINER_ID, 'Image': 'busybox:latest', "State": { diff --git a/tests/test.py b/tests/test.py index 2e3b652b0b..f6535b2e23 100644 --- a/tests/test.py +++ b/tests/test.py @@ -69,6 +69,14 @@ def fake_resolve_authconfig(authconfig, registry=None): return None +def fake_inspect_container(self, container, tty=False): + return fake_api.get_fake_inspect_container(tty=tty)[1] + + +def fake_inspect_container_tty(self, container): + return fake_inspect_container(self, container, tty=True) + + def fake_resp(url, data=None, **kwargs): status_code, content = fake_api.fake_responses[url]() return response(status_code=status_code, content=content) @@ -1546,7 +1554,9 @@ def test_url_compatibility_tcp(self): def test_logs(self): try: - logs = self.client.logs(fake_api.FAKE_CONTAINER_ID) + with mock.patch('docker.Client.inspect_container', + fake_inspect_container): + logs = self.client.logs(fake_api.FAKE_CONTAINER_ID) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -1565,7 +1575,9 @@ def test_logs(self): def test_logs_with_dict_instead_of_id(self): try: - logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID}) + with mock.patch('docker.Client.inspect_container', + fake_inspect_container): + logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID}) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -1584,7 +1596,9 @@ def test_logs_with_dict_instead_of_id(self): def test_log_streaming(self): try: - self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True) + with mock.patch('docker.Client.inspect_container', + fake_inspect_container): + self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -1598,7 +1612,10 @@ def test_log_streaming(self): def test_log_tail(self): try: - self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False, tail=10) + with mock.patch('docker.Client.inspect_container', + fake_inspect_container): + self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False, + tail=10) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -1610,6 +1627,27 @@ def test_log_tail(self): stream=False ) + def test_log_tty(self): + try: + m = mock.Mock() + with mock.patch('docker.Client.inspect_container', + fake_inspect_container_tty): + with mock.patch('docker.Client._stream_raw_result', + m): + self.client.logs(fake_api.FAKE_CONTAINER_ID, + stream=True) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + self.assertTrue(m.called) + fake_request.assert_called_with( + url_prefix + 'containers/3cc2351ab11b/logs', + params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1, + 'tail': 'all'}, + timeout=DEFAULT_TIMEOUT_SECONDS, + stream=True + ) + def test_diff(self): try: self.client.diff(fake_api.FAKE_CONTAINER_ID)