From 50a60717f064f77974807cba7f9defd8f4e1cf4e Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 28 Jun 2017 12:06:03 -0700 Subject: [PATCH 01/32] split_port should not break when passed a non-string argument Signed-off-by: Joffrey F --- docker/utils/ports.py | 1 + tests/unit/utils_test.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/docker/utils/ports.py b/docker/utils/ports.py index 57332dee..8f713c72 100644 --- a/docker/utils/ports.py +++ b/docker/utils/ports.py @@ -54,6 +54,7 @@ def port_range(start, end, proto, randomly_available_port=False): def split_port(port): + port = str(port) match = PORT_SPEC.match(port) if match is None: _raise_invalid_port(port) diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index c25881d1..a2d463d7 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -587,6 +587,9 @@ class PortsTest(unittest.TestCase): def test_split_port_empty_string(self): self.assertRaises(ValueError, lambda: split_port("")) + def test_split_port_non_string(self): + assert split_port(1243) == (['1243'], None) + def test_build_port_bindings_with_one_port(self): port_bindings = build_port_bindings(["127.0.0.1:1000:1000"]) self.assertEqual(port_bindings["1000"], [("127.0.0.1", "1000")]) From 3d84dbee59ba289d3f55455dbe38b929c677e687 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 28 Jun 2017 12:25:10 -0700 Subject: [PATCH 02/32] Bump 2.4.1 Signed-off-by: Joffrey F --- docker/version.py | 2 +- docs/change-log.md | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docker/version.py b/docker/version.py index 8f40f467..7953c904 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.4.0" +version = "2.4.1" 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 194f7347..4ccb617d 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,16 @@ Change log ========== +2.4.1 +----- + +[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/35?closed=1) + +### Bugfixes + +* Fixed a bug where the `split_port` utility would raise an exception when + passed a non-string argument. + 2.4.0 ----- From 14e61848146c45c4892417833c8a7e98f48e864e Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 28 Jun 2017 13:26:04 -0700 Subject: [PATCH 03/32] Compose 1.14.0 hack Signed-off-by: Joffrey F --- docker/utils/ports.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker/utils/ports.py b/docker/utils/ports.py index 8f713c72..bf7d6972 100644 --- a/docker/utils/ports.py +++ b/docker/utils/ports.py @@ -54,6 +54,11 @@ def port_range(start, end, proto, randomly_available_port=False): def split_port(port): + if hasattr(port, 'legacy_repr'): + # This is the worst hack, but it prevents a bug in Compose 1.14.0 + # https://github.com/docker/docker-py/issues/1668 + # TODO: remove once fixed in Compose stable + port = port.legacy_repr() port = str(port) match = PORT_SPEC.match(port) if match is None: From 43f87e9f63a99c56f05afbf28e3a973151cacee5 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 28 Jun 2017 13:32:18 -0700 Subject: [PATCH 04/32] Bump 2.4.2 Signed-off-by: Joffrey F --- docker/version.py | 2 +- docs/change-log.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/version.py b/docker/version.py index 7953c904..af1bd5b5 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.4.1" +version = "2.4.2" 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 4ccb617d..7099d794 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,10 +1,10 @@ Change log ========== -2.4.1 +2.4.2 ----- -[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/35?closed=1) +[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/36?closed=1) ### Bugfixes From 2b128077c1fffc0d9f308dc31543bb602b599282 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 22 Jun 2017 16:42:15 -0700 Subject: [PATCH 05/32] Shift test matrix forward Signed-off-by: Joffrey F --- Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 987df7af..35792788 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,7 +7,7 @@ def images = [:] // Note: Swarm in dind seem notoriously flimsy with 1.12.1+, which is why we're // sticking with 1.12.0 for the 1.12 series -def dockerVersions = ["1.12.0", "1.13.1", "17.04.0-ce", "17.05.0-ce"] +def dockerVersions = ["1.13.1", "17.04.0-ce", "17.05.0-ce", "17.06.0-ce-rc5"] def buildImage = { name, buildargs, pyTag -> img = docker.image(name) @@ -35,7 +35,7 @@ def buildImages = { -> } def getAPIVersion = { engineVersion -> - def versionMap = ['1.12.': '1.24', '1.13.': '1.26', '17.04': '1.27', '17.05': '1.29'] + def versionMap = ['1.13.': '1.26', '17.04': '1.27', '17.05': '1.29', '17.06': '1.30'] return versionMap[engineVersion.substring(0, 5)] } @@ -63,7 +63,7 @@ def runTests = { Map settings -> def testContainerName = "dpy-tests-\$BUILD_NUMBER-\$EXECUTOR_NUMBER-${pythonVersion}-${dockerVersion}" try { sh """docker run -d --name ${dindContainerName} -v /tmp --privileged \\ - dockerswarm/dind:${dockerVersion} docker daemon -H tcp://0.0.0.0:2375 + dockerswarm/dind:${dockerVersion} dockerd -H tcp://0.0.0.0:2375 """ sh """docker run \\ --name ${testContainerName} --volumes-from ${dindContainerName} \\ From e0c7e4d60e1e76cdfceeb782bbcc91d87d2a5d0d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 27 Jun 2017 18:06:12 -0700 Subject: [PATCH 06/32] dev version Signed-off-by: Joffrey F --- docker/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/version.py b/docker/version.py index af1bd5b5..a7452d4f 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.4.2" +version = "2.5.0-dev" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) From a23cd3d8e874635718e93b121dec58a08c8f766c Mon Sep 17 00:00:00 2001 From: Matthew Berry Date: Thu, 13 Jul 2017 23:20:24 -0500 Subject: [PATCH 07/32] Fix #1673 check resource error in container network API Container network functions checked 'image' as resource ID and not 'container'. This caused a traceback when using container as named argument. Signed-off-by: Matthew Berry --- docker/api/network.py | 4 ++-- tests/unit/api_network_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/api/network.py b/docker/api/network.py index 5ebb41af..5549bf0c 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -200,7 +200,7 @@ class NetworkApiMixin(object): res = self._get(url, params=params) return self._result(res, json=True) - @check_resource('image') + @check_resource('container') @minimum_version('1.21') def connect_container_to_network(self, container, net_id, ipv4_address=None, ipv6_address=None, @@ -237,7 +237,7 @@ class NetworkApiMixin(object): res = self._post_json(url, data=data) self._raise_for_status(res) - @check_resource('image') + @check_resource('container') @minimum_version('1.21') def disconnect_container_from_network(self, container, net_id, force=False): diff --git a/tests/unit/api_network_test.py b/tests/unit/api_network_test.py index f997a1b8..96cdc4b1 100644 --- a/tests/unit/api_network_test.py +++ b/tests/unit/api_network_test.py @@ -147,8 +147,8 @@ class NetworkTest(BaseAPIClientTest): with mock.patch('docker.api.client.APIClient.post', post): self.client.connect_container_to_network( - {'Id': container_id}, - network_id, + container={'Id': container_id}, + net_id=network_id, aliases=['foo', 'bar'], links=[('baz', 'quux')] ) @@ -176,7 +176,7 @@ class NetworkTest(BaseAPIClientTest): with mock.patch('docker.api.client.APIClient.post', post): self.client.disconnect_container_from_network( - {'Id': container_id}, network_id) + container={'Id': container_id}, net_id=network_id) self.assertEqual( post.call_args[0][0], From 9abcaccb89fe5767e541061befc1d24c64380243 Mon Sep 17 00:00:00 2001 From: Brandon Jackson Date: Tue, 18 Jul 2017 09:35:31 -0500 Subject: [PATCH 08/32] By not specifying a specific tag, the example would download every Ubuntu tag that exists. This oversight caused my machine to run out of disk space holding all the image diffs. Signed-off-by: Brandon Jackson --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 38963b32..747b98b2 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ client = docker.from_env() You can run containers: ```python ->>> client.containers.run("ubuntu", "echo hello world") +>>> client.containers.run("ubuntu:latest", "echo hello world") 'hello world\n' ``` From 48b5c07c3a7c008befe5349095eee0117ba8e3de Mon Sep 17 00:00:00 2001 From: Dima Spivak Date: Mon, 31 Jul 2017 15:04:15 -0700 Subject: [PATCH 09/32] client.networks.create check_duplicates docs not reflective of behavior Fixes #1693 Signed-off-by: Dima Spivak --- docker/api/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/api/network.py b/docker/api/network.py index 5549bf0c..befbb583 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -52,7 +52,7 @@ class NetworkApiMixin(object): options (dict): Driver options as a key-value dictionary ipam (IPAMConfig): Optional custom IP scheme for the network. check_duplicate (bool): Request daemon to check for networks with - same name. Default: ``True``. + same name. Default: ``None``. internal (bool): Restrict external access to the network. Default ``False``. labels (dict): Map of labels to set on the network. Default From bf9d06db251fc3befbdbe560bc03d2009ce927e7 Mon Sep 17 00:00:00 2001 From: Jakub Goszczurny Date: Mon, 3 Jul 2017 22:22:37 +0200 Subject: [PATCH 10/32] Generating regexp from .dockerignore file in a similar way as docker-ce. Signed-off-by: Jakub Goszczurny --- docker/utils/fnmatch.py | 27 +++++++++++++++++++-------- tests/unit/utils_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docker/utils/fnmatch.py b/docker/utils/fnmatch.py index e95b63ce..e51bd815 100644 --- a/docker/utils/fnmatch.py +++ b/docker/utils/fnmatch.py @@ -65,19 +65,32 @@ def translate(pat): There is no way to quote meta-characters. """ - recursive_mode = False i, n = 0, len(pat) - res = '' + res = '^' while i < n: c = pat[i] i = i + 1 if c == '*': if i < n and pat[i] == '*': - recursive_mode = True + # is some flavor of "**" i = i + 1 - res = res + '.*' + # Treat **/ as ** so eat the "/" + if pat[i] == '/': + i = i + 1 + if i >= n: + # is "**EOF" - to align with .gitignore just accept all + res = res + '.*' + else: + # is "**" + # Note that this allows for any # of /'s (even 0) because + # the .* will eat everything, even /'s + res = res + '(.*/)?' + else: + # is "*" so map it to anything but "/" + res = res + '[^/]*' elif c == '?': - res = res + '.' + # "?" is any char except "/" + res = res + '[^/]' elif c == '[': j = i if j < n and pat[j] == '!': @@ -96,8 +109,6 @@ def translate(pat): elif stuff[0] == '^': stuff = '\\' + stuff res = '%s[%s]' % (res, stuff) - elif recursive_mode and c == '/': - res = res + re.escape(c) + '?' else: res = res + re.escape(c) - return res + '\Z(?ms)' + return res + '$' diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index a2d463d7..7045d23c 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -639,6 +639,14 @@ class ExcludePathsTest(unittest.TestCase): 'foo', 'foo/bar', 'bar', + 'target', + 'target/subdir', + 'subdir', + 'subdir/target', + 'subdir/target/subdir', + 'subdir/subdir2', + 'subdir/subdir2/target', + 'subdir/subdir2/target/subdir' ] files = [ @@ -654,6 +662,14 @@ class ExcludePathsTest(unittest.TestCase): 'foo/bar/a.py', 'bar/a.py', 'foo/Dockerfile3', + 'target/file.txt', + 'target/subdir/file.txt', + 'subdir/file.txt', + 'subdir/target/file.txt', + 'subdir/target/subdir/file.txt', + 'subdir/subdir2/file.txt', + 'subdir/subdir2/target/file.txt', + 'subdir/subdir2/target/subdir/file.txt', ] all_paths = set(dirs + files) @@ -844,6 +860,15 @@ class ExcludePathsTest(unittest.TestCase): self.all_paths - set(['foo/bar', 'foo/bar/a.py']) ) + def test_single_and_double_wildcard(self): + assert self.exclude(['**/target/*/*']) == convert_paths( + self.all_paths - set( + ['target/subdir/file.txt', + 'subdir/target/subdir/file.txt', + 'subdir/subdir2/target/subdir/file.txt'] + ) + ) + class TarTest(unittest.TestCase): def test_tar_with_excludes(self): From bf15e27d6dcb0fe66c03dd0822fa1c88ee09e914 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 1 Aug 2017 13:49:48 -0700 Subject: [PATCH 11/32] Temporarily - do not run py33 tests on travis Signed-off-by: Joffrey F --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6b48142f..cd64b445 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,8 @@ python: - "3.5" env: - TOX_ENV=py27 - - TOX_ENV=py33 +# FIXME: default travis worker does not carry py33 anymore. Can this be configured? +# - TOX_ENV=py33 - TOX_ENV=py34 - TOX_ENV=py35 - TOX_ENV=flake8 From f7e7a8564e6d40fe6b4dfc739db460c56245c63b Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 7 Aug 2017 09:14:21 -0500 Subject: [PATCH 12/32] Fix domainname documentation in create_container function It looks like this was probably originally copypasta'ed from dns_search and not edited afterward. Signed-off-by: Erik Johnson --- docker/api/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/api/container.py b/docker/api/container.py index 532a9c6d..06c575d5 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -399,7 +399,7 @@ class ContainerApiMixin(object): name (str): A name for the container entrypoint (str or list): An entrypoint working_dir (str): Path to the working directory - domainname (str or list): Set custom DNS search domains + domainname (str): The domain name to use for the container memswap_limit (int): host_config (dict): A dictionary created with :py:meth:`create_host_config`. From 48377d52e9fe1f76add3c91fe3d4ede1898be37a Mon Sep 17 00:00:00 2001 From: Andreas Backx Date: Sun, 6 Aug 2017 17:15:09 +0200 Subject: [PATCH 13/32] Added wait to the Container class documentation. The container class documentation did not automatically document the `Container.wait` method. Signed-off-by: Andreas Backx --- docs/containers.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/containers.rst b/docs/containers.rst index 6c895c6b..7c41bfdd 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -53,3 +53,4 @@ Container objects .. automethod:: top .. automethod:: unpause .. automethod:: update + .. automethod:: wait From 62fda980e4159e54ea7920f174a2395101f964bf Mon Sep 17 00:00:00 2001 From: Artem Bolshakov Date: Tue, 25 Jul 2017 12:38:23 +0300 Subject: [PATCH 14/32] client.containers.run returns None if none of json-file or journald logging drivers used Signed-off-by: Artem Bolshakov --- docker/errors.py | 10 ++++-- docker/models/containers.py | 16 +++++++++- tests/integration/models_containers_test.py | 18 +++++++++++ tests/unit/errors_test.py | 34 ++++++++++++++++++++- tests/unit/fake_api.py | 6 ++++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/docker/errors.py b/docker/errors.py index 0da97f4e..1f8ac23c 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -127,8 +127,14 @@ class ContainerError(DockerException): self.command = command self.image = image self.stderr = stderr - msg = ("Command '{}' in image '{}' returned non-zero exit status {}: " - "{}").format(command, image, exit_status, stderr) + + if stderr is None: + msg = ("Command '{}' in image '{}' returned non-zero exit " + "status {}").format(command, image, exit_status, stderr) + else: + msg = ("Command '{}' in image '{}' returned non-zero exit " + "status {}: {}").format(command, image, exit_status, stderr) + super(ContainerError, self).__init__(msg) diff --git a/docker/models/containers.py b/docker/models/containers.py index cf01b275..a3598f28 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -667,6 +667,13 @@ class ContainerCollection(Collection): The container logs, either ``STDOUT``, ``STDERR``, or both, depending on the value of the ``stdout`` and ``stderr`` arguments. + ``STDOUT`` and ``STDERR`` may be read only if either ``json-file`` + or ``journald`` logging driver used. Thus, if you are using none of + these drivers, a ``None`` object is returned instead. See the + `Engine API documentation + `_ + for full details. + If ``detach`` is ``True``, a :py:class:`Container` object is returned instead. @@ -709,7 +716,14 @@ class ContainerCollection(Collection): if exit_status != 0: stdout = False stderr = True - out = container.logs(stdout=stdout, stderr=stderr) + + logging_driver = container.attrs['HostConfig']['LogConfig']['Type'] + + if logging_driver == 'json-file' or logging_driver == 'journald': + out = container.logs(stdout=stdout, stderr=stderr) + else: + out = None + if remove: container.remove() if exit_status != 0: diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index b76a88ff..ce3349ba 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -88,6 +88,24 @@ class ContainerCollectionTest(BaseIntegrationTest): assert 'Networks' in attrs['NetworkSettings'] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] + def test_run_with_none_driver(self): + client = docker.from_env(version=TEST_API_VERSION) + + out = client.containers.run( + "alpine", "echo hello", + log_config=dict(type='none') + ) + self.assertEqual(out, None) + + def test_run_with_json_file_driver(self): + client = docker.from_env(version=TEST_API_VERSION) + + out = client.containers.run( + "alpine", "echo hello", + log_config=dict(type='json-file') + ) + self.assertEqual(out, b'hello\n') + def test_get(self): client = docker.from_env(version=TEST_API_VERSION) container = client.containers.run("alpine", "sleep 300", detach=True) diff --git a/tests/unit/errors_test.py b/tests/unit/errors_test.py index b78af4e1..9678669c 100644 --- a/tests/unit/errors_test.py +++ b/tests/unit/errors_test.py @@ -2,8 +2,10 @@ import unittest import requests -from docker.errors import (APIError, DockerException, +from docker.errors import (APIError, ContainerError, DockerException, create_unexpected_kwargs_error) +from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID +from .fake_api_client import make_fake_client class APIErrorTest(unittest.TestCase): @@ -77,6 +79,36 @@ class APIErrorTest(unittest.TestCase): assert err.is_client_error() is True +class ContainerErrorTest(unittest.TestCase): + def test_container_without_stderr(self): + """The massage does not contain stderr""" + client = make_fake_client() + container = client.containers.get(FAKE_CONTAINER_ID) + command = "echo Hello World" + exit_status = 42 + image = FAKE_IMAGE_ID + stderr = None + + err = ContainerError(container, exit_status, command, image, stderr) + msg = ("Command '{}' in image '{}' returned non-zero exit status {}" + ).format(command, image, exit_status, stderr) + assert str(err) == msg + + def test_container_with_stderr(self): + """The massage contains stderr""" + client = make_fake_client() + container = client.containers.get(FAKE_CONTAINER_ID) + command = "echo Hello World" + exit_status = 42 + image = FAKE_IMAGE_ID + stderr = "Something went wrong" + + err = ContainerError(container, exit_status, command, image, stderr) + msg = ("Command '{}' in image '{}' returned non-zero exit status {}: " + "{}").format(command, image, exit_status, stderr) + assert str(err) == msg + + class CreateUnexpectedKwargsErrorTest(unittest.TestCase): def test_create_unexpected_kwargs_error_single(self): e = create_unexpected_kwargs_error('f', {'foo': 'bar'}) diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py index ff0f1b65..2ba85bbf 100644 --- a/tests/unit/fake_api.py +++ b/tests/unit/fake_api.py @@ -146,6 +146,12 @@ def get_fake_inspect_container(tty=False): "StartedAt": "2013-09-25T14:01:18.869545111+02:00", "Ghost": False }, + "HostConfig": { + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + }, "MacAddress": "02:42:ac:11:00:0a" } return status_code, response From f3374959b7145d0c25042d51065b5d387832d19c Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 7 Aug 2017 12:05:40 -0700 Subject: [PATCH 15/32] Improve ContainerError message compute Signed-off-by: Joffrey F --- docker/errors.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docker/errors.py b/docker/errors.py index 1f8ac23c..2a2f871e 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -128,12 +128,9 @@ class ContainerError(DockerException): self.image = image self.stderr = stderr - if stderr is None: - msg = ("Command '{}' in image '{}' returned non-zero exit " - "status {}").format(command, image, exit_status, stderr) - else: - msg = ("Command '{}' in image '{}' returned non-zero exit " - "status {}: {}").format(command, image, exit_status, stderr) + err = ": {}".format(stderr) if stderr is not None else "" + msg = ("Command '{}' in image '{}' returned non-zero exit " + "status {}{}").format(command, image, exit_status, err) super(ContainerError, self).__init__(msg) From 9e793806ff79559c3bc591d8c52a3bbe3cdb7350 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Tue, 1 Aug 2017 12:16:56 +0200 Subject: [PATCH 16/32] Return the result of the API when using remove_image and load_image Those calls return result that can be used by the developers. Signed-off-by: Cecile Tonglet --- docker/api/image.py | 4 ++-- tests/integration/api_image_test.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/api/image.py b/docker/api/image.py index 181c4a1e..85ff435d 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -272,7 +272,7 @@ class ImageApiMixin(object): data (binary): Image data to be loaded. """ res = self._post(self._url("/images/load"), data=data) - self._raise_for_status(res) + return self._result(res, True) @utils.minimum_version('1.25') def prune_images(self, filters=None): @@ -455,7 +455,7 @@ class ImageApiMixin(object): """ params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/{0}", image), params=params) - self._raise_for_status(res) + return self._result(res, True) def search(self, term): """ diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index 917bc505..192e6f8d 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -113,7 +113,8 @@ class RemoveImageTest(BaseAPIIntegrationTest): self.assertIn('Id', res) img_id = res['Id'] self.tmp_imgs.append(img_id) - self.client.remove_image(img_id, force=True) + logs = self.client.remove_image(img_id, force=True) + self.assertIn({"Deleted": img_id}, logs) images = self.client.images(all=True) res = [x for x in images if x['Id'].startswith(img_id)] self.assertEqual(len(res), 0) From 7139e2d8f1ea82340417add02090bfaf7794f159 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 16 Jun 2017 14:05:54 -0700 Subject: [PATCH 17/32] Return generator for output of load_image endpoint Signed-off-by: Joffrey F --- Jenkinsfile | 5 ++--- docker/api/image.py | 29 ++++++++++++++++++++++++++--- docker/models/images.py | 3 +++ tests/integration/api_image_test.py | 13 +++++++++++++ tests/unit/api_image_test.py | 14 ++++++++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 35792788..9e1b4912 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,9 +5,8 @@ def imageNamePy2 def imageNamePy3 def images = [:] -// Note: Swarm in dind seem notoriously flimsy with 1.12.1+, which is why we're -// sticking with 1.12.0 for the 1.12 series -def dockerVersions = ["1.13.1", "17.04.0-ce", "17.05.0-ce", "17.06.0-ce-rc5"] + +def dockerVersions = ["1.13.1", "17.04.0-ce", "17.05.0-ce", "17.06.0-ce"] def buildImage = { name, buildargs, pyTag -> img = docker.image(name) diff --git a/docker/api/image.py b/docker/api/image.py index 85ff435d..41cc267e 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -262,7 +262,7 @@ class ImageApiMixin(object): self._get(self._url("/images/{0}/json", image)), True ) - def load_image(self, data): + def load_image(self, data, quiet=None): """ Load an image that was previously saved using :py:meth:`~docker.api.image.ImageApiMixin.get_image` (or ``docker @@ -270,9 +270,32 @@ class ImageApiMixin(object): Args: data (binary): Image data to be loaded. + quiet (boolean): Suppress progress details in response. + + Returns: + (generator): Progress output as JSON objects. Only available for + API version >= 1.23 + + Raises: + :py:class:`docker.errors.APIError` + If the server returns an error. """ - res = self._post(self._url("/images/load"), data=data) - return self._result(res, True) + params = {} + + if quiet is not None: + if utils.version_lt(self._version, '1.23'): + raise errors.InvalidVersion( + 'quiet is not supported in API version < 1.23' + ) + params['quiet'] = quiet + + res = self._post( + self._url("/images/load"), data=data, params=params, stream=True + ) + if utils.version_gte(self._version, '1.23'): + return self._stream_helper(res, decode=True) + + self._raise_for_status(res) @utils.minimum_version('1.25') def prune_images(self, filters=None): diff --git a/docker/models/images.py b/docker/models/images.py index d4e24c60..3837929d 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -235,6 +235,9 @@ class ImageCollection(Collection): Args: data (binary): Image data to be loaded. + Returns: + (generator): Progress output as JSON objects + Raises: :py:class:`docker.errors.APIError` If the server returns an error. diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index 192e6f8d..14fb77aa 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -249,6 +249,19 @@ class ImportImageTest(BaseAPIIntegrationTest): assert img_data['Config']['Cmd'] == ['echo'] assert img_data['Config']['User'] == 'foobar' + # Docs say output is available in 1.23, but this test fails on 1.12.0 + @requires_api_version('1.24') + def test_get_load_image(self): + test_img = 'hello-world:latest' + self.client.pull(test_img) + data = self.client.get_image(test_img) + assert data + output = self.client.load_image(data) + assert any([ + line for line in output + if 'Loaded image: {}'.format(test_img) in line.get('stream', '') + ]) + @contextlib.contextmanager def temporary_http_file_server(self, stream): '''Serve data from an IO stream over HTTP.''' diff --git a/tests/unit/api_image_test.py b/tests/unit/api_image_test.py index 36b2a468..f1e42cc1 100644 --- a/tests/unit/api_image_test.py +++ b/tests/unit/api_image_test.py @@ -369,5 +369,19 @@ class ImageTest(BaseAPIClientTest): 'POST', url_prefix + 'images/load', data='Byte Stream....', + stream=True, + params={}, + timeout=DEFAULT_TIMEOUT_SECONDS + ) + + def test_load_image_quiet(self): + self.client.load_image('Byte Stream....', quiet=True) + + fake_request.assert_called_with( + 'POST', + url_prefix + 'images/load', + data='Byte Stream....', + stream=True, + params={'quiet': True}, timeout=DEFAULT_TIMEOUT_SECONDS ) From 7f5739dc025af101e939b1403cf46a68fbc2dc97 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Aug 2017 14:54:53 -0700 Subject: [PATCH 18/32] Leading slash in .dockerignore should be ignored Signed-off-by: Joffrey F --- docker/utils/build.py | 1 + tests/unit/utils_test.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docker/utils/build.py b/docker/utils/build.py index 79b72495..d4223e74 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -26,6 +26,7 @@ def exclude_paths(root, patterns, dockerfile=None): if dockerfile is None: dockerfile = 'Dockerfile' + patterns = [p.lstrip('/') for p in patterns] exceptions = [p for p in patterns if p.startswith('!')] include_patterns = [p[1:] for p in exceptions] diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 7045d23c..4a391fac 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -768,6 +768,11 @@ class ExcludePathsTest(unittest.TestCase): self.all_paths - set(['foo/a.py']) ) + def test_single_subdir_single_filename_leading_slash(self): + assert self.exclude(['/foo/a.py']) == convert_paths( + self.all_paths - set(['foo/a.py']) + ) + def test_single_subdir_with_path_traversal(self): assert self.exclude(['foo/whoops/../a.py']) == convert_paths( self.all_paths - set(['foo/a.py']) From 0494c4f262cc489469e4c8d6356e825d849f9125 Mon Sep 17 00:00:00 2001 From: cyli Date: Thu, 13 Apr 2017 17:25:55 -0700 Subject: [PATCH 19/32] Require "requests[security]" if the `[tls]` option is selected, which also installs: pyOpenSSL, cryptography, idna and installs cryptography's version of openssl in Mac OS (which by default has an ancient version of openssl that doesn't support TLS 1.2). Signed-off-by: cyli --- README.md | 4 ++++ requirements.txt | 2 +- setup.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 747b98b2..3ff124d7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ The latest stable version [is available on PyPI](https://pypi.python.org/pypi/do pip install docker +If you are intending to connect to a docker host via TLS, add `docker[tls]` to your requirements instead, or install with pip: + + pip install docker[tls] + ## Usage Connect to Docker using the default socket or the configuration in your environment: diff --git a/requirements.txt b/requirements.txt index 37541312..423ffb70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -requests==2.11.1 +requests[security]==2.11.1 six>=1.4.0 websocket-client==0.32.0 backports.ssl_match_hostname>=3.5 ; python_version < '3.5' diff --git a/setup.py b/setup.py index 31180d23..534c9495 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,16 @@ extras_require = { # ssl_match_hostname to verify hosts match with certificates via # ServerAltname: https://pypi.python.org/pypi/backports.ssl_match_hostname ':python_version < "3.3"': 'ipaddress >= 1.0.16', + + # If using docker-py over TLS, highly recommend this option is pip-installed + # or pinned. + + # TODO: if pip installign both "requests" and "requests[security]", the + # extra package from the "security" option are not installed (see + # https://github.com/pypa/pip/issues/4391). Once that's fixed, instead of + # installing the extra dependencies, install the following instead: + # 'requests[security] >= 2.5.2, != 2.11.0, != 2.12.2' + 'tls': ['pyOpenSSL>=0.14', 'cryptography>=1.3.4', 'idna>=2.0.0'], } version = None From 380914aaaa2cd3ebb0f2367f214d5de6c8eada89 Mon Sep 17 00:00:00 2001 From: cyli Date: Mon, 22 May 2017 15:45:06 -0700 Subject: [PATCH 20/32] If we're pinning exact versions of things for requirements.txt, pin all dependencies of dependencies as well so we can get a consistent build. Signed-off-by: cyli --- requirements.txt | 20 +++++++++++++++----- setup.py | 6 +++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 423ffb70..f3c61e79 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,16 @@ -requests[security]==2.11.1 -six>=1.4.0 -websocket-client==0.32.0 -backports.ssl_match_hostname>=3.5 ; python_version < '3.5' -ipaddress==1.0.16 ; python_version < '3.3' +appdirs==1.4.3 +asn1crypto==0.22.0 +backports.ssl-match-hostname==3.5.0.1 +cffi==1.10.0 +cryptography==1.9 docker-pycreds==0.2.1 +enum34==1.1.6 +idna==2.5 +ipaddress==1.0.18 +packaging==16.8 +pycparser==2.17 +pyOpenSSL==17.0.0 +pyparsing==2.2.0 +requests==2.14.2 +six==1.10.0 +websocket-client==0.40.0 diff --git a/setup.py b/setup.py index 534c9495..4a33c8df 100644 --- a/setup.py +++ b/setup.py @@ -36,10 +36,10 @@ extras_require = { # ServerAltname: https://pypi.python.org/pypi/backports.ssl_match_hostname ':python_version < "3.3"': 'ipaddress >= 1.0.16', - # If using docker-py over TLS, highly recommend this option is pip-installed - # or pinned. + # If using docker-py over TLS, highly recommend this option is + # pip-installed or pinned. - # TODO: if pip installign both "requests" and "requests[security]", the + # TODO: if pip installing both "requests" and "requests[security]", the # extra package from the "security" option are not installed (see # https://github.com/pypa/pip/issues/4391). Once that's fixed, instead of # installing the extra dependencies, install the following instead: From b54c76c3c1e89390d27f816eed97afbd2c3b9bf8 Mon Sep 17 00:00:00 2001 From: Ying Date: Fri, 16 Jun 2017 18:46:09 -0700 Subject: [PATCH 21/32] Upgrade tox and virtualenv in appveyor to make sure we have the latest pip. Signed-off-by: Ying --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 1fc67cc0..41cde625 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ version: '{branch}-{build}' install: - "SET PATH=C:\\Python27-x64;C:\\Python27-x64\\Scripts;%PATH%" - "python --version" - - "pip install tox==2.1.1 virtualenv==13.1.2" + - "pip install tox==2.7.0 virtualenv==15.1.0" # Build the binary after tests build: false From d49c136d042db105a20053ed933776534ff8f5b3 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Aug 2017 15:38:09 -0700 Subject: [PATCH 22/32] Daemon expects full URL of hub in auth config dict in build payload Signed-off-by: Joffrey F --- docker/api/build.py | 5 ++++- docker/auth.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/api/build.py b/docker/api/build.py index cbef4a8b..5d4e7720 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -274,7 +274,10 @@ class BuildApiMixin(object): self._auth_configs, registry ) else: - auth_data = self._auth_configs + auth_data = self._auth_configs.copy() + # See https://github.com/docker/docker-py/issues/1683 + if auth.INDEX_NAME in auth_data: + auth_data[auth.INDEX_URL] = auth_data[auth.INDEX_NAME] log.debug( 'Sending auth config ({0})'.format( diff --git a/docker/auth.py b/docker/auth.py index ec9c45b9..c3fb062e 100644 --- a/docker/auth.py +++ b/docker/auth.py @@ -10,7 +10,7 @@ from . import errors from .constants import IS_WINDOWS_PLATFORM INDEX_NAME = 'docker.io' -INDEX_URL = 'https://{0}/v1/'.format(INDEX_NAME) +INDEX_URL = 'https://index.{0}/v1/'.format(INDEX_NAME) DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json') LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg' TOKEN_USERNAME = '' @@ -118,7 +118,7 @@ def _resolve_authconfig_credstore(authconfig, registry, credstore_name): if not registry or registry == INDEX_NAME: # The ecosystem is a little schizophrenic with index.docker.io VS # docker.io - in that case, it seems the full URL is necessary. - registry = 'https://index.docker.io/v1/' + registry = INDEX_URL log.debug("Looking for auth entry for {0}".format(repr(registry))) store = dockerpycreds.Store(credstore_name) try: From b4802ea12626bb9d987c34f679ac04d97a402f9f Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Aug 2017 16:07:47 -0700 Subject: [PATCH 23/32] Handle untyped ContainerSpec dict in _check_api_features Signed-off-by: Joffrey F --- docker/api/service.py | 2 +- tests/integration/api_service_test.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docker/api/service.py b/docker/api/service.py index cc16cc37..4b555a5f 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -38,7 +38,7 @@ def _check_api_features(version, task_template, update_config): 'Placement.preferences is not supported in' ' API version < 1.27' ) - if task_template.container_spec.get('TTY'): + if task_template.get('ContainerSpec', {}).get('TTY'): if utils.version_lt(version, '1.25'): raise errors.InvalidVersion( 'ContainerSpec.TTY is not supported in API version < 1.25' diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index 54111a7b..c966916e 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -376,6 +376,23 @@ class ServiceTest(BaseAPIIntegrationTest): assert 'TTY' in con_spec assert con_spec['TTY'] is True + @requires_api_version('1.25') + def test_create_service_with_tty_dict(self): + container_spec = { + 'Image': BUSYBOX, + 'Command': ['true'], + 'TTY': True + } + task_tmpl = docker.types.TaskTemplate(container_spec) + name = self.get_service_name() + svc_id = self.client.create_service(task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + assert 'TaskTemplate' in svc_info['Spec'] + assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate'] + con_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec'] + assert 'TTY' in con_spec + assert con_spec['TTY'] is True + def test_create_service_global_mode(self): container_spec = docker.types.ContainerSpec( BUSYBOX, ['echo', 'hello'] From 2a6926b5aba00f83c8c1702f5dde3d5eaa855d29 Mon Sep 17 00:00:00 2001 From: adrianliaw Date: Sat, 6 May 2017 19:29:39 +0800 Subject: [PATCH 24/32] Use collection's get method inside ImageCollection's list method Signed-off-by: Adrian Liaw --- docker/models/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/models/images.py b/docker/models/images.py index 3837929d..d1b29ad8 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -224,7 +224,7 @@ class ImageCollection(Collection): If the server returns an error. """ resp = self.client.api.images(name=name, all=all, filters=filters) - return [self.prepare_model(r) for r in resp] + return [self.get(r["Id"]) for r in resp] def load(self, data): """ From 6b59dc62715a1e387543de02ade80de84aa2171c Mon Sep 17 00:00:00 2001 From: David Steines Date: Mon, 3 Apr 2017 21:58:59 -0400 Subject: [PATCH 25/32] Allow detach and remove for api version >= 1.25 and use auto_remove when both are set. Continue raising an exception for api versions <1.25. Signed-off-by: David Steines --- docker/models/containers.py | 10 ++++++++-- tests/unit/models_containers_test.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index a3598f28..d9db79df 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -4,6 +4,7 @@ from ..api import APIClient from ..errors import (ContainerError, ImageNotFound, create_unexpected_kwargs_error) from ..types import HostConfig +from ..utils import compare_version from .images import Image from .resource import Collection, Model @@ -690,8 +691,12 @@ class ContainerCollection(Collection): image = image.id detach = kwargs.pop("detach", False) if detach and remove: - raise RuntimeError("The options 'detach' and 'remove' cannot be " - "used together.") + if compare_version("1.24", + self.client.api._version) > 0: + kwargs["auto_remove"] = True + else: + raise RuntimeError("The options 'detach' and 'remove' cannot " + "be used together in api versions < 1.25.") if kwargs.get('network') and kwargs.get('network_mode'): raise RuntimeError( @@ -849,6 +854,7 @@ RUN_CREATE_KWARGS = [ # kwargs to copy straight from run to host_config RUN_HOST_CONFIG_KWARGS = [ + 'auto_remove', 'blkio_weight_device', 'blkio_weight', 'cap_add', diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 70c86480..5eaa45ac 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -273,9 +273,39 @@ class ContainerCollectionTest(unittest.TestCase): client.api.remove_container.assert_called_with(FAKE_CONTAINER_ID) client = make_fake_client() + client.api._version = '1.24' with self.assertRaises(RuntimeError): client.containers.run("alpine", detach=True, remove=True) + client = make_fake_client() + client.api._version = '1.23' + with self.assertRaises(RuntimeError): + client.containers.run("alpine", detach=True, remove=True) + + client = make_fake_client() + client.api._version = '1.25' + client.containers.run("alpine", detach=True, remove=True) + client.api.remove_container.assert_not_called() + client.api.create_container.assert_called_with( + command=None, + image='alpine', + detach=True, + host_config={'AutoRemove': True, + 'NetworkMode': 'default'} + ) + + client = make_fake_client() + client.api._version = '1.26' + client.containers.run("alpine", detach=True, remove=True) + client.api.remove_container.assert_not_called() + client.api.create_container.assert_called_with( + command=None, + image='alpine', + detach=True, + host_config={'AutoRemove': True, + 'NetworkMode': 'default'} + ) + def test_create(self): client = make_fake_client() container = client.containers.create( From d5c4ce203aa5839966a221079d1be44e572e92af Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Aug 2017 17:40:07 -0700 Subject: [PATCH 26/32] Use better version comparison function Signed-off-by: Joffrey F --- docker/models/containers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index d9db79df..688decca 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -4,7 +4,7 @@ from ..api import APIClient from ..errors import (ContainerError, ImageNotFound, create_unexpected_kwargs_error) from ..types import HostConfig -from ..utils import compare_version +from ..utils import version_gte from .images import Image from .resource import Collection, Model @@ -691,8 +691,7 @@ class ContainerCollection(Collection): image = image.id detach = kwargs.pop("detach", False) if detach and remove: - if compare_version("1.24", - self.client.api._version) > 0: + if version_gte(self.client.api._version, '1.25'): kwargs["auto_remove"] = True else: raise RuntimeError("The options 'detach' and 'remove' cannot " From a6065df64d848a0fc1ce9f2638b7b2a33f407145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20F=C3=A9ron?= Date: Mon, 30 Jan 2017 19:06:20 +0100 Subject: [PATCH 27/32] Add support for the `squash` flag when building MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added a test that compares the number of layers in the default mode, and with the new flag Signed-off-by: Gabriel Féron --- docker/api/build.py | 13 ++++++++++++- tests/integration/api_build_test.py | 28 +++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docker/api/build.py b/docker/api/build.py index 5d4e7720..f9678a39 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -18,7 +18,8 @@ class BuildApiMixin(object): custom_context=False, encoding=None, pull=False, forcerm=False, dockerfile=None, container_limits=None, decode=False, buildargs=None, gzip=False, shmsize=None, - labels=None, cache_from=None, target=None, network_mode=None): + labels=None, cache_from=None, target=None, network_mode=None, + squash=None): """ Similar to the ``docker build`` command. Either ``path`` or ``fileobj`` needs to be set. ``path`` can be a local path (to a directory @@ -98,6 +99,8 @@ class BuildApiMixin(object): Dockerfile network_mode (str): networking mode for the run commands during build + squash (bool): Squash the resulting images layers into a + single layer. Returns: A generator for the build output. @@ -218,6 +221,14 @@ class BuildApiMixin(object): 'network_mode was only introduced in API version 1.25' ) + if squash: + if utils.version_gte(self._version, '1.25'): + params.update({'squash': squash}) + else: + raise errors.InvalidVersion( + 'squash was only introduced in API version 1.25' + ) + if context is not None: headers = {'Content-Type': 'application/tar'} if encoding: diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index 609964f0..209c1f28 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -9,7 +9,7 @@ import pytest import six from .base import BaseAPIIntegrationTest -from ..helpers import requires_api_version +from ..helpers import requires_api_version, requires_experimental class BuildTest(BaseAPIIntegrationTest): @@ -244,6 +244,32 @@ class BuildTest(BaseAPIIntegrationTest): with pytest.raises(errors.NotFound): self.client.inspect_image('dockerpytest_nonebuild') + @requires_api_version('1.25') + @requires_experimental + def test_build_squash(self): + script = io.BytesIO('\n'.join([ + 'FROM busybox', + 'RUN echo blah > /file_1', + 'RUN echo blahblah > /file_2', + 'RUN echo blahblahblah > /file_3' + ]).encode('ascii')) + + def build_squashed(squash): + tag = 'squash' if squash else 'nosquash' + stream = self.client.build( + fileobj=script, tag=tag, squash=squash + ) + self.tmp_imgs.append(tag) + for chunk in stream: + pass + + return self.client.inspect_image(tag) + + non_squashed = build_squashed(False) + squashed = build_squashed(True) + self.assertEqual(len(non_squashed['RootFS']['Layers']), 4) + self.assertEqual(len(squashed['RootFS']['Layers']), 2) + def test_build_stderr_data(self): control_chars = ['\x1b[91m', '\x1b[0m'] snippet = 'Ancient Temple (Mystic Oriental Dream ~ Ancient Temple)' From d9df2a8b75d8a36691a15cdb27213b1db5fa4a61 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 16 Aug 2017 17:31:36 -0700 Subject: [PATCH 28/32] Fix handling of non-multiplexed (TTY) streams over upgraded sockets Signed-off-by: Joffrey F --- docker/api/client.py | 24 +++++++++++++++++------- docker/api/container.py | 4 +++- docker/api/exec_api.py | 2 +- docker/utils/socket.py | 21 ++++++++++++++++++++- tests/integration/api_build_test.py | 2 +- tests/unit/api_test.py | 2 +- 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/docker/api/client.py b/docker/api/client.py index 65b5baa9..1de10c77 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -32,7 +32,7 @@ from ..errors import ( from ..tls import TLSConfig from ..transport import SSLAdapter, UnixAdapter from ..utils import utils, check_resource, update_headers -from ..utils.socket import frames_iter +from ..utils.socket import frames_iter, socket_raw_iter from ..utils.json_stream import json_stream try: from ..transport import NpipeAdapter @@ -362,13 +362,19 @@ class APIClient( for out in response.iter_content(chunk_size=1, decode_unicode=True): yield out - def _read_from_socket(self, response, stream): + def _read_from_socket(self, response, stream, tty=False): socket = self._get_raw_response_socket(response) - if stream: - return frames_iter(socket) + gen = None + if tty is False: + gen = frames_iter(socket) else: - return six.binary_type().join(frames_iter(socket)) + gen = socket_raw_iter(socket) + + if stream: + return gen + else: + return six.binary_type().join(gen) def _disable_socket_timeout(self, socket): """ Depending on the combination of python version and whether we're @@ -398,9 +404,13 @@ class APIClient( s.settimeout(None) - def _get_result(self, container, stream, res): + @check_resource('container') + def _check_is_tty(self, container): cont = self.inspect_container(container) - return self._get_result_tty(stream, res, cont['Config']['Tty']) + return cont['Config']['Tty'] + + def _get_result(self, container, stream, res): + return self._get_result_tty(stream, res, self._check_is_tty(container)) def _get_result_tty(self, stream, res, is_tty): # Stream multi-plexing was only introduced in API v1.6. Anything diff --git a/docker/api/container.py b/docker/api/container.py index 06c575d5..dde13254 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -52,7 +52,9 @@ class ContainerApiMixin(object): u = self._url("/containers/{0}/attach", container) response = self._post(u, headers=headers, params=params, stream=stream) - return self._read_from_socket(response, stream) + return self._read_from_socket( + response, stream, self._check_is_tty(container) + ) @utils.check_resource('container') def attach_socket(self, container, params=None, ws=False): diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py index 2b407cef..6f42524e 100644 --- a/docker/api/exec_api.py +++ b/docker/api/exec_api.py @@ -153,4 +153,4 @@ class ExecApiMixin(object): return self._result(res) if socket: return self._get_raw_response_socket(res) - return self._read_from_socket(res, stream) + return self._read_from_socket(res, stream, tty) diff --git a/docker/utils/socket.py b/docker/utils/socket.py index 4080f253..54392d2b 100644 --- a/docker/utils/socket.py +++ b/docker/utils/socket.py @@ -75,5 +75,24 @@ def frames_iter(socket): break while n > 0: result = read(socket, n) - n -= len(result) + if result is None: + continue + data_length = len(result) + if data_length == 0: + # We have reached EOF + return + n -= data_length yield result + + +def socket_raw_iter(socket): + """ + Returns a generator of data read from the socket. + This is used for non-multiplexed streams. + """ + while True: + result = read(socket) + if len(result) == 0: + # We have reached EOF + return + yield result diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index 209c1f28..d0aa5c21 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -244,8 +244,8 @@ class BuildTest(BaseAPIIntegrationTest): with pytest.raises(errors.NotFound): self.client.inspect_image('dockerpytest_nonebuild') + @requires_experimental(until=None) @requires_api_version('1.25') - @requires_experimental def test_build_squash(self): script = io.BytesIO('\n'.join([ 'FROM busybox', diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index 83848c52..6ac92c40 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -83,7 +83,7 @@ def fake_delete(self, url, *args, **kwargs): return fake_request('DELETE', url, *args, **kwargs) -def fake_read_from_socket(self, response, stream): +def fake_read_from_socket(self, response, stream, tty=False): return six.binary_type() From 921aba107b75b4571d8e7391d199011e07048b8a Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 17 Aug 2017 12:38:24 -0700 Subject: [PATCH 29/32] Update test versions Signed-off-by: Joffrey F --- Jenkinsfile | 4 ++-- Makefile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 9e1b4912..6fa27289 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,7 +6,7 @@ def imageNamePy3 def images = [:] -def dockerVersions = ["1.13.1", "17.04.0-ce", "17.05.0-ce", "17.06.0-ce"] +def dockerVersions = ["1.13.1", "17.04.0-ce", "17.05.0-ce", "17.06.1-ce", "17.07.0-ce-rc3"] def buildImage = { name, buildargs, pyTag -> img = docker.image(name) @@ -34,7 +34,7 @@ def buildImages = { -> } def getAPIVersion = { engineVersion -> - def versionMap = ['1.13.': '1.26', '17.04': '1.27', '17.05': '1.29', '17.06': '1.30'] + def versionMap = ['1.13.': '1.26', '17.04': '1.27', '17.05': '1.29', '17.06': '1.30', '17.07': '1.31'] return versionMap[engineVersion.substring(0, 5)] } diff --git a/Makefile b/Makefile index e4cd3f7b..c6c6d56c 100644 --- a/Makefile +++ b/Makefile @@ -41,8 +41,8 @@ integration-test: build integration-test-py3: build-py3 docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test tests/integration/${file} -TEST_API_VERSION ?= 1.29 -TEST_ENGINE_VERSION ?= 17.05.0-ce +TEST_API_VERSION ?= 1.30 +TEST_ENGINE_VERSION ?= 17.06.0-ce .PHONY: integration-dind integration-dind: build build-py3 From 7d559a957c5908a3cc2b7bee3336869b33d87107 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 17 Aug 2017 12:38:36 -0700 Subject: [PATCH 30/32] Update default API version Signed-off-by: Joffrey F --- docker/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/constants.py b/docker/constants.py index 91a65282..6de8fad6 100644 --- a/docker/constants.py +++ b/docker/constants.py @@ -1,7 +1,7 @@ import sys from .version import version -DEFAULT_DOCKER_API_VERSION = '1.26' +DEFAULT_DOCKER_API_VERSION = '1.30' MINIMUM_DOCKER_API_VERSION = '1.21' DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 From 9b6ff333ac0e1fbebb8fe4881d29b36c07f15a51 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 17 Aug 2017 12:38:52 -0700 Subject: [PATCH 31/32] Bump 2.5.0 Signed-off-by: Joffrey F --- Jenkinsfile | 2 +- docker/version.py | 2 +- docs/change-log.md | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6fa27289..a83d7bf1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,7 +6,7 @@ def imageNamePy3 def images = [:] -def dockerVersions = ["1.13.1", "17.04.0-ce", "17.05.0-ce", "17.06.1-ce", "17.07.0-ce-rc3"] +def dockerVersions = ["1.13.1", "17.04.0-ce", "17.05.0-ce", "17.06.0-ce", "17.07.0-ce-rc3"] def buildImage = { name, buildargs, pyTag -> img = docker.image(name) diff --git a/docker/version.py b/docker/version.py index a7452d4f..066b62e7 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.5.0-dev" +version = "2.5.0" 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 7099d794..894cd1e3 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,40 @@ Change log ========== +2.5.0 +----- + +[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/33?closed=1) + +### Features + +* Added support for the `squash` parameter in `APIClient.build` and + `DockerClient.images.build`. +* When using API version 1.23 or above, `load_image` will now return a + generator of progress as JSON `dict`s. +* `remove_image` now returns the content of the API's response. + + +### Bugfixes + +* Fixed an issue where the `auto_remove` parameter in + `DockerClient.containers.run` was not taken into account. +* Fixed a bug where `.dockerignore` patterns starting with a slash + were ignored. +* Fixed an issue with the handling of `**` patterns in `.dockerignore` +* Fixed a bug where building `FROM` a private Docker Hub image when not + using a cred store would fail. +* Fixed a bug where calling `create_service` or `update_service` with + `task_template` as a `dict` would raise an exception. +* Fixed the handling of TTY-enabled containers in `attach` and `exec_run`. +* `DockerClient.containers.run` will no longer attempt to stream logs if the + log driver doesn't support the operation. + +### Miscellaneous + +* Added extra requirements for better TLS support on some platforms. + These can be installed or required through the `docker[tls]` notation. + 2.4.2 ----- From 8d14709c1804b3803351c1a6509820eaab52b6ef Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 17 Aug 2017 13:50:20 -0700 Subject: [PATCH 32/32] Changelog typo Signed-off-by: Joffrey F --- docs/change-log.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index 894cd1e3..199e5ce8 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -4,7 +4,7 @@ Change log 2.5.0 ----- -[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/33?closed=1) +[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/34?closed=1) ### Features