From 8da5c1b1e78d89284a59df0e64eaa1cd1e37fdd7 Mon Sep 17 00:00:00 2001 From: Alexey Rokhin Date: Thu, 23 Mar 2017 16:24:04 +0300 Subject: [PATCH 01/16] Add cpu_count, cpu_percent, cpus parameters to container HostConfig. Signed-off-by: Alexey Rokhin --- docker/models/containers.py | 6 ++++++ docker/types/containers.py | 27 +++++++++++++++++++++++++- tests/unit/api_container_test.py | 31 ++++++++++++++++++++++++++++++ tests/unit/dockertypes_test.py | 33 ++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index 0d328e72a9..5ad16cc414 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -456,10 +456,13 @@ def run(self, image, command=None, stdout=True, stderr=False, cap_add (list of str): Add kernel capabilities. For example, ``["SYS_ADMIN", "MKNOD"]``. cap_drop (list of str): Drop kernel capabilities. + cpu_count (int): CPU count (Windows only). cpu_group (int): The length of a CPU period in microseconds. + cpu_percent (int): CPU percent (Windows only). cpu_period (int): Microseconds of CPU time that the container can get in a CPU period. cpu_shares (int): CPU shares (relative weight). + cpus (float): Number of CPUs. cpuset_cpus (str): CPUs in which to allow execution (``0-3``, ``0,1``). detach (bool): Run container in the background and return a @@ -803,9 +806,12 @@ def prune(self, filters=None): 'cap_add', 'cap_drop', 'cgroup_parent', + 'cpu_count', + 'cpu_percent', 'cpu_period', 'cpu_quota', 'cpu_shares', + 'cpus', 'cpuset_cpus', 'device_read_bps', 'device_read_iops', diff --git a/docker/types/containers.py b/docker/types/containers.py index 5a5079a81a..57d2b7975c 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -118,7 +118,8 @@ def __init__(self, version, binds=None, port_bindings=None, tmpfs=None, oom_score_adj=None, dns_opt=None, cpu_shares=None, cpuset_cpus=None, userns_mode=None, pids_limit=None, isolation=None, auto_remove=False, storage_opt=None, - init=None, init_path=None): + init=None, init_path=None, + cpu_count=None, cpu_percent=None, cpus=None): if mem_limit is not None: self['Memory'] = parse_bytes(mem_limit) @@ -428,6 +429,30 @@ def __init__(self, version, binds=None, port_bindings=None, raise host_config_version_error('init_path', '1.25') self['InitPath'] = init_path + if cpu_count: + if not isinstance(cpu_count, int): + raise host_config_type_error('cpu_count', cpu_count, 'int') + if version_lt(version, '1.25'): + raise host_config_version_error('cpu_count', '1.25') + + self['CpuCount'] = cpu_count + + if cpu_percent: + if not isinstance(cpu_percent, int): + raise host_config_type_error('cpu_percent', cpu_percent, 'int') + if version_lt(version, '1.25'): + raise host_config_version_error('cpu_percent', '1.25') + + self['CpuPercent'] = cpu_percent + + if cpus: + if not isinstance(cpus, (float, int)): + raise host_config_type_error('cpus', cpus, 'float') + if version_lt(version, '1.25'): + raise host_config_version_error('cpus', '1.25') + + self['NanoCpus'] = int(1000000000 * cpus) + def host_config_type_error(param, param_value, expected): error_msg = 'Invalid type for {0} param: expected {1} but found {2}' diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index 51d6678151..dc169a3b85 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -1155,6 +1155,37 @@ def test_create_container_with_unicode_envvars(self): self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data'])['Env'], expected) + @requires_api_version('1.25') + def test_create_container_with_host_config_cpus(self): + self.client.create_container( + 'busybox', 'ls', host_config=self.client.create_host_config( + cpu_count=1, + cpu_percent=20, + cpus=10 + ) + ) + + args = fake_request.call_args + self.assertEqual(args[0][1], + url_prefix + 'containers/create') + + self.assertEqual(json.loads(args[1]['data']), + json.loads(''' + {"Tty": false, "Image": "busybox", + "Cmd": ["ls"], "AttachStdin": false, + "AttachStderr": true, + "AttachStdout": true, "OpenStdin": false, + "StdinOnce": false, + "NetworkDisabled": false, + "HostConfig": { + "CpuCount": 1, + "CpuPercent": 20, + "NanoCpus": 10000000000, + "NetworkMode": "default" + }}''')) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + class ContainerTest(BaseAPIClientTest): def test_list_containers(self): diff --git a/tests/unit/dockertypes_test.py b/tests/unit/dockertypes_test.py index 5c470ffa2f..60aeb50cae 100644 --- a/tests/unit/dockertypes_test.py +++ b/tests/unit/dockertypes_test.py @@ -165,6 +165,39 @@ def test_create_host_config_invalid_mem_swappiness(self): with pytest.raises(TypeError): create_host_config(version='1.24', mem_swappiness='40') + def test_create_host_config_invalid_cpu_count_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpu_count='1') + + def test_create_host_config_with_cpu_count(self): + config = create_host_config(version='1.25', cpu_count=2) + self.assertEqual(config.get('CpuCount'), 2) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpu_count=1)) + + def test_create_host_config_invalid_cpu_percent_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpu_percent='1') + + def test_create_host_config_with_cpu_percent(self): + config = create_host_config(version='1.25', cpu_percent=15) + self.assertEqual(config.get('CpuPercent'), 15) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpu_percent=10)) + + def test_create_host_config_invalid_cpus_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpus='0') + + def test_create_host_config_with_cpus(self): + config = create_host_config(version='1.25', cpus=100) + self.assertEqual(config.get('NanoCpus'), 100000000000) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpus=1)) + class UlimitTest(unittest.TestCase): def test_create_host_config_dict_ulimit(self): From e89fad28f821a866059913099e4785bcd34bec1e Mon Sep 17 00:00:00 2001 From: Alexey Rokhin Date: Thu, 23 Mar 2017 16:24:04 +0300 Subject: [PATCH 02/16] Add cpu_count, cpu_percent, cpus parameters to container HostConfig. Signed-off-by: Alexey Rokhin --- docker/models/containers.py | 6 ++++++ docker/types/containers.py | 27 +++++++++++++++++++++++++- tests/unit/api_container_test.py | 31 ++++++++++++++++++++++++++++++ tests/unit/dockertypes_test.py | 33 ++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index 0d328e72a9..5ad16cc414 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -456,10 +456,13 @@ def run(self, image, command=None, stdout=True, stderr=False, cap_add (list of str): Add kernel capabilities. For example, ``["SYS_ADMIN", "MKNOD"]``. cap_drop (list of str): Drop kernel capabilities. + cpu_count (int): CPU count (Windows only). cpu_group (int): The length of a CPU period in microseconds. + cpu_percent (int): CPU percent (Windows only). cpu_period (int): Microseconds of CPU time that the container can get in a CPU period. cpu_shares (int): CPU shares (relative weight). + cpus (float): Number of CPUs. cpuset_cpus (str): CPUs in which to allow execution (``0-3``, ``0,1``). detach (bool): Run container in the background and return a @@ -803,9 +806,12 @@ def prune(self, filters=None): 'cap_add', 'cap_drop', 'cgroup_parent', + 'cpu_count', + 'cpu_percent', 'cpu_period', 'cpu_quota', 'cpu_shares', + 'cpus', 'cpuset_cpus', 'device_read_bps', 'device_read_iops', diff --git a/docker/types/containers.py b/docker/types/containers.py index 5a5079a81a..57d2b7975c 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -118,7 +118,8 @@ def __init__(self, version, binds=None, port_bindings=None, tmpfs=None, oom_score_adj=None, dns_opt=None, cpu_shares=None, cpuset_cpus=None, userns_mode=None, pids_limit=None, isolation=None, auto_remove=False, storage_opt=None, - init=None, init_path=None): + init=None, init_path=None, + cpu_count=None, cpu_percent=None, cpus=None): if mem_limit is not None: self['Memory'] = parse_bytes(mem_limit) @@ -428,6 +429,30 @@ def __init__(self, version, binds=None, port_bindings=None, raise host_config_version_error('init_path', '1.25') self['InitPath'] = init_path + if cpu_count: + if not isinstance(cpu_count, int): + raise host_config_type_error('cpu_count', cpu_count, 'int') + if version_lt(version, '1.25'): + raise host_config_version_error('cpu_count', '1.25') + + self['CpuCount'] = cpu_count + + if cpu_percent: + if not isinstance(cpu_percent, int): + raise host_config_type_error('cpu_percent', cpu_percent, 'int') + if version_lt(version, '1.25'): + raise host_config_version_error('cpu_percent', '1.25') + + self['CpuPercent'] = cpu_percent + + if cpus: + if not isinstance(cpus, (float, int)): + raise host_config_type_error('cpus', cpus, 'float') + if version_lt(version, '1.25'): + raise host_config_version_error('cpus', '1.25') + + self['NanoCpus'] = int(1000000000 * cpus) + def host_config_type_error(param, param_value, expected): error_msg = 'Invalid type for {0} param: expected {1} but found {2}' diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index 51d6678151..dc169a3b85 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -1155,6 +1155,37 @@ def test_create_container_with_unicode_envvars(self): self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data'])['Env'], expected) + @requires_api_version('1.25') + def test_create_container_with_host_config_cpus(self): + self.client.create_container( + 'busybox', 'ls', host_config=self.client.create_host_config( + cpu_count=1, + cpu_percent=20, + cpus=10 + ) + ) + + args = fake_request.call_args + self.assertEqual(args[0][1], + url_prefix + 'containers/create') + + self.assertEqual(json.loads(args[1]['data']), + json.loads(''' + {"Tty": false, "Image": "busybox", + "Cmd": ["ls"], "AttachStdin": false, + "AttachStderr": true, + "AttachStdout": true, "OpenStdin": false, + "StdinOnce": false, + "NetworkDisabled": false, + "HostConfig": { + "CpuCount": 1, + "CpuPercent": 20, + "NanoCpus": 10000000000, + "NetworkMode": "default" + }}''')) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + class ContainerTest(BaseAPIClientTest): def test_list_containers(self): diff --git a/tests/unit/dockertypes_test.py b/tests/unit/dockertypes_test.py index 5c470ffa2f..60aeb50cae 100644 --- a/tests/unit/dockertypes_test.py +++ b/tests/unit/dockertypes_test.py @@ -165,6 +165,39 @@ def test_create_host_config_invalid_mem_swappiness(self): with pytest.raises(TypeError): create_host_config(version='1.24', mem_swappiness='40') + def test_create_host_config_invalid_cpu_count_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpu_count='1') + + def test_create_host_config_with_cpu_count(self): + config = create_host_config(version='1.25', cpu_count=2) + self.assertEqual(config.get('CpuCount'), 2) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpu_count=1)) + + def test_create_host_config_invalid_cpu_percent_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpu_percent='1') + + def test_create_host_config_with_cpu_percent(self): + config = create_host_config(version='1.25', cpu_percent=15) + self.assertEqual(config.get('CpuPercent'), 15) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpu_percent=10)) + + def test_create_host_config_invalid_cpus_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.25', cpus='0') + + def test_create_host_config_with_cpus(self): + config = create_host_config(version='1.25', cpus=100) + self.assertEqual(config.get('NanoCpus'), 100000000000) + self.assertRaises( + InvalidVersion, lambda: create_host_config( + version='1.24', cpus=1)) + class UlimitTest(unittest.TestCase): def test_create_host_config_dict_ulimit(self): From dc485ef6bd4fe98f0efbbe0eb733c7ccfc7f126f Mon Sep 17 00:00:00 2001 From: alex-dr Date: Wed, 22 Mar 2017 00:41:32 -0400 Subject: [PATCH 03/16] Fix APIError status_code property for client/server errors requests.Response objects evaluate as falsy when the status_code attribute is in the 400-500 range. Therefore we are assured that prior to this change, APIError would show `is_server_error() == False` when generated with a 500-level response and `is_client_error() == False` when generated with a 400-level response. This is not desirable. Added some seemingly dry (not DRY) unit tests to ensure nothing silly slips back in here. Signed-off-by: alex-dr --- docker/errors.py | 2 +- tests/unit/errors_test.py | 65 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/docker/errors.py b/docker/errors.py index d9b197d1a3..03b89c11e3 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -59,7 +59,7 @@ def __str__(self): @property def status_code(self): - if self.response: + if self.response is not None: return self.response.status_code def is_client_error(self): diff --git a/tests/unit/errors_test.py b/tests/unit/errors_test.py index 876ede3b5f..b78af4e109 100644 --- a/tests/unit/errors_test.py +++ b/tests/unit/errors_test.py @@ -1,5 +1,7 @@ import unittest +import requests + from docker.errors import (APIError, DockerException, create_unexpected_kwargs_error) @@ -11,6 +13,69 @@ def test_api_error_is_caught_by_dockerexception(self): except DockerException: pass + def test_status_code_200(self): + """The status_code property is present with 200 response.""" + resp = requests.Response() + resp.status_code = 200 + err = APIError('', response=resp) + assert err.status_code == 200 + + def test_status_code_400(self): + """The status_code property is present with 400 response.""" + resp = requests.Response() + resp.status_code = 400 + err = APIError('', response=resp) + assert err.status_code == 400 + + def test_status_code_500(self): + """The status_code property is present with 500 response.""" + resp = requests.Response() + resp.status_code = 500 + err = APIError('', response=resp) + assert err.status_code == 500 + + def test_is_server_error_200(self): + """Report not server error on 200 response.""" + resp = requests.Response() + resp.status_code = 200 + err = APIError('', response=resp) + assert err.is_server_error() is False + + def test_is_server_error_300(self): + """Report not server error on 300 response.""" + resp = requests.Response() + resp.status_code = 300 + err = APIError('', response=resp) + assert err.is_server_error() is False + + def test_is_server_error_400(self): + """Report not server error on 400 response.""" + resp = requests.Response() + resp.status_code = 400 + err = APIError('', response=resp) + assert err.is_server_error() is False + + def test_is_server_error_500(self): + """Report server error on 500 response.""" + resp = requests.Response() + resp.status_code = 500 + err = APIError('', response=resp) + assert err.is_server_error() is True + + def test_is_client_error_500(self): + """Report not client error on 500 response.""" + resp = requests.Response() + resp.status_code = 500 + err = APIError('', response=resp) + assert err.is_client_error() is False + + def test_is_client_error_400(self): + """Report client error on 400 response.""" + resp = requests.Response() + resp.status_code = 400 + err = APIError('', response=resp) + assert err.is_client_error() is True + class CreateUnexpectedKwargsErrorTest(unittest.TestCase): def test_create_unexpected_kwargs_error_single(self): From 5f9fd74ccdbe04d464e99139b7f910b1532da74e Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 22 Mar 2017 16:13:33 -0700 Subject: [PATCH 04/16] Set infinite timeout for the `events` method Signed-off-by: Joffrey F --- docker/api/daemon.py | 3 ++- tests/integration/api_service_test.py | 4 ++-- tests/unit/api_test.py | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docker/api/daemon.py b/docker/api/daemon.py index 00367bc309..91c777f092 100644 --- a/docker/api/daemon.py +++ b/docker/api/daemon.py @@ -68,9 +68,10 @@ def events(self, since=None, until=None, filters=None, decode=None): 'until': until, 'filters': filters } + url = self._url('/events') return self._stream_helper( - self._get(self._url('/events'), params=params, stream=True), + self._get(url, params=params, stream=True, timeout=None), decode=decode ) diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index 3bfabe91dd..6858ad0e54 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -363,7 +363,7 @@ def test_create_service_with_secret(self): self.tmp_secrets.append(secret_id) secret_ref = docker.types.SecretReference(secret_id, secret_name) container_spec = docker.types.ContainerSpec( - 'busybox', ['top'], secrets=[secret_ref] + 'busybox', ['sleep', '999'], secrets=[secret_ref] ) task_tmpl = docker.types.TaskTemplate(container_spec) name = self.get_service_name() @@ -388,7 +388,7 @@ def test_create_service_with_unicode_secret(self): self.tmp_secrets.append(secret_id) secret_ref = docker.types.SecretReference(secret_id, secret_name) container_spec = docker.types.ContainerSpec( - 'busybox', ['top'], secrets=[secret_ref] + 'busybox', ['sleep', '999'], secrets=[secret_ref] ) task_tmpl = docker.types.TaskTemplate(container_spec) name = self.get_service_name() diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index b632d209be..83848c524a 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -229,7 +229,7 @@ def test_events(self): url_prefix + 'events', params={'since': None, 'until': None, 'filters': None}, stream=True, - timeout=DEFAULT_TIMEOUT_SECONDS + timeout=None ) def test_events_with_since_until(self): @@ -249,7 +249,7 @@ def test_events_with_since_until(self): 'filters': None }, stream=True, - timeout=DEFAULT_TIMEOUT_SECONDS + timeout=None ) def test_events_with_filters(self): @@ -268,7 +268,7 @@ def test_events_with_filters(self): 'filters': expected_filters }, stream=True, - timeout=DEFAULT_TIMEOUT_SECONDS + timeout=None ) def _socket_path_for_client_session(self, client): From ac018dc7a3092e74d341b72c2e7dd06e262aa1a4 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 28 Mar 2017 15:04:05 -0700 Subject: [PATCH 05/16] Add reload() in docs for Container and Secret classes Signed-off-by: Joffrey F --- docs/change-log.md | 13 +++++++++++++ docs/containers.rst | 1 + docs/secrets.rst | 1 + 3 files changed, 15 insertions(+) diff --git a/docs/change-log.md b/docs/change-log.md index c04cd4be8f..5d9b05b37d 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,19 @@ Change log ========== +2.2.1 +----- + +[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/32?closed=1) + +### Bugfixes + +* Fixed a bug where the `status_code` attribute of `APIError` exceptions would + not reflect the expected value. +* Fixed an issue where the `events` method would time out unexpectedly if no + data was sent by the engine for a given amount of time. + + 2.2.0 ----- diff --git a/docs/containers.rst b/docs/containers.rst index 9b27a306b8..20529b0eff 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -40,6 +40,7 @@ Container objects .. automethod:: logs .. automethod:: pause .. automethod:: put_archive + .. automethod:: reload .. automethod:: remove .. automethod:: rename .. automethod:: resize diff --git a/docs/secrets.rst b/docs/secrets.rst index 49e149847d..d1c39f1a16 100644 --- a/docs/secrets.rst +++ b/docs/secrets.rst @@ -26,4 +26,5 @@ Secret objects The raw representation of this object from the server. + .. automethod:: reload .. automethod:: remove From ded2f9932f6c8f295db68e84bdc3d16b4080a96c Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 28 Mar 2017 16:22:35 -0700 Subject: [PATCH 06/16] Add 17.04 (CE) RC1 to list of engine versions to be tested Signed-off-by: Joffrey F --- Jenkinsfile | 6 +++--- Makefile | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index b5af1bada2..a61e6d5165 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"] +def dockerVersions = ["1.12.0", "1.13.1", "17.04.0-ce-rc1"] def buildImage = { name, buildargs, pyTag -> img = docker.image(name) @@ -35,8 +35,8 @@ def buildImages = { -> } def getAPIVersion = { engineVersion -> - def versionMap = ['1.12': '1.24', '1.13': '1.26'] - return versionMap[engineVersion.substring(0, 4)] + def versionMap = ['1.12.': '1.24', '1.13.': '1.26', '17.04': '1.27'] + return versionMap[engineVersion.substring(0, 5)] } def runTests = { Map settings -> diff --git a/Makefile b/Makefile index 411795848e..e4c64e71e7 100644 --- a/Makefile +++ b/Makefile @@ -41,14 +41,17 @@ 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.27 +TEST_ENGINE_VERSION ?= 17.04.0-ce-rc1 + .PHONY: integration-dind integration-dind: build build-py3 docker rm -vf dpy-dind || : - docker run -d --name dpy-dind --privileged dockerswarm/dind:1.13.1 docker daemon\ + docker run -d --name dpy-dind --privileged dockerswarm/dind:${TEST_ENGINE_VERSION} docker daemon\ -H tcp://0.0.0.0:2375 --experimental - docker run --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=1.26"\ + docker run --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\ --link=dpy-dind:docker docker-sdk-python py.test tests/integration - docker run --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=1.26"\ + docker run --rm --env="DOCKER_HOST=tcp://docker:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\ --link=dpy-dind:docker docker-sdk-python3 py.test tests/integration docker rm -vf dpy-dind @@ -57,14 +60,14 @@ integration-dind-ssl: build-dind-certs build build-py3 docker run -d --name dpy-dind-certs dpy-dind-certs docker run -d --env="DOCKER_HOST=tcp://localhost:2375" --env="DOCKER_TLS_VERIFY=1"\ --env="DOCKER_CERT_PATH=/certs" --volumes-from dpy-dind-certs --name dpy-dind-ssl\ - -v /tmp --privileged dockerswarm/dind:1.13.1 docker daemon --tlsverify\ + -v /tmp --privileged dockerswarm/dind:${TEST_ENGINE_VERSION} docker daemon --tlsverify\ --tlscacert=/certs/ca.pem --tlscert=/certs/server-cert.pem\ --tlskey=/certs/server-key.pem -H tcp://0.0.0.0:2375 --experimental docker run --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\ - --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=1.26"\ + --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\ --link=dpy-dind-ssl:docker docker-sdk-python py.test tests/integration docker run --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\ - --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=1.26"\ + --env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\ --link=dpy-dind-ssl:docker docker-sdk-python3 py.test tests/integration docker rm -vf dpy-dind-ssl dpy-dind-certs From 1d12c25d00f414e45d58af327e2d9c3f9aa1ae6d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 28 Mar 2017 16:29:41 -0700 Subject: [PATCH 07/16] Add appveyor.yml config Signed-off-by: Joffrey F --- appveyor.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..10e32a540b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,13 @@ + +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" + +# Build the binary after tests +build: false + +test_script: + - "tox" From 085d3d04a384abf362be049ce289a29ca49dd8a7 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 28 Mar 2017 16:58:51 -0700 Subject: [PATCH 08/16] Fix test issues Signed-off-by: Joffrey F --- appveyor.yml | 1 - docker/utils/build.py | 10 +++-- docker/utils/fnmatch.py | 9 ++--- tests/unit/utils_test.py | 80 +++++++++++++++++++--------------------- tox.ini | 2 +- 5 files changed, 48 insertions(+), 54 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 10e32a540b..1fc67cc024 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,3 @@ - version: '{branch}-{build}' install: diff --git a/docker/utils/build.py b/docker/utils/build.py index 6ba47b39fb..79b72495d9 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -1,5 +1,6 @@ import os +from ..constants import IS_WINDOWS_PLATFORM from .fnmatch import fnmatch from .utils import create_archive @@ -39,7 +40,7 @@ def exclude_paths(root, patterns, dockerfile=None): # If the Dockerfile is in a subdirectory that is excluded, get_paths # will not descend into it and the file will be skipped. This ensures # it doesn't happen. - set([dockerfile]) + set([dockerfile.replace('/', os.path.sep)]) if os.path.exists(os.path.join(root, dockerfile)) else set() ) @@ -130,9 +131,12 @@ def match_path(path, pattern): if pattern: pattern = os.path.relpath(pattern) + pattern_components = pattern.split(os.path.sep) + if len(pattern_components) == 1 and IS_WINDOWS_PLATFORM: + pattern_components = pattern.split('/') + if '**' not in pattern: - pattern_components = pattern.split(os.path.sep) path_components = path.split(os.path.sep)[:len(pattern_components)] else: path_components = path.split(os.path.sep) - return fnmatch('/'.join(path_components), pattern) + return fnmatch('/'.join(path_components), '/'.join(pattern_components)) diff --git a/docker/utils/fnmatch.py b/docker/utils/fnmatch.py index 80bdf77329..e95b63ceb1 100644 --- a/docker/utils/fnmatch.py +++ b/docker/utils/fnmatch.py @@ -39,15 +39,13 @@ def fnmatch(name, pat): If you don't want this, use fnmatchcase(FILENAME, PATTERN). """ - import os - name = os.path.normcase(name) - pat = os.path.normcase(pat) + name = name.lower() + pat = pat.lower() return fnmatchcase(name, pat) def fnmatchcase(name, pat): """Test whether FILENAME matches PATTERN, including case. - This is a version of fnmatch() which doesn't case-normalize its arguments. """ @@ -67,7 +65,6 @@ def translate(pat): There is no way to quote meta-characters. """ - recursive_mode = False i, n = 0, len(pat) res = '' @@ -100,7 +97,7 @@ def translate(pat): stuff = '\\' + stuff res = '%s[%s]' % (res, stuff) elif recursive_mode and c == '/': - res = res + '/?' + res = res + re.escape(c) + '?' else: res = res + re.escape(c) return res + '\Z(?ms)' diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 4c3c3664a2..25ed0f9b7f 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -618,9 +618,11 @@ def test_build_port_bindings_with_nonmatching_internal_port_ranges(self): def convert_paths(collection): - if not IS_WINDOWS_PLATFORM: - return collection - return set(map(lambda x: x.replace('/', '\\'), collection)) + return set(map(convert_path, collection)) + + +def convert_path(path): + return path.replace('/', os.path.sep) class ExcludePathsTest(unittest.TestCase): @@ -685,12 +687,12 @@ def test_exclude_custom_dockerfile(self): set(['Dockerfile.alt', '.dockerignore']) assert self.exclude(['*'], dockerfile='foo/Dockerfile3') == \ - set(['foo/Dockerfile3', '.dockerignore']) + convert_paths(set(['foo/Dockerfile3', '.dockerignore'])) def test_exclude_dockerfile_child(self): includes = self.exclude(['foo/'], dockerfile='foo/Dockerfile3') - assert 'foo/Dockerfile3' in includes - assert 'foo/a.py' not in includes + assert convert_path('foo/Dockerfile3') in includes + assert convert_path('foo/a.py') not in includes def test_single_filename(self): assert self.exclude(['a.py']) == convert_paths( @@ -917,6 +919,7 @@ def test_tar_with_directory_symlinks(self): sorted(tar_data.getnames()), ['bar', 'bar/foo', 'foo'] ) + @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No UNIX sockets on Win32') def test_tar_socket_file(self): base = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base) @@ -945,62 +948,53 @@ class ShouldCheckDirectoryTest(unittest.TestCase): ] def test_should_check_directory_not_excluded(self): - self.assertTrue( - should_check_directory('not_excluded', self.exclude_patterns, - self.include_patterns) + assert should_check_directory( + 'not_excluded', self.exclude_patterns, self.include_patterns ) - - self.assertTrue( - should_check_directory('dir/with', self.exclude_patterns, - self.include_patterns) + assert should_check_directory( + convert_path('dir/with'), self.exclude_patterns, + self.include_patterns ) def test_shoud_check_parent_directories_of_excluded(self): - self.assertTrue( - should_check_directory('dir', self.exclude_patterns, - self.include_patterns) + assert should_check_directory( + 'dir', self.exclude_patterns, self.include_patterns ) - self.assertTrue( - should_check_directory('dir/with', self.exclude_patterns, - self.include_patterns) + assert should_check_directory( + convert_path('dir/with'), self.exclude_patterns, + self.include_patterns ) def test_should_not_check_excluded_directories_with_no_exceptions(self): - self.assertFalse( - should_check_directory('exclude_rather_large_directory', - self.exclude_patterns, self.include_patterns - ) + assert not should_check_directory( + 'exclude_rather_large_directory', self.exclude_patterns, + self.include_patterns ) - self.assertFalse( - should_check_directory('dir/with/subdir_excluded', - self.exclude_patterns, self.include_patterns - ) + assert not should_check_directory( + convert_path('dir/with/subdir_excluded'), self.exclude_patterns, + self.include_patterns ) def test_should_check_excluded_directory_with_exceptions(self): - self.assertTrue( - should_check_directory('dir/with/exceptions', - self.exclude_patterns, self.include_patterns - ) + assert should_check_directory( + convert_path('dir/with/exceptions'), self.exclude_patterns, + self.include_patterns ) - self.assertTrue( - should_check_directory('dir/with/exceptions/in', - self.exclude_patterns, self.include_patterns - ) + assert should_check_directory( + convert_path('dir/with/exceptions/in'), self.exclude_patterns, + self.include_patterns ) def test_should_not_check_siblings_of_exceptions(self): - self.assertFalse( - should_check_directory('dir/with/exceptions/but_not_here', - self.exclude_patterns, self.include_patterns - ) + assert not should_check_directory( + convert_path('dir/with/exceptions/but_not_here'), + self.exclude_patterns, self.include_patterns ) def test_should_check_subdirectories_of_exceptions(self): - self.assertTrue( - should_check_directory('dir/with/exceptions/like_this_one/subdir', - self.exclude_patterns, self.include_patterns - ) + assert should_check_directory( + convert_path('dir/with/exceptions/like_this_one/subdir'), + self.exclude_patterns, self.include_patterns ) diff --git a/tox.ini b/tox.ini index 1a41c6edac..5a5e5415ad 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skipsdist=True [testenv] usedevelop=True commands = - py.test --cov=docker {posargs:tests/unit} + py.test -v --cov=docker {posargs:tests/unit} deps = -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt From 372674aba2ad4e4ca854e7abb4d507c04e444065 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 3 Apr 2017 16:45:09 -0700 Subject: [PATCH 09/16] Update mentions of the default API version in docs Signed-off-by: Joffrey F --- docker/api/client.py | 2 +- docker/client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/api/client.py b/docker/api/client.py index 99d7879cb8..749b061dce 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -75,7 +75,7 @@ class APIClient( base_url (str): URL to the Docker server. For example, ``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``. version (str): The version of the API to use. Set to ``auto`` to - automatically detect the server's version. Default: ``1.24`` + automatically detect the server's version. Default: ``1.26`` timeout (int): Default timeout for API calls, in seconds. tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass ``True`` to enable it with default options, or pass a diff --git a/docker/client.py b/docker/client.py index 151e1944ef..09abd63322 100644 --- a/docker/client.py +++ b/docker/client.py @@ -24,7 +24,7 @@ class DockerClient(object): base_url (str): URL to the Docker server. For example, ``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``. version (str): The version of the API to use. Set to ``auto`` to - automatically detect the server's version. Default: ``1.24`` + automatically detect the server's version. Default: ``1.26`` timeout (int): Default timeout for API calls, in seconds. tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass ``True`` to enable it with default options, or pass a @@ -58,7 +58,7 @@ def from_env(cls, **kwargs): Args: version (str): The version of the API to use. Set to ``auto`` to - automatically detect the server's version. Default: ``1.24`` + automatically detect the server's version. Default: ``1.26`` timeout (int): Default timeout for API calls, in seconds. ssl_version (int): A valid `SSL version`_. assert_hostname (bool): Verify the hostname of the server. From 889fefc2eda8f95e3ccd5d2e640a702f81281b7d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 5 Apr 2017 17:07:43 -0700 Subject: [PATCH 10/16] 2.3.0-dev 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 b2474bd739..320dae622d 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.2.0" +version = "2.3.0-dev" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) From 8c4cde898407904354056c01ed01ca563d6f511f Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 5 Apr 2017 17:05:08 -0700 Subject: [PATCH 11/16] Add support for volume_driver in HostConfig Some cleanup in ContainerConfig + warning if volume_driver is provided (API>1.20) Signed-off-by: Joffrey F --- docker/types/containers.py | 91 ++++++++++++++++++-------------- tests/unit/api_container_test.py | 11 ++-- tests/unit/dockertypes_test.py | 25 ++++++++- 3 files changed, 79 insertions(+), 48 deletions(-) diff --git a/docker/types/containers.py b/docker/types/containers.py index 57d2b7975c..d6fff4a0e5 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -118,7 +118,7 @@ def __init__(self, version, binds=None, port_bindings=None, tmpfs=None, oom_score_adj=None, dns_opt=None, cpu_shares=None, cpuset_cpus=None, userns_mode=None, pids_limit=None, isolation=None, auto_remove=False, storage_opt=None, - init=None, init_path=None, + init=None, init_path=None, volume_driver=None, cpu_count=None, cpu_percent=None, cpus=None): if mem_limit is not None: @@ -429,6 +429,10 @@ def __init__(self, version, binds=None, port_bindings=None, raise host_config_version_error('init_path', '1.25') self['InitPath'] = init_path + if volume_driver is not None: + if version_lt(version, '1.21'): + raise host_config_version_error('volume_driver', '1.21') + self['VolumeDriver'] = volume_driver if cpu_count: if not isinstance(cpu_count, int): raise host_config_type_error('cpu_count', cpu_count, 'int') @@ -454,6 +458,7 @@ def __init__(self, version, binds=None, port_bindings=None, self['NanoCpus'] = int(1000000000 * cpus) + def host_config_type_error(param, param_value, expected): error_msg = 'Invalid type for {0} param: expected {1} but found {2}' return TypeError(error_msg.format(param, expected, type(param_value))) @@ -481,43 +486,27 @@ def __init__( stop_signal=None, networking_config=None, healthcheck=None, stop_timeout=None ): - if isinstance(command, six.string_types): - command = split_command(command) - - if isinstance(entrypoint, six.string_types): - entrypoint = split_command(entrypoint) - - if isinstance(environment, dict): - environment = format_environment(environment) - - if labels is not None and version_lt(version, '1.18'): - raise errors.InvalidVersion( - 'labels were only introduced in API version 1.18' - ) + if version_gte(version, '1.10'): + message = ('{0!r} parameter has no effect on create_container().' + ' It has been moved to host_config') + if dns is not None: + raise errors.InvalidVersion(message.format('dns')) + if volumes_from is not None: + raise errors.InvalidVersion(message.format('volumes_from')) - if cpuset is not None or cpu_shares is not None: - if version_gte(version, '1.18'): + if version_lt(version, '1.18'): + if labels is not None: + raise errors.InvalidVersion( + 'labels were only introduced in API version 1.18' + ) + else: + if cpuset is not None or cpu_shares is not None: warnings.warn( 'The cpuset_cpus and cpu_shares options have been moved to' ' host_config in API version 1.18, and will be removed', DeprecationWarning ) - if stop_signal is not None and version_lt(version, '1.21'): - raise errors.InvalidVersion( - 'stop_signal was only introduced in API version 1.21' - ) - - if stop_timeout is not None and version_lt(version, '1.25'): - raise errors.InvalidVersion( - 'stop_timeout was only introduced in API version 1.25' - ) - - if healthcheck is not None and version_lt(version, '1.24'): - raise errors.InvalidVersion( - 'Health options were only introduced in API version 1.24' - ) - if version_lt(version, '1.19'): if volume_driver is not None: raise errors.InvalidVersion( @@ -538,6 +527,38 @@ def __init__( 'version 1.19' ) + if version_lt(version, '1.21'): + if stop_signal is not None: + raise errors.InvalidVersion( + 'stop_signal was only introduced in API version 1.21' + ) + else: + if volume_driver is not None: + warnings.warn( + 'The volume_driver option has been moved to' + ' host_config in API version 1.21, and will be removed', + DeprecationWarning + ) + + if stop_timeout is not None and version_lt(version, '1.25'): + raise errors.InvalidVersion( + 'stop_timeout was only introduced in API version 1.25' + ) + + if healthcheck is not None and version_lt(version, '1.24'): + raise errors.InvalidVersion( + 'Health options were only introduced in API version 1.24' + ) + + if isinstance(command, six.string_types): + command = split_command(command) + + if isinstance(entrypoint, six.string_types): + entrypoint = split_command(entrypoint) + + if isinstance(environment, dict): + environment = format_environment(environment) + if isinstance(labels, list): labels = dict((lbl, six.text_type('')) for lbl in labels) @@ -591,14 +612,6 @@ def __init__( attach_stdin = True stdin_once = True - if version_gte(version, '1.10'): - message = ('{0!r} parameter has no effect on create_container().' - ' It has been moved to host_config') - if dns is not None: - raise errors.InvalidVersion(message.format('dns')) - if volumes_from is not None: - raise errors.InvalidVersion(message.format('volumes_from')) - self.update({ 'Hostname': hostname, 'Domainname': domainname, diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index dc169a3b85..15f19f2e0b 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -407,11 +407,8 @@ def test_create_container_with_volumes_from(self): {'Content-Type': 'application/json'}) def test_create_container_empty_volumes_from(self): - self.client.create_container('busybox', 'true', volumes_from=[]) - - args = fake_request.call_args - data = json.loads(args[1]['data']) - self.assertTrue('VolumesFrom' not in data) + with pytest.raises(docker.errors.InvalidVersion): + self.client.create_container('busybox', 'true', volumes_from=[]) def test_create_named_container(self): self.client.create_container('busybox', 'true', @@ -978,11 +975,11 @@ def test_create_container_with_named_volume(self): self.client.create_container( 'busybox', 'true', host_config=self.client.create_host_config( + volume_driver='foodriver', binds={volume_name: { "bind": mount_dest, "ro": False }}), - volume_driver='foodriver', ) args = fake_request.call_args @@ -990,8 +987,8 @@ def test_create_container_with_named_volume(self): args[0][1], url_prefix + 'containers/create' ) expected_payload = self.base_create_payload() - expected_payload['VolumeDriver'] = 'foodriver' expected_payload['HostConfig'] = self.client.create_host_config() + expected_payload['HostConfig']['VolumeDriver'] = 'foodriver' expected_payload['HostConfig']['Binds'] = ["name:/mnt:rw"] self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual(args[1]['headers'], diff --git a/tests/unit/dockertypes_test.py b/tests/unit/dockertypes_test.py index 60aeb50cae..fd7c8816d6 100644 --- a/tests/unit/dockertypes_test.py +++ b/tests/unit/dockertypes_test.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- import unittest +import warnings import pytest from docker.constants import DEFAULT_DOCKER_API_VERSION from docker.errors import InvalidArgument, InvalidVersion from docker.types import ( - EndpointConfig, HostConfig, IPAMConfig, IPAMPool, LogConfig, Mount, - ServiceMode, Ulimit, + ContainerConfig, EndpointConfig, HostConfig, IPAMConfig, IPAMPool, + LogConfig, Mount, ServiceMode, Ulimit, ) try: @@ -198,6 +199,26 @@ def test_create_host_config_with_cpus(self): InvalidVersion, lambda: create_host_config( version='1.24', cpus=1)) + def test_create_host_config_with_volume_driver(self): + with pytest.raises(InvalidVersion): + create_host_config(version='1.20', volume_driver='local') + + config = create_host_config(version='1.21', volume_driver='local') + assert config.get('VolumeDriver') == 'local' + + +class ContainerConfigTest(unittest.TestCase): + def test_create_container_config_volume_driver_warning(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + ContainerConfig( + version='1.21', image='scratch', command=None, + volume_driver='local' + ) + + assert len(w) == 1 + assert 'The volume_driver option has been moved' in str(w[0].message) + class UlimitTest(unittest.TestCase): def test_create_host_config_dict_ulimit(self): From 93ece93a01adca319ac23af325a2147bb0b6a24a Mon Sep 17 00:00:00 2001 From: Peter Slovak Date: Fri, 7 Apr 2017 00:18:58 +0200 Subject: [PATCH 12/16] docs renames: cpu_group->cpu_period, cpu_period->cpu_quota Signed-off-by: Peter Slovak --- docker/api/container.py | 4 ++-- docker/models/containers.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/api/container.py b/docker/api/container.py index 4e7364b6db..85e5e90ae7 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -473,8 +473,8 @@ def create_host_config(self, *args, **kwargs): cap_add (list of str): Add kernel capabilities. For example, ``["SYS_ADMIN", "MKNOD"]``. cap_drop (list of str): Drop kernel capabilities. - cpu_group (int): The length of a CPU period in microseconds. - cpu_period (int): Microseconds of CPU time that the container can + cpu_period (int): The length of a CPU period in microseconds. + cpu_quota (int): Microseconds of CPU time that the container can get in a CPU period. cpu_shares (int): CPU shares (relative weight). cpuset_cpus (str): CPUs in which to allow execution (``0-3``, diff --git a/docker/models/containers.py b/docker/models/containers.py index 5ad16cc414..ce369e0757 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -457,10 +457,10 @@ def run(self, image, command=None, stdout=True, stderr=False, ``["SYS_ADMIN", "MKNOD"]``. cap_drop (list of str): Drop kernel capabilities. cpu_count (int): CPU count (Windows only). - cpu_group (int): The length of a CPU period in microseconds. - cpu_percent (int): CPU percent (Windows only). - cpu_period (int): Microseconds of CPU time that the container can + cpu_quota (int): Microseconds of CPU time that the container can get in a CPU period. + cpu_percent (int): CPU percent (Windows only). + cpu_period (int): The length of a CPU period in microseconds. cpu_shares (int): CPU shares (relative weight). cpus (float): Number of CPUs. cpuset_cpus (str): CPUs in which to allow execution (``0-3``, From b5d55826fe6759ffdf43f6f24702cf73e2e84e04 Mon Sep 17 00:00:00 2001 From: Peter Slovak Date: Fri, 7 Apr 2017 00:22:22 +0200 Subject: [PATCH 13/16] removed duplicate mem_limit arg desc; type now consistent accross models (float->int) Signed-off-by: Peter Slovak --- docker/models/containers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index ce369e0757..e9560cd6d9 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -513,14 +513,12 @@ def run(self, image, command=None, stdout=True, stderr=False, driver. mac_address (str): MAC address to assign to the container. - mem_limit (float or str): Memory limit. Accepts float values + mem_limit (int or str): Memory limit. 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 character, bytes are assumed as an intended unit. - mem_limit (str or int): Maximum amount of memory container is - allowed to consume. (e.g. ``1G``). mem_swappiness (int): Tune a container's memory swappiness behavior. Accepts number between 0 and 100. memswap_limit (str or int): Maximum amount of memory + swap a From abf2942445f7cc466b85984465c21ab76a9cafb5 Mon Sep 17 00:00:00 2001 From: ewanbarr Date: Wed, 5 Apr 2017 19:06:59 +0200 Subject: [PATCH 14/16] Minor typo correction The stdout argument name was repeated in the run method docstring. The second should be replaced by stderr. Signed-off-by: Ewan Barr --- docker/models/containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index e9560cd6d9..f6bd906546 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -586,7 +586,7 @@ def run(self, image, command=None, stdout=True, stderr=False, stdin_open (bool): Keep ``STDIN`` open even if not attached. stdout (bool): Return logs from ``STDOUT`` when ``detach=False``. Default: ``True``. - stdout (bool): Return logs from ``STDERR`` when ``detach=False``. + stderr (bool): Return logs from ``STDERR`` when ``detach=False``. Default: ``False``. stop_signal (str): The stop signal to use to stop the container (e.g. ``SIGINT``). From 88303c2a54c1f20a992d54462e915877378496e3 Mon Sep 17 00:00:00 2001 From: Santhosh Manohar Date: Fri, 7 Apr 2017 11:19:37 -0700 Subject: [PATCH 15/16] Add 'verbose' option for network inspect api Signed-off-by: Santhosh Manohar --- docker/api/network.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docker/api/network.py b/docker/api/network.py index 9652228de1..46cd68c5ed 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -168,14 +168,23 @@ def remove_network(self, net_id): self._raise_for_status(res) @minimum_version('1.21') - def inspect_network(self, net_id): + def inspect_network(self, net_id, verbose=False): """ Get detailed information about a network. Args: net_id (str): ID of network + verbose (bool): If set shows the service details across the cluster + in swarm mode """ - url = self._url("/networks/{0}", net_id) + if verbose is True: + if version_lt(self._version, '1.28'): + raise InvalidVersion( + 'Verbose option was introduced in API 1.28' + ) + url = self._url("/networks/{0}?verbose=true", net_id) + else: + url = self._url("/networks/{0}", net_id) res = self._get(url) return self._result(res, json=True) From 9c257295865f35da4785365c701fad400f4386d4 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 10 Apr 2017 16:02:43 -0700 Subject: [PATCH 16/16] merge with upstream Signed-off-by: Alexey Rokhin --- docker/api/network.py | 21 ++++++++++----------- tests/unit/dockertypes_test.py | 14 +++++++------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docker/api/network.py b/docker/api/network.py index 46cd68c5ed..74f4cd2b30 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -168,24 +168,23 @@ def remove_network(self, net_id): self._raise_for_status(res) @minimum_version('1.21') - def inspect_network(self, net_id, verbose=False): + def inspect_network(self, net_id, verbose=None): """ Get detailed information about a network. Args: net_id (str): ID of network - verbose (bool): If set shows the service details across the cluster - in swarm mode + verbose (bool): Show the service details across the cluster in + swarm mode. """ - if verbose is True: + params = {} + if verbose is not None: if version_lt(self._version, '1.28'): - raise InvalidVersion( - 'Verbose option was introduced in API 1.28' - ) - url = self._url("/networks/{0}?verbose=true", net_id) - else: - url = self._url("/networks/{0}", net_id) - res = self._get(url) + raise InvalidVersion('verbose was introduced in API 1.28') + params['verbose'] = verbose + + url = self._url("/networks/{0}", net_id) + res = self._get(url, params=params) return self._result(res, json=True) @check_resource diff --git a/tests/unit/dockertypes_test.py b/tests/unit/dockertypes_test.py index fd7c8816d6..4ec8dff397 100644 --- a/tests/unit/dockertypes_test.py +++ b/tests/unit/dockertypes_test.py @@ -166,6 +166,13 @@ def test_create_host_config_invalid_mem_swappiness(self): with pytest.raises(TypeError): create_host_config(version='1.24', mem_swappiness='40') + def test_create_host_config_with_volume_driver(self): + with pytest.raises(InvalidVersion): + create_host_config(version='1.20', volume_driver='local') + + config = create_host_config(version='1.21', volume_driver='local') + assert config.get('VolumeDriver') == 'local' + def test_create_host_config_invalid_cpu_count_types(self): with pytest.raises(TypeError): create_host_config(version='1.25', cpu_count='1') @@ -199,13 +206,6 @@ def test_create_host_config_with_cpus(self): InvalidVersion, lambda: create_host_config( version='1.24', cpus=1)) - def test_create_host_config_with_volume_driver(self): - with pytest.raises(InvalidVersion): - create_host_config(version='1.20', volume_driver='local') - - config = create_host_config(version='1.21', volume_driver='local') - assert config.get('VolumeDriver') == 'local' - class ContainerConfigTest(unittest.TestCase): def test_create_container_config_volume_driver_warning(self):