From 742daa2665dcbe64fb6b4ec0c21272cfa57d80ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BF=8A=E6=9D=B0?= Date: Wed, 28 Sep 2016 11:01:32 +0800 Subject: [PATCH 01/11] replace on_failure with on-failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 徐俊杰 Signed-off-by: XuPaco --- docker/types/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/types/services.py b/docker/types/services.py index 8488d6e2bc..063779cd88 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -152,7 +152,7 @@ def __init__(self, parallelism=0, delay=None, failure_action='continue'): class RestartConditionTypesEnum(object): _values = ( 'none', - 'on_failure', + 'on-failure', 'any', ) NONE, ON_FAILURE, ANY = _values From 31afde6b875a2486bf7bee5ff12acdb886a9dd51 Mon Sep 17 00:00:00 2001 From: Pierre Tardy Date: Tue, 4 Oct 2016 13:13:04 +0200 Subject: [PATCH 02/11] fix for got an unexpected keyword argument 'num_pools' requests's HTTPAdapter API is pool_connections for number of connection of the pool Signed-off-by: Pierre Tardy --- docker/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/client.py b/docker/client.py index 47ad09e9a5..8fd3b5a12b 100644 --- a/docker/client.py +++ b/docker/client.py @@ -86,7 +86,7 @@ def __init__(self, base_url=None, version=None, tls.configure_client(self) elif tls: self._custom_adapter = ssladapter.SSLAdapter( - num_pools=num_pools + pool_connections=num_pools ) self.mount('https://', self._custom_adapter) self.base_url = base_url From c9839503107f74ac499bdb18d3867cde39502477 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 12 Oct 2016 16:06:43 -0700 Subject: [PATCH 03/11] Remove trailing slashes in result of utils.parse_host Signed-off-by: Joffrey F --- docker/utils/utils.py | 4 ++-- tests/unit/utils_test.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index d46f8fcdbb..dcd4f586e0 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -438,8 +438,8 @@ def parse_host(addr, is_win32=False, tls=False): "Bind address needs a port: {0}".format(addr)) if proto == "http+unix" or proto == 'npipe': - return "{0}://{1}".format(proto, host) - return "{0}://{1}:{2}{3}".format(proto, host, port, path) + return "{0}://{1}".format(proto, host).rstrip('/') + return "{0}://{1}:{2}{3}".format(proto, host, port, path).rstrip('/') def parse_devices(devices): diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 2a2759d033..059c82d3bb 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -522,6 +522,11 @@ def test_parse_host_tls_tcp_proto(self): expected_result = 'https://myhost.docker.net:3348' assert parse_host(host_value, tls=True) == expected_result + def test_parse_host_trailing_slash(self): + host_value = 'tcp://myhost.docker.net:2376/' + expected_result = 'http://myhost.docker.net:2376' + assert parse_host(host_value) == expected_result + class ParseRepositoryTagTest(base.BaseTestCase): sha = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' From 4028e298a12f47023f107e61ab1aebab38275c1d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 12 Oct 2016 17:19:08 -0700 Subject: [PATCH 04/11] Do not break when calling format_environment with unicode values Signed-off-by: Joffrey F --- docker/utils/utils.py | 3 +++ tests/unit/utils_test.py | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index dcd4f586e0..72706dfb38 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -986,6 +986,9 @@ def format_environment(environment): def format_env(key, value): if value is None: return key + if isinstance(value, six.binary_type): + value = value.decode('utf-8') + return u'{key}={value}'.format(key=key, value=value) return [format_env(*var) for var in six.iteritems(environment)] diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 059c82d3bb..a06cbea320 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -20,11 +20,11 @@ create_host_config, Ulimit, LogConfig, parse_bytes, parse_env_file, exclude_paths, convert_volume_binds, decode_json_header, tar, split_command, create_ipam_config, create_ipam_pool, parse_devices, - update_headers, + update_headers ) from docker.utils.ports import build_port_bindings, split_port -from docker.utils.utils import create_endpoint_config +from docker.utils.utils import create_endpoint_config, format_environment from .. import base from ..helpers import make_tree @@ -1047,3 +1047,18 @@ def test_tar_with_directory_symlinks(self): self.assertEqual( sorted(tar_data.getnames()), ['bar', 'bar/foo', 'foo'] ) + + +class FormatEnvironmentTest(base.BaseTestCase): + def test_format_env_binary_unicode_value(self): + env_dict = { + 'ARTIST_NAME': b'\xec\x86\xa1\xec\xa7\x80\xec\x9d\x80' + } + assert format_environment(env_dict) == [u'ARTIST_NAME=송지은'] + + def test_format_env_no_value(self): + env_dict = { + 'FOO': None, + 'BAR': '', + } + assert sorted(format_environment(env_dict)) == ['BAR=', 'FOO'] From 84c21312e41c668d103b5c9df184f564dba560ef Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 11 Oct 2016 15:16:43 -0700 Subject: [PATCH 05/11] Do not allow bufsize to be 0 in NpipeSocket.makefile() Signed-off-by: Joffrey F --- docker/transport/npipesocket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/transport/npipesocket.py b/docker/transport/npipesocket.py index 9010cebe13..a34e02c048 100644 --- a/docker/transport/npipesocket.py +++ b/docker/transport/npipesocket.py @@ -94,7 +94,7 @@ def makefile(self, mode=None, bufsize=None): if mode.strip('b') != 'r': raise NotImplementedError() rawio = NpipeFileIOBase(self) - if bufsize is None or bufsize < 0: + if bufsize is None or bufsize <= 0: bufsize = io.DEFAULT_BUFFER_SIZE return io.BufferedReader(rawio, buffer_size=bufsize) From 78c57329558dcd27ce295d3877e1f0b93f2a2e89 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 11 Oct 2016 17:19:20 -0700 Subject: [PATCH 06/11] Several fixes to npipe support - Fix _get_raw_response_socket to always return the NpipeSocket object - Override NpipeHTTPConnectionPool._get_conn to avoid crash in urllib3 - Fix NpipeSocket.recv_into for Python 2 - Do not call select() on NpipeSocket objects Signed-off-by: Joffrey F --- docker/client.py | 4 +++- docker/transport/__init__.py | 1 + docker/transport/npipeconn.py | 23 ++++++++++++++++++++++- docker/transport/npipesocket.py | 10 ++++++++++ docker/utils/socket.py | 9 ++++++++- 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/docker/client.py b/docker/client.py index 8fd3b5a12b..3fa19e00a9 100644 --- a/docker/client.py +++ b/docker/client.py @@ -218,7 +218,9 @@ def _create_websocket_connection(self, url): def _get_raw_response_socket(self, response): self._raise_for_status(response) - if six.PY3: + if self.base_url == "http+docker://localnpipe": + sock = response.raw._fp.fp.raw.sock + elif six.PY3: sock = response.raw._fp.fp.raw if self.base_url.startswith("https://"): sock = sock._sock diff --git a/docker/transport/__init__.py b/docker/transport/__init__.py index d647483e2a..46dfdf8e76 100644 --- a/docker/transport/__init__.py +++ b/docker/transport/__init__.py @@ -2,5 +2,6 @@ from .unixconn import UnixAdapter try: from .npipeconn import NpipeAdapter + from .npipesocket import NpipeSocket except ImportError: pass \ No newline at end of file diff --git a/docker/transport/npipeconn.py b/docker/transport/npipeconn.py index 917fa8b3bf..017738e6a5 100644 --- a/docker/transport/npipeconn.py +++ b/docker/transport/npipeconn.py @@ -14,7 +14,6 @@ except ImportError: import urllib3 - RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer @@ -46,6 +45,28 @@ def _new_conn(self): self.npipe_path, self.timeout ) + # When re-using connections, urllib3 tries to call select() on our + # NpipeSocket instance, causing a crash. To circumvent this, we override + # _get_conn, where that check happens. + def _get_conn(self, timeout): + conn = None + try: + conn = self.pool.get(block=self.block, timeout=timeout) + + except AttributeError: # self.pool is None + raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") + + except six.moves.queue.Empty: + if self.block: + raise urllib3.exceptions.EmptyPoolError( + self, + "Pool reached maximum size and no more " + "connections are allowed." + ) + pass # Oh well, we'll create a new connection then + + return conn or self._new_conn() + class NpipeAdapter(requests.adapters.HTTPAdapter): def __init__(self, base_url, timeout=60, diff --git a/docker/transport/npipesocket.py b/docker/transport/npipesocket.py index a34e02c048..527f0abbf0 100644 --- a/docker/transport/npipesocket.py +++ b/docker/transport/npipesocket.py @@ -1,6 +1,7 @@ import functools import io +import six import win32file import win32pipe @@ -114,6 +115,9 @@ def recvfrom_into(self, buf, nbytes=0, flags=0): @check_closed def recv_into(self, buf, nbytes=0): + if six.PY2: + return self._recv_into_py2(buf, nbytes) + readbuf = buf if not isinstance(buf, memoryview): readbuf = memoryview(buf) @@ -124,6 +128,12 @@ def recv_into(self, buf, nbytes=0): ) return len(data) + def _recv_into_py2(self, buf, nbytes): + err, data = win32file.ReadFile(self._handle, nbytes or len(buf)) + n = len(data) + buf[:n] = data + return n + @check_closed def send(self, string, flags=0): err, nbytes = win32file.WriteFile(self._handle, string) diff --git a/docker/utils/socket.py b/docker/utils/socket.py index ed343507d8..164b845afc 100644 --- a/docker/utils/socket.py +++ b/docker/utils/socket.py @@ -5,6 +5,11 @@ import six +try: + from ..transport import NpipeSocket +except ImportError: + NpipeSocket = type(None) + class SocketError(Exception): pass @@ -14,10 +19,12 @@ def read(socket, n=4096): """ Reads at most n bytes from socket """ + recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK) # wait for data to become available - select.select([socket], [], []) + if not isinstance(socket, NpipeSocket): + select.select([socket], [], []) try: if hasattr(socket, 'recv'): From 213dc8f757d22e569faa985ef221f8d53abb2fa6 Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 3 Oct 2016 18:34:49 +0300 Subject: [PATCH 07/11] Support requests versions from 2.11.1 onwards Bug #1155 has been fixed starting with requests 2.11.1 and excluding it from dependencies causes failures when using latest versions of both libs together in our project. Signed-off-by: Yuriy Taraday --- requirements.txt | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1e5284600f..375413122b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -requests==2.5.3 +requests==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' -docker-pycreds==0.2.1 \ No newline at end of file +docker-pycreds==0.2.1 diff --git a/setup.py b/setup.py index 9233ac2a8a..00e1febdc7 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ SOURCE_DIR = os.path.join(ROOT_DIR) requirements = [ - 'requests >= 2.5.2, < 2.11', + 'requests >= 2.5.2, != 2.11.0', 'six >= 1.4.0', 'websocket-client >= 0.32.0', 'docker-pycreds >= 0.2.1' From a6f12777f17dd0a4f7d97908720dc02781f762fd Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 14 Oct 2016 16:58:23 -0700 Subject: [PATCH 08/11] Bump version to 1.10.4 ; Update changelog Signed-off-by: Joffrey F --- docker/version.py | 2 +- docs/change_log.md | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docker/version.py b/docker/version.py index 2bf8436d36..12eb03b3aa 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "1.10.3" +version = "1.10.4" 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 e32df1e974..17490d34d9 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -1,6 +1,28 @@ Change Log ========== +1.10.4 +------ + +[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/24?closed=1) + +### Bugfixes + +* Fixed an issue where `RestartPolicy.condition_types.ON_FAILURE` would yield + an invalid value. +* Fixed an issue where the SSL connection adapter would receive an invalid + argument. +* Fixed an issue that caused the Client to fail to reach API endpoints when + the provided `base_url` had a trailing slash. +* Fixed a bug where some `environment` values in `create_container` + containing unicode characters would raise an encoding error. +* Fixed a number of issues tied with named pipe transport on Windows. + +### Miscellaneous + +* Adjusted version requirements for the `requests` library. + + 1.10.3 ------ From 89758511f8730894f88c612cb025b4ffa3c9fd27 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 13 Oct 2016 16:40:55 -0700 Subject: [PATCH 09/11] Update tests to avoid failures on Windows platforms Signed-off-by: Joffrey F --- tests/integration/build_test.py | 12 +-- tests/integration/conftest.py | 4 +- tests/integration/container_test.py | 45 +++++---- tests/integration/image_test.py | 8 +- tests/integration/network_test.py | 25 +++-- tests/unit/api_test.py | 5 +- tests/unit/container_test.py | 8 +- tests/unit/fake_api.py | 3 + tests/unit/utils_test.py | 140 +++++++++++++++++----------- 9 files changed, 144 insertions(+), 106 deletions(-) diff --git a/tests/integration/build_test.py b/tests/integration/build_test.py index cc8a8626de..c7a5fbe6f4 100644 --- a/tests/integration/build_test.py +++ b/tests/integration/build_test.py @@ -1,5 +1,4 @@ import io -import json import os import shutil import tempfile @@ -22,14 +21,11 @@ def test_build_streaming(self): 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' ' /tmp/silence.tar.gz' ]).encode('ascii')) - stream = self.client.build(fileobj=script, stream=True) - logs = '' + stream = self.client.build(fileobj=script, stream=True, decode=True) + logs = [] for chunk in stream: - if six.PY3: - chunk = chunk.decode('utf-8') - json.loads(chunk) # ensure chunk is a single, valid JSON blob - logs += chunk - self.assertNotEqual(logs, '') + logs.append(chunk) + assert len(logs) > 0 def test_build_from_stringio(self): if six.PY3: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b17419504e..e65dd1db86 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,6 +1,5 @@ from __future__ import print_function -import json import sys import warnings @@ -19,8 +18,7 @@ def setup_test_session(): c.inspect_image(BUSYBOX) except docker.errors.NotFound: print("\npulling {0}".format(BUSYBOX), file=sys.stderr) - for data in c.pull(BUSYBOX, stream=True): - data = json.loads(data.decode('utf-8')) + for data in c.pull(BUSYBOX, stream=True, decode=True): status = data.get("status") progress = data.get("progress") detail = "{0} - {1}".format(status, progress) diff --git a/tests/integration/container_test.py b/tests/integration/container_test.py index 27d3046bbc..ae2a701c1b 100644 --- a/tests/integration/container_test.py +++ b/tests/integration/container_test.py @@ -3,6 +3,7 @@ import tempfile import docker +from docker.constants import IS_WINDOWS_PLATFORM from docker.utils.socket import next_frame_size from docker.utils.socket import read_exactly import pytest @@ -524,13 +525,13 @@ def test_get_file_stat_from_container(self): def test_copy_file_to_container(self): data = b'Deaf To All But The Song' - with tempfile.NamedTemporaryFile() as test_file: + with tempfile.NamedTemporaryFile(delete=False) as test_file: test_file.write(data) test_file.seek(0) ctnr = self.client.create_container( BUSYBOX, 'cat {0}'.format( - os.path.join('/vol1', os.path.basename(test_file.name)) + os.path.join('/vol1/', os.path.basename(test_file.name)) ), volumes=['/vol1'] ) @@ -822,11 +823,12 @@ def test_kill_with_dict_instead_of_id(self): self.assertEqual(state['Running'], False) def test_kill_with_signal(self): - container = self.client.create_container(BUSYBOX, ['sleep', '60']) - id = container['Id'] - self.client.start(id) + id = self.client.create_container(BUSYBOX, ['sleep', '60']) self.tmp_containers.append(id) - self.client.kill(id, signal=signal.SIGKILL) + self.client.start(id) + self.client.kill( + id, signal=signal.SIGKILL if not IS_WINDOWS_PLATFORM else 9 + ) exitcode = self.client.wait(id) self.assertNotEqual(exitcode, 0) container_info = self.client.inspect_container(id) @@ -902,28 +904,34 @@ def test_port(self): class ContainerTopTest(helpers.BaseTestCase): def test_top(self): container = self.client.create_container( - BUSYBOX, ['sleep', '60']) + BUSYBOX, ['sleep', '60'] + ) - id = container['Id'] + self.tmp_containers.append(container) self.client.start(container) - res = self.client.top(container['Id']) - self.assertEqual( - res['Titles'], - ['UID', 'PID', 'PPID', 'C', 'STIME', 'TTY', 'TIME', 'CMD'] - ) - self.assertEqual(len(res['Processes']), 1) - self.assertEqual(res['Processes'][0][7], 'sleep 60') - self.client.kill(id) + res = self.client.top(container) + if IS_WINDOWS_PLATFORM: + assert res['Titles'] == ['PID', 'USER', 'TIME', 'COMMAND'] + else: + assert res['Titles'] == [ + 'UID', 'PID', 'PPID', 'C', 'STIME', 'TTY', 'TIME', 'CMD' + ] + assert len(res['Processes']) == 1 + assert res['Processes'][0][-1] == 'sleep 60' + self.client.kill(container) + @pytest.mark.skipif( + IS_WINDOWS_PLATFORM, reason='No psargs support on windows' + ) def test_top_with_psargs(self): container = self.client.create_container( BUSYBOX, ['sleep', '60']) - id = container['Id'] + self.tmp_containers.append(container) self.client.start(container) - res = self.client.top(container['Id'], 'waux') + res = self.client.top(container, 'waux') self.assertEqual( res['Titles'], ['USER', 'PID', '%CPU', '%MEM', 'VSZ', 'RSS', @@ -931,7 +939,6 @@ def test_top_with_psargs(self): ) self.assertEqual(len(res['Processes']), 1) self.assertEqual(res['Processes'][0][10], 'sleep 60') - self.client.kill(id) class RestartContainerTest(helpers.BaseTestCase): diff --git a/tests/integration/image_test.py b/tests/integration/image_test.py index a61b58aeb7..24800f2122 100644 --- a/tests/integration/image_test.py +++ b/tests/integration/image_test.py @@ -57,12 +57,10 @@ def test_pull_streaming(self): self.client.remove_image('hello-world') except docker.errors.APIError: pass - stream = self.client.pull('hello-world', stream=True) + stream = self.client.pull('hello-world', stream=True, decode=True) self.tmp_imgs.append('hello-world') for chunk in stream: - if six.PY3: - chunk = chunk.decode('utf-8') - json.loads(chunk) # ensure chunk is a single, valid JSON blob + assert isinstance(chunk, dict) self.assertGreaterEqual( len(self.client.images('hello-world')), 1 ) @@ -152,7 +150,7 @@ def dummy_tar_stream(self, n_bytes): @contextlib.contextmanager def dummy_tar_file(self, n_bytes): '''Yields the name of a valid tar file of size n_bytes.''' - with tempfile.NamedTemporaryFile() as tar_file: + with tempfile.NamedTemporaryFile(delete=False) as tar_file: self.write_dummy_tar_content(n_bytes, tar_file) tar_file.seek(0) yield tar_file.name diff --git a/tests/integration/network_test.py b/tests/integration/network_test.py index 6726db4b49..5f852ab6b9 100644 --- a/tests/integration/network_test.py +++ b/tests/integration/network_test.py @@ -69,19 +69,18 @@ def test_create_network_with_ipam_config(self): assert ipam.pop('Options', None) is None - assert ipam == { - 'Driver': 'default', - 'Config': [{ - 'Subnet': "172.28.0.0/16", - 'IPRange': "172.28.5.0/24", - 'Gateway': "172.28.5.254", - 'AuxiliaryAddresses': { - "a": "172.28.1.5", - "b": "172.28.1.6", - "c": "172.28.1.7", - }, - }], - } + assert ipam['Driver'] == 'default' + + assert ipam['Config'] == [{ + 'Subnet': "172.28.0.0/16", + 'IPRange': "172.28.5.0/24", + 'Gateway': "172.28.5.254", + 'AuxiliaryAddresses': { + "a": "172.28.1.5", + "b": "172.28.1.6", + "c": "172.28.1.7", + }, + }] @requires_api_version('1.21') def test_create_network_with_host_driver_fails(self): diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index 389b5f5360..8faca6b158 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -86,7 +86,7 @@ def fake_delete(self, url, *args, **kwargs): def fake_read_from_socket(self, response, stream): return six.binary_type() -url_base = 'http+docker://localunixsocket/' +url_base = '{0}/'.format(fake_api.prefix) url_prefix = '{0}v{1}/'.format( url_base, docker.constants.DEFAULT_DOCKER_API_VERSION) @@ -422,6 +422,9 @@ def early_response_sending_handler(self, connection): data += connection.recv(2048) + @pytest.mark.skipif( + docker.constants.IS_WINDOWS_PLATFORM, reason='Unix only' + ) def test_early_stream_response(self): self.request_handler = self.early_response_sending_handler lines = [] diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 8871b85452..db3dd74eb4 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -270,8 +270,8 @@ def test_create_container_with_entrypoint(self): {'Content-Type': 'application/json'}) def test_create_container_with_cpu_shares(self): - self.client.create_container('busybox', 'ls', - cpu_shares=5) + with pytest.deprecated_call(): + self.client.create_container('busybox', 'ls', cpu_shares=5) args = fake_request.call_args self.assertEqual(args[0][1], @@ -316,8 +316,8 @@ def test_create_container_with_host_config_cpu_shares(self): {'Content-Type': 'application/json'}) def test_create_container_with_cpuset(self): - self.client.create_container('busybox', 'ls', - cpuset='0,1') + with pytest.deprecated_call(): + self.client.create_container('busybox', 'ls', cpuset='0,1') args = fake_request.call_args self.assertEqual(args[0][1], diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py index 1e9d318df5..65a8c42447 100644 --- a/tests/unit/fake_api.py +++ b/tests/unit/fake_api.py @@ -408,6 +408,9 @@ def post_fake_update_container(): # Maps real api url to fake response callback prefix = 'http+docker://localunixsocket' +if constants.IS_WINDOWS_PLATFORM: + prefix = 'http+docker://localnpipe' + fake_responses = { '{0}/version'.format(prefix): get_fake_raw_version, diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index a06cbea320..290874f416 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -13,7 +13,9 @@ import six from docker.client import Client -from docker.constants import DEFAULT_DOCKER_API_VERSION +from docker.constants import ( + DEFAULT_DOCKER_API_VERSION, IS_WINDOWS_PLATFORM +) from docker.errors import DockerException, InvalidVersion from docker.utils import ( parse_repository_tag, parse_host, convert_filters, kwargs_from_env, @@ -809,6 +811,12 @@ def test_build_port_bindings_with_nonmatching_internal_port_ranges(self): self.assertEqual(port_bindings["2000"], [("127.0.0.1", "2000")]) +def convert_paths(collection): + if not IS_WINDOWS_PLATFORM: + return collection + return set(map(lambda x: x.replace('/', '\\'), collection)) + + class ExcludePathsTest(base.BaseTestCase): dirs = [ 'foo', @@ -843,7 +851,7 @@ def exclude(self, patterns, dockerfile=None): return set(exclude_paths(self.base, patterns, dockerfile=dockerfile)) def test_no_excludes(self): - assert self.exclude(['']) == self.all_paths + assert self.exclude(['']) == convert_paths(self.all_paths) def test_no_dupes(self): paths = exclude_paths(self.base, ['!a.py']) @@ -858,7 +866,9 @@ def test_exclude_dockerfile_dockerignore(self): Dockerfile and/or .dockerignore, don't exclude them from the actual tar file. """ - assert self.exclude(['Dockerfile', '.dockerignore']) == self.all_paths + assert self.exclude(['Dockerfile', '.dockerignore']) == convert_paths( + self.all_paths + ) def test_exclude_custom_dockerfile(self): """ @@ -877,94 +887,116 @@ def test_exclude_dockerfile_child(self): assert 'foo/a.py' not in includes def test_single_filename(self): - assert self.exclude(['a.py']) == self.all_paths - set(['a.py']) + assert self.exclude(['a.py']) == convert_paths( + self.all_paths - set(['a.py']) + ) def test_single_filename_leading_dot_slash(self): - assert self.exclude(['./a.py']) == self.all_paths - set(['a.py']) + assert self.exclude(['./a.py']) == convert_paths( + self.all_paths - set(['a.py']) + ) # As odd as it sounds, a filename pattern with a trailing slash on the # end *will* result in that file being excluded. def test_single_filename_trailing_slash(self): - assert self.exclude(['a.py/']) == self.all_paths - set(['a.py']) + assert self.exclude(['a.py/']) == convert_paths( + self.all_paths - set(['a.py']) + ) def test_wildcard_filename_start(self): - assert self.exclude(['*.py']) == self.all_paths - set([ - 'a.py', 'b.py', 'cde.py', - ]) + assert self.exclude(['*.py']) == convert_paths( + self.all_paths - set(['a.py', 'b.py', 'cde.py']) + ) def test_wildcard_with_exception(self): - assert self.exclude(['*.py', '!b.py']) == self.all_paths - set([ - 'a.py', 'cde.py', - ]) + assert self.exclude(['*.py', '!b.py']) == convert_paths( + self.all_paths - set(['a.py', 'cde.py']) + ) def test_wildcard_with_wildcard_exception(self): - assert self.exclude(['*.*', '!*.go']) == self.all_paths - set([ - 'a.py', 'b.py', 'cde.py', 'Dockerfile.alt', - ]) + assert self.exclude(['*.*', '!*.go']) == convert_paths( + self.all_paths - set([ + 'a.py', 'b.py', 'cde.py', 'Dockerfile.alt', + ]) + ) def test_wildcard_filename_end(self): - assert self.exclude(['a.*']) == self.all_paths - set(['a.py', 'a.go']) + assert self.exclude(['a.*']) == convert_paths( + self.all_paths - set(['a.py', 'a.go']) + ) def test_question_mark(self): - assert self.exclude(['?.py']) == self.all_paths - set(['a.py', 'b.py']) + assert self.exclude(['?.py']) == convert_paths( + self.all_paths - set(['a.py', 'b.py']) + ) def test_single_subdir_single_filename(self): - assert self.exclude(['foo/a.py']) == self.all_paths - set(['foo/a.py']) + 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']) == self.all_paths - set([ - 'foo/a.py', - ]) + assert self.exclude(['foo/whoops/../a.py']) == convert_paths( + self.all_paths - set(['foo/a.py']) + ) def test_single_subdir_wildcard_filename(self): - assert self.exclude(['foo/*.py']) == self.all_paths - set([ - 'foo/a.py', 'foo/b.py', - ]) + assert self.exclude(['foo/*.py']) == convert_paths( + self.all_paths - set(['foo/a.py', 'foo/b.py']) + ) def test_wildcard_subdir_single_filename(self): - assert self.exclude(['*/a.py']) == self.all_paths - set([ - 'foo/a.py', 'bar/a.py', - ]) + assert self.exclude(['*/a.py']) == convert_paths( + self.all_paths - set(['foo/a.py', 'bar/a.py']) + ) def test_wildcard_subdir_wildcard_filename(self): - assert self.exclude(['*/*.py']) == self.all_paths - set([ - 'foo/a.py', 'foo/b.py', 'bar/a.py', - ]) + assert self.exclude(['*/*.py']) == convert_paths( + self.all_paths - set(['foo/a.py', 'foo/b.py', 'bar/a.py']) + ) def test_directory(self): - assert self.exclude(['foo']) == self.all_paths - set([ - 'foo', 'foo/a.py', 'foo/b.py', - 'foo/bar', 'foo/bar/a.py', 'foo/Dockerfile3' - ]) + assert self.exclude(['foo']) == convert_paths( + self.all_paths - set([ + 'foo', 'foo/a.py', 'foo/b.py', 'foo/bar', 'foo/bar/a.py', + 'foo/Dockerfile3' + ]) + ) def test_directory_with_trailing_slash(self): - assert self.exclude(['foo']) == self.all_paths - set([ - 'foo', 'foo/a.py', 'foo/b.py', - 'foo/bar', 'foo/bar/a.py', 'foo/Dockerfile3' - ]) + assert self.exclude(['foo']) == convert_paths( + self.all_paths - set([ + 'foo', 'foo/a.py', 'foo/b.py', + 'foo/bar', 'foo/bar/a.py', 'foo/Dockerfile3' + ]) + ) def test_directory_with_single_exception(self): - assert self.exclude(['foo', '!foo/bar/a.py']) == self.all_paths - set([ - 'foo/a.py', 'foo/b.py', 'foo', 'foo/bar', - 'foo/Dockerfile3' - ]) + assert self.exclude(['foo', '!foo/bar/a.py']) == convert_paths( + self.all_paths - set([ + 'foo/a.py', 'foo/b.py', 'foo', 'foo/bar', + 'foo/Dockerfile3' + ]) + ) def test_directory_with_subdir_exception(self): - assert self.exclude(['foo', '!foo/bar']) == self.all_paths - set([ - 'foo/a.py', 'foo/b.py', 'foo', - 'foo/Dockerfile3' - ]) + assert self.exclude(['foo', '!foo/bar']) == convert_paths( + self.all_paths - set([ + 'foo/a.py', 'foo/b.py', 'foo', 'foo/Dockerfile3' + ]) + ) def test_directory_with_wildcard_exception(self): - assert self.exclude(['foo', '!foo/*.py']) == self.all_paths - set([ - 'foo/bar', 'foo/bar/a.py', 'foo', - 'foo/Dockerfile3' - ]) + assert self.exclude(['foo', '!foo/*.py']) == convert_paths( + self.all_paths - set([ + 'foo/bar', 'foo/bar/a.py', 'foo', 'foo/Dockerfile3' + ]) + ) def test_subdirectory(self): - assert self.exclude(['foo/bar']) == self.all_paths - set([ - 'foo/bar', 'foo/bar/a.py', - ]) + assert self.exclude(['foo/bar']) == convert_paths( + self.all_paths - set(['foo/bar', 'foo/bar/a.py']) + ) class TarTest(base.Cleanup, base.BaseTestCase): @@ -1023,6 +1055,7 @@ def test_tar_with_empty_directory(self): tar_data = tarfile.open(fileobj=archive) self.assertEqual(sorted(tar_data.getnames()), ['bar', 'foo']) + @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows') def test_tar_with_file_symlinks(self): base = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base) @@ -1036,6 +1069,7 @@ def test_tar_with_file_symlinks(self): sorted(tar_data.getnames()), ['bar', 'bar/foo', 'foo'] ) + @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows') def test_tar_with_directory_symlinks(self): base = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base) From cf4aa9c89ec950ed1d4dbb12b23d6584e29719f0 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 13 Oct 2016 16:45:01 -0700 Subject: [PATCH 10/11] Fix dockerignore exclusion logic on Windows Signed-off-by: Joffrey F --- docker/utils/utils.py | 4 ++-- tests/integration/container_test.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 72706dfb38..8d55b574d1 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -194,8 +194,8 @@ def match_path(path, pattern): if pattern: pattern = os.path.relpath(pattern) - pattern_components = pattern.split('/') - path_components = path.split('/')[:len(pattern_components)] + pattern_components = pattern.split(os.path.sep) + path_components = path.split(os.path.sep)[:len(pattern_components)] return fnmatch('/'.join(path_components), pattern) diff --git a/tests/integration/container_test.py b/tests/integration/container_test.py index ae2a701c1b..e390acbf42 100644 --- a/tests/integration/container_test.py +++ b/tests/integration/container_test.py @@ -415,6 +415,9 @@ def setUp(self): ['touch', os.path.join(self.mount_dest, self.filename)], ) + @pytest.mark.xfail( + IS_WINDOWS_PLATFORM, reason='Test not designed for Windows platform' + ) def test_create_with_binds_rw(self): container = self.run_with_volume( @@ -430,6 +433,9 @@ def test_create_with_binds_rw(self): inspect_data = self.client.inspect_container(container) self.check_container_data(inspect_data, True) + @pytest.mark.xfail( + IS_WINDOWS_PLATFORM, reason='Test not designed for Windows platform' + ) def test_create_with_binds_ro(self): self.run_with_volume( False, From 413832f468447aab95400ca2a8c8b5d6743cdadf Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 14 Oct 2016 17:19:31 -0700 Subject: [PATCH 11/11] More changelog entries Signed-off-by: Joffrey F --- docs/change_log.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/change_log.md b/docs/change_log.md index 17490d34d9..de6303746c 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -17,10 +17,13 @@ Change Log * Fixed a bug where some `environment` values in `create_container` containing unicode characters would raise an encoding error. * Fixed a number of issues tied with named pipe transport on Windows. +* Fixed a bug where inclusion patterns in `.dockerignore` would cause some + excluded files to appear in the build context on Windows. ### Miscellaneous * Adjusted version requirements for the `requests` library. +* It is now possible to run the docker-py test suite on Windows. 1.10.3