From e4093ab258df6e1978e1090ddadede1c869abc1e Mon Sep 17 00:00:00 2001 From: Yong Wen Chua Date: Tue, 16 May 2017 15:36:09 +0800 Subject: [PATCH 01/30] Add `target` argument to image building This is related to the multi-stage image building that was introduced in 17.05 (API 1.29). This allows a user to specify the stage of a multi-stage Dockerfile to build for, rather than the final stage. Signed-off-by: Yong Wen Chua --- docker/api/build.py | 12 +++++++++++- docker/models/images.py | 2 ++ tests/integration/api_build_test.py | 22 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docker/api/build.py b/docker/api/build.py index 5c34c47b3..f30be4168 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -18,7 +18,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, 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): + labels=None, cache_from=None, target=None): """ Similar to the ``docker build`` command. Either ``path`` or ``fileobj`` needs to be set. ``path`` can be a local path (to a directory @@ -94,6 +94,8 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, labels (dict): A dictionary of labels to set on the image. cache_from (list): A list of images used for build cache resolution. + target (str): Name of the build-stage to build in a multi-stage + Dockerfile. Returns: A generator for the build output. @@ -198,6 +200,14 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, 'cache_from was only introduced in API version 1.25' ) + if target: + if utils.version_gte(self._version, '1.29'): + params.update({'target': target}) + else: + raise errors.InvalidVersion( + 'target was only introduced in API version 1.29' + ) + if context is not None: headers = {'Content-Type': 'application/tar'} if encoding: diff --git a/docker/models/images.py b/docker/models/images.py index 52a44b27b..a9ed65ee3 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -151,6 +151,8 @@ def build(self, **kwargs): decoded into dicts on the fly. Default ``False``. cache_from (list): A list of images used for build cache resolution. + target (str): Name of the build-stage to build in a multi-stage + Dockerfile. Returns: (:py:class:`Image`): The built image. diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index fe5d994dd..623b66093 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -189,6 +189,28 @@ def test_build_with_cache_from(self): counter += 1 assert counter == 0 + @requires_api_version('1.29') + def test_build_container_with_target(self): + script = io.BytesIO('\n'.join([ + 'FROM busybox as first', + 'RUN mkdir -p /tmp/test', + 'RUN touch /tmp/silence.tar.gz', + 'FROM alpine:latest', + 'WORKDIR /root/' + 'COPY --from=first /tmp/silence.tar.gz .', + 'ONBUILD RUN echo "This should not be in the final image"' + ]).encode('ascii')) + + stream = self.client.build( + fileobj=script, target='first', tag='build1' + ) + self.tmp_imgs.append('build1') + for chunk in stream: + pass + + info = self.client.inspect_image('build1') + self.assertEqual(info['Config']['OnBuild'], []) + def test_build_stderr_data(self): control_chars = ['\x1b[91m', '\x1b[0m'] snippet = 'Ancient Temple (Mystic Oriental Dream ~ Ancient Temple)' From 7880c5af1de66ed4555a30eeb19dc0093536f2f0 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 May 2017 17:19:37 -0700 Subject: [PATCH 02/30] 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 c734a1611..6979e1bef 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.3.0" +version = "2.4.0-dev" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) From 9cc021dfa684ab1a614d473e78f9c4c0fc960585 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 May 2017 19:05:32 -0700 Subject: [PATCH 03/30] Add support for placement preferences and platforms in TaskTemplate Signed-off-by: Joffrey F --- docker/api/service.py | 69 ++++++++++++++++----------- docker/types/__init__.py | 4 +- docker/types/services.py | 31 +++++++++++- tests/integration/api_service_test.py | 43 +++++++++++++++++ 4 files changed, 115 insertions(+), 32 deletions(-) diff --git a/docker/api/service.py b/docker/api/service.py index 4972c16d1..aea93cbfc 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -3,6 +3,43 @@ from ..types import ServiceMode +def _check_api_features(version, task_template, update_config): + if update_config is not None: + if utils.version_lt(version, '1.25'): + if 'MaxFailureRatio' in update_config: + raise errors.InvalidVersion( + 'UpdateConfig.max_failure_ratio is not supported in' + ' API version < 1.25' + ) + if 'Monitor' in update_config: + raise errors.InvalidVersion( + 'UpdateConfig.monitor is not supported in' + ' API version < 1.25' + ) + + if task_template is not None: + if 'ForceUpdate' in task_template and utils.version_lt( + version, '1.25'): + raise errors.InvalidVersion( + 'force_update is not supported in API version < 1.25' + ) + + if task_template.get('Placement'): + if utils.version_lt(version, '1.30'): + if task_template['Placement'].get('Platforms'): + raise errors.InvalidVersion( + 'Placement.platforms is not supported in' + ' API version < 1.30' + ) + + if utils.version_lt(version, '1.27'): + if task_template['Placement'].get('Preferences'): + raise errors.InvalidVersion( + 'Placement.preferences is not supported in' + ' API version < 1.27' + ) + + class ServiceApiMixin(object): @utils.minimum_version('1.24') def create_service( @@ -43,6 +80,8 @@ def create_service( ) endpoint_spec = endpoint_config + _check_api_features(self._version, task_template, update_config) + url = self._url('/services/create') headers = {} image = task_template.get('ContainerSpec', {}).get('Image', None) @@ -67,17 +106,6 @@ def create_service( } if update_config is not None: - if utils.version_lt(self._version, '1.25'): - if 'MaxFailureRatio' in update_config: - raise errors.InvalidVersion( - 'UpdateConfig.max_failure_ratio is not supported in' - ' API version < 1.25' - ) - if 'Monitor' in update_config: - raise errors.InvalidVersion( - 'UpdateConfig.monitor is not supported in' - ' API version < 1.25' - ) data['UpdateConfig'] = update_config return self._result( @@ -282,6 +310,8 @@ def update_service(self, service, version, task_template=None, name=None, ) endpoint_spec = endpoint_config + _check_api_features(self._version, task_template, update_config) + url = self._url('/services/{0}/update', service) data = {} headers = {} @@ -294,12 +324,6 @@ def update_service(self, service, version, task_template=None, name=None, mode = ServiceMode(mode) data['Mode'] = mode if task_template is not None: - if 'ForceUpdate' in task_template and utils.version_lt( - self._version, '1.25'): - raise errors.InvalidVersion( - 'force_update is not supported in API version < 1.25' - ) - image = task_template.get('ContainerSpec', {}).get('Image', None) if image is not None: registry, repo_name = auth.resolve_repository_name(image) @@ -308,17 +332,6 @@ def update_service(self, service, version, task_template=None, name=None, headers['X-Registry-Auth'] = auth_header data['TaskTemplate'] = task_template if update_config is not None: - if utils.version_lt(self._version, '1.25'): - if 'MaxFailureRatio' in update_config: - raise errors.InvalidVersion( - 'UpdateConfig.max_failure_ratio is not supported in' - ' API version < 1.25' - ) - if 'Monitor' in update_config: - raise errors.InvalidVersion( - 'UpdateConfig.monitor is not supported in' - ' API version < 1.25' - ) data['UpdateConfig'] = update_config if networks is not None: diff --git a/docker/types/__init__.py b/docker/types/__init__.py index 0e8877601..edc919dfc 100644 --- a/docker/types/__init__.py +++ b/docker/types/__init__.py @@ -3,7 +3,7 @@ from .healthcheck import Healthcheck from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig from .services import ( - ContainerSpec, DriverConfig, EndpointSpec, Mount, Resources, RestartPolicy, - SecretReference, ServiceMode, TaskTemplate, UpdateConfig + ContainerSpec, DriverConfig, EndpointSpec, Mount, Placement, Resources, + RestartPolicy, SecretReference, ServiceMode, TaskTemplate, UpdateConfig ) from .swarm import SwarmSpec, SwarmExternalCA diff --git a/docker/types/services.py b/docker/types/services.py index 012f7b019..7456a42ba 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -20,7 +20,9 @@ class TaskTemplate(dict): individual container created as part of the service. restart_policy (RestartPolicy): Specification for the restart policy which applies to containers created as part of this service. - placement (:py:class:`list`): A list of constraints. + placement (Placement): Placement instructions for the scheduler. + If a list is passed instead, it is assumed to be a list of + constraints as part of a :py:class:`Placement` object. force_update (int): A counter that triggers an update even if no relevant parameters have been changed. """ @@ -33,7 +35,7 @@ def __init__(self, container_spec, resources=None, restart_policy=None, self['RestartPolicy'] = restart_policy if placement: if isinstance(placement, list): - placement = {'Constraints': placement} + placement = Placement(constraints=placement) self['Placement'] = placement if log_driver: self['LogDriver'] = log_driver @@ -452,3 +454,28 @@ def __init__(self, secret_id, secret_name, filename=None, uid=None, 'GID': gid or '0', 'Mode': mode } + + +class Placement(dict): + """ + Placement constraints to be used as part of a :py:class:`TaskTemplate` + + Args: + constraints (list): A list of constraints + preferences (list): Preferences provide a way to make the + scheduler aware of factors such as topology. They are provided + in order from highest to lowest precedence. + platforms (list): A list of platforms expressed as ``(arch, os)`` + tuples + """ + def __init__(self, constraints=None, preferences=None, platforms=None): + if constraints is not None: + self['Constraints'] = constraints + if preferences is not None: + self['Preferences'] = preferences + if platforms: + self['Platforms'] = [] + for plat in platforms: + self['Platforms'].append({ + 'Architecture': plat[0], 'OS': plat[1] + }) diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index 914e516bc..8ac852d96 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -270,6 +270,49 @@ def test_create_service_with_placement(self): assert (svc_info['Spec']['TaskTemplate']['Placement'] == {'Constraints': ['node.id=={}'.format(node_id)]}) + def test_create_service_with_placement_object(self): + node_id = self.client.nodes()[0]['ID'] + container_spec = docker.types.ContainerSpec(BUSYBOX, ['true']) + placemt = docker.types.Placement( + constraints=['node.id=={}'.format(node_id)] + ) + task_tmpl = docker.types.TaskTemplate( + container_spec, placement=placemt + ) + name = self.get_service_name() + svc_id = self.client.create_service(task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + assert 'Placement' in svc_info['Spec']['TaskTemplate'] + assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt + + @requires_api_version('1.30') + def test_create_service_with_placement_platform(self): + container_spec = docker.types.ContainerSpec(BUSYBOX, ['true']) + placemt = docker.types.Placement(platforms=[('x86_64', 'linux')]) + task_tmpl = docker.types.TaskTemplate( + container_spec, placement=placemt + ) + name = self.get_service_name() + svc_id = self.client.create_service(task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + assert 'Placement' in svc_info['Spec']['TaskTemplate'] + assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt + + @requires_api_version('1.27') + def test_create_service_with_placement_preferences(self): + container_spec = docker.types.ContainerSpec(BUSYBOX, ['true']) + placemt = docker.types.Placement(preferences=[ + {'Spread': {'SpreadDescriptor': 'com.dockerpy.test'}} + ]) + task_tmpl = docker.types.TaskTemplate( + container_spec, placement=placemt + ) + name = self.get_service_name() + svc_id = self.client.create_service(task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + assert 'Placement' in svc_info['Spec']['TaskTemplate'] + assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt + def test_create_service_with_endpoint_spec(self): container_spec = docker.types.ContainerSpec(BUSYBOX, ['true']) task_tmpl = docker.types.TaskTemplate(container_spec) From ff718f5dac2ba00ffbb52c0f3b1af5b687f07930 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 17 May 2017 15:23:36 -0700 Subject: [PATCH 04/30] Add support for ingress in create_network Signed-off-by: Joffrey F --- docker/api/network.py | 13 ++++++++++++- docker/models/networks.py | 2 ++ tests/integration/api_network_test.py | 8 ++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docker/api/network.py b/docker/api/network.py index 74f4cd2b3..3a454546c 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -41,7 +41,8 @@ def networks(self, names=None, ids=None, filters=None): @minimum_version('1.21') def create_network(self, name, driver=None, options=None, ipam=None, check_duplicate=None, internal=False, labels=None, - enable_ipv6=False, attachable=None, scope=None): + enable_ipv6=False, attachable=None, scope=None, + ingress=None): """ Create a network. Similar to the ``docker network create``. @@ -60,6 +61,8 @@ def create_network(self, name, driver=None, options=None, ipam=None, attachable (bool): If enabled, and the network is in the global scope, non-service containers on worker nodes will be able to connect to the network. + ingress (bool): If set, create an ingress network which provides + the routing-mesh in swarm mode. Returns: (dict): The created network reference object @@ -129,6 +132,14 @@ def create_network(self, name, driver=None, options=None, ipam=None, ) data['Attachable'] = attachable + if ingress is not None: + if version_lt(self._version, '1.29'): + raise InvalidVersion( + 'ingress is not supported in API version < 1.29' + ) + + data['Ingress'] = ingress + url = self._url("/networks/create") res = self._post_json(url, data=data) return self._result(res, json=True) diff --git a/docker/models/networks.py b/docker/models/networks.py index 586809753..afb0ebe8b 100644 --- a/docker/models/networks.py +++ b/docker/models/networks.py @@ -111,6 +111,8 @@ def create(self, name, *args, **kwargs): labels (dict): Map of labels to set on the network. Default ``None``. enable_ipv6 (bool): Enable IPv6 on the network. Default ``False``. + ingress (bool): If set, create an ingress network which provides + the routing-mesh in swarm mode. Returns: (:py:class:`Network`): The network that was created. diff --git a/tests/integration/api_network_test.py b/tests/integration/api_network_test.py index b3ae51208..5439dd7b2 100644 --- a/tests/integration/api_network_test.py +++ b/tests/integration/api_network_test.py @@ -452,6 +452,14 @@ def test_create_network_attachable(self): net = self.client.inspect_network(net_id) assert net['Attachable'] is True + @requires_api_version('1.29') + def test_create_network_ingress(self): + assert self.client.init_swarm('eth0') + self.client.remove_network('ingress') + _, net_id = self.create_network(driver='overlay', ingress=True) + net = self.client.inspect_network(net_id) + assert net['Ingress'] is True + @requires_api_version('1.25') def test_prune_networks(self): net_name, _ = self.create_network() From f6f5652eb265fb8311b1e6ad4b16979b9808f908 Mon Sep 17 00:00:00 2001 From: Alexey Rokhin Date: Wed, 17 May 2017 23:18:18 +0300 Subject: [PATCH 05/30] fix type checking for nano_cpus Signed-off-by: Alexey Rokhin --- docker/types/containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/types/containers.py b/docker/types/containers.py index 18d18381d..f33c5e836 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -466,7 +466,7 @@ def __init__(self, version, binds=None, port_bindings=None, self['CpuPercent'] = cpu_percent if nano_cpus: - if not isinstance(nano_cpus, int): + if not isinstance(nano_cpus, six.integer_types): raise host_config_type_error('nano_cpus', nano_cpus, 'int') if version_lt(version, '1.25'): raise host_config_version_error('nano_cpus', '1.25') From 41aae65ab2714168e56118c494d16128ef0929b2 Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 18 May 2017 10:06:58 +0800 Subject: [PATCH 06/30] update swarm remove test status code from 500 to >= 400 Signed-off-by: allencloud --- tests/integration/api_swarm_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/api_swarm_test.py b/tests/integration/api_swarm_test.py index 0a2f69f3c..666c689f5 100644 --- a/tests/integration/api_swarm_test.py +++ b/tests/integration/api_swarm_test.py @@ -173,4 +173,4 @@ def test_remove_main_node(self): with pytest.raises(docker.errors.APIError) as e: self.client.remove_node(node_id, True) - assert e.value.response.status_code == 500 + assert e.value.response.status_code >= 400 From 45aec93089b8b5133ed4eae125ffc29878b788ea Mon Sep 17 00:00:00 2001 From: Chris Ottinger Date: Sat, 27 May 2017 00:21:19 +1000 Subject: [PATCH 07/30] fix #1625 where ImageCollection.build() could return early with incorrect image_id depending on docer build output Signed-off-by: Chris Ottinger --- docker/models/images.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/models/images.py b/docker/models/images.py index a9ed65ee3..9af040cfc 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -169,19 +169,20 @@ def build(self, **kwargs): if isinstance(resp, six.string_types): return self.get(resp) last_event = None + image_id = None for chunk in json_stream(resp): if 'error' in chunk: raise BuildError(chunk['error']) if 'stream' in chunk: match = re.search( - r'(Successfully built |sha256:)([0-9a-f]+)', + r'(^Successfully built |sha256:)([0-9a-f]+)$', chunk['stream'] ) if match: image_id = match.group(2) - return self.get(image_id) last_event = chunk - + if image_id: + return self.get(image_id) raise BuildError(last_event or 'Unknown') def get(self, name): From 6ef9d426eb259650c8a4ff5c25c878f462c2bb9d Mon Sep 17 00:00:00 2001 From: Chris Ottinger Date: Sat, 27 May 2017 10:29:36 +1000 Subject: [PATCH 08/30] added integration test for #1625 for ImageCollection.build() that verfies that the build method uses the last success message for extracting the image id Signed-off-by: Chris Ottinger --- tests/integration/models_images_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration/models_images_test.py b/tests/integration/models_images_test.py index 6d61e4977..2b45429d8 100644 --- a/tests/integration/models_images_test.py +++ b/tests/integration/models_images_test.py @@ -39,6 +39,17 @@ def test_build_with_multiple_success(self): self.tmp_imgs.append(image.id) assert client.containers.run(image) == b"hello world\n" + def test_build_with_success_build_output(self): + client = docker.from_env(version=TEST_API_VERSION) + image = client.images.build( + tag='dup-txt-tag', fileobj=io.BytesIO( + "FROM alpine\n" + "CMD echo Successfully built 33c838732b70".encode('ascii') + ) + ) + self.tmp_imgs.append(image.id) + assert client.containers.run(image) == b"Successfully built 33c838732b70\n" + def test_list(self): client = docker.from_env(version=TEST_API_VERSION) image = client.images.pull('alpine:latest') From 1223fc144fc9b529959ba554f1f5e45e63c50514 Mon Sep 17 00:00:00 2001 From: Chris Ottinger Date: Sat, 27 May 2017 11:24:58 +1000 Subject: [PATCH 09/30] new integration task linting for #1625 Signed-off-by: Chris Ottinger --- tests/integration/models_images_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/models_images_test.py b/tests/integration/models_images_test.py index 2b45429d8..721a19db6 100644 --- a/tests/integration/models_images_test.py +++ b/tests/integration/models_images_test.py @@ -44,11 +44,11 @@ def test_build_with_success_build_output(self): image = client.images.build( tag='dup-txt-tag', fileobj=io.BytesIO( "FROM alpine\n" - "CMD echo Successfully built 33c838732b70".encode('ascii') + "CMD echo Successfully built abcd1234".encode('ascii') ) ) self.tmp_imgs.append(image.id) - assert client.containers.run(image) == b"Successfully built 33c838732b70\n" + assert client.containers.run(image) == b"Successfully built abcd1234\n" def test_list(self): client = docker.from_env(version=TEST_API_VERSION) From 9eecfb0d2fa6973c08dd78e1cc81cbd2098ec3b8 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 30 May 2017 11:45:00 -0700 Subject: [PATCH 10/30] Fix misleading build method docs Signed-off-by: Joffrey F --- docker/models/images.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker/models/images.py b/docker/models/images.py index a9ed65ee3..81a21d66b 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -126,9 +126,6 @@ def build(self, **kwargs): rm (bool): Remove intermediate containers. The ``docker build`` command now defaults to ``--rm=true``, but we have kept the old default of `False` to preserve backward compatibility - stream (bool): *Deprecated for API version > 1.8 (always True)*. - Return a blocking generator you can iterate over to retrieve - build output as it happens timeout (int): HTTP timeout custom_context (bool): Optional if using ``fileobj`` encoding (str): The encoding for a stream. Set to ``gzip`` for From 6ae24b9e60cd8f080a6c657ccbdffd22056169dd Mon Sep 17 00:00:00 2001 From: Madhuri Kumari Date: Thu, 1 Jun 2017 15:09:46 +0000 Subject: [PATCH 11/30] Add support for ``runtime`` in container create and run API --- docker/api/container.py | 6 ++++-- docker/models/containers.py | 2 ++ docker/types/containers.py | 10 +++++++--- docs/change-log.md | 2 ++ tests/integration/api_container_test.py | 9 +++++++++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docker/api/container.py b/docker/api/container.py index 97b540593..0abfca4f7 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -238,7 +238,7 @@ def create_container(self, image, command=None, hostname=None, user=None, memswap_limit=None, cpuset=None, host_config=None, mac_address=None, labels=None, volume_driver=None, stop_signal=None, networking_config=None, - healthcheck=None, stop_timeout=None): + healthcheck=None, stop_timeout=None, runtime=None): """ Creates a container. Parameters are similar to those for the ``docker run`` command except it doesn't support the attach options (``-a``). @@ -417,6 +417,7 @@ def create_container(self, image, command=None, hostname=None, user=None, Default: 10 networking_config (dict): A networking configuration generated by :py:meth:`create_networking_config`. + runtime (str): The name of the runtime tool to create container. Returns: A dictionary with an image 'Id' key and a 'Warnings' key. @@ -441,7 +442,7 @@ def create_container(self, image, command=None, hostname=None, user=None, network_disabled, entrypoint, cpu_shares, working_dir, domainname, memswap_limit, cpuset, host_config, mac_address, labels, volume_driver, stop_signal, networking_config, healthcheck, - stop_timeout + stop_timeout, runtime ) return self.create_container_from_config(config, name) @@ -576,6 +577,7 @@ def create_host_config(self, *args, **kwargs): values are: ``host`` volumes_from (:py:class:`list`): List of container names or IDs to get volumes from. + runtime (str): The name of the runtime tool to manage container. Returns: diff --git a/docker/models/containers.py b/docker/models/containers.py index 4bb2cf863..46f900e06 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -659,6 +659,7 @@ def run(self, image, command=None, stdout=True, stderr=False, volumes_from (:py:class:`list`): List of container names or IDs to get volumes from. working_dir (str): Path to the working directory. + runtime (str): The name of the runtime tool to create container. Returns: The container logs, either ``STDOUT``, ``STDERR``, or both, @@ -885,6 +886,7 @@ def prune(self, filters=None): 'userns_mode', 'version', 'volumes_from', + 'runtime' ] diff --git a/docker/types/containers.py b/docker/types/containers.py index f33c5e836..f834c7851 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -120,7 +120,7 @@ def __init__(self, version, binds=None, port_bindings=None, isolation=None, auto_remove=False, storage_opt=None, init=None, init_path=None, volume_driver=None, cpu_count=None, cpu_percent=None, nano_cpus=None, - cpuset_mems=None): + cpuset_mems=None, runtime=None): if mem_limit is not None: self['Memory'] = parse_bytes(mem_limit) @@ -473,6 +473,9 @@ def __init__(self, version, binds=None, port_bindings=None, self['NanoCpus'] = nano_cpus + if runtime: + self['Runtime'] = runtime + def host_config_type_error(param, param_value, expected): error_msg = 'Invalid type for {0} param: expected {1} but found {2}' @@ -499,7 +502,7 @@ def __init__( working_dir=None, domainname=None, memswap_limit=None, cpuset=None, host_config=None, mac_address=None, labels=None, volume_driver=None, stop_signal=None, networking_config=None, healthcheck=None, - stop_timeout=None + stop_timeout=None, runtime=None ): if version_gte(version, '1.10'): message = ('{0!r} parameter has no effect on create_container().' @@ -659,5 +662,6 @@ def __init__( 'VolumeDriver': volume_driver, 'StopSignal': stop_signal, 'Healthcheck': healthcheck, - 'StopTimeout': stop_timeout + 'StopTimeout': stop_timeout, + 'Runtime': runtime }) diff --git a/docs/change-log.md b/docs/change-log.md index 3d58f931f..20bf9e092 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -122,6 +122,8 @@ Change log * Added support for `force_update` in `TaskTemplate` * Made `name` parameter optional in `APIClient.create_volume` and `DockerClient.volumes.create` +* Added support for `runtime` in `APIClient.create_container` and + `DockerClient.containers.run` ### Bugfixes diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index fb4c4e4ad..c499e35e4 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -1256,6 +1256,15 @@ def test_container_cpuset(self): self.assertEqual(inspect_data['HostConfig']['CpusetCpus'], cpuset_cpus) + def test_create_with_runtime(self): + container = self.client.create_container( + BUSYBOX, ['echo', 'test'], runtime='runc' + ) + self.tmp_containers.append(container['Id']) + config = self.client.inspect_container(container) + assert config['Config']['Runtime'] == 'runc' + + class LinkTest(BaseAPIIntegrationTest): def test_remove_link(self): # Create containers From 5dd91cd4aaa2e7cd8dde1dd316d53cab25ef9b78 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Mon, 5 Jun 2017 17:51:37 +0200 Subject: [PATCH 12/30] Rewrite the split_port function using re In the case of a defined format with specific parts, a regular expression with named capturing bits make reasoning about the parts simpler than imlementing a parser from scratch. Signed-off-by: kaiyou --- docker/utils/ports.py | 107 ++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 67 deletions(-) diff --git a/docker/utils/ports.py b/docker/utils/ports.py index 3708958d4..57332deee 100644 --- a/docker/utils/ports.py +++ b/docker/utils/ports.py @@ -1,3 +1,16 @@ +import re + +PORT_SPEC = re.compile( + "^" # Match full string + "(" # External part + "((?P[a-fA-F\d.:]+):)?" # Address + "(?P[\d]*)(-(?P[\d]+))?:" # External range + ")?" + "(?P[\d]+)(-(?P[\d]+))?" # Internal range + "(?P/(udp|tcp))?" # Protocol + "$" # Match full string +) + def add_port_mapping(port_bindings, internal_port, external): if internal_port in port_bindings: @@ -24,81 +37,41 @@ def build_port_bindings(ports): return port_bindings -def to_port_range(port, randomly_available_port=False): - if not port: - return None - - protocol = "" - if "/" in port: - parts = port.split("/") - if len(parts) != 2: - _raise_invalid_port(port) - - port, protocol = parts - protocol = "/" + protocol - - if randomly_available_port: - return ["%s%s" % (port, protocol)] - - parts = str(port).split('-') - - if len(parts) == 1: - return ["%s%s" % (port, protocol)] - - if len(parts) == 2: - full_port_range = range(int(parts[0]), int(parts[1]) + 1) - return ["%s%s" % (p, protocol) for p in full_port_range] - - raise ValueError('Invalid port range "%s", should be ' - 'port or startport-endport' % port) - - def _raise_invalid_port(port): raise ValueError('Invalid port "%s", should be ' '[[remote_ip:]remote_port[-remote_port]:]' 'port[/protocol]' % port) -def split_port(port): - parts = str(port).split(':') - - if not 1 <= len(parts) <= 3: - _raise_invalid_port(port) - - if len(parts) == 1: - internal_port, = parts - if not internal_port: - _raise_invalid_port(port) - return to_port_range(internal_port), None - if len(parts) == 2: - external_port, internal_port = parts - - internal_range = to_port_range(internal_port) - if internal_range is None: - _raise_invalid_port(port) - - external_range = to_port_range(external_port, len(internal_range) == 1) - if external_range is None: - _raise_invalid_port(port) - - if len(internal_range) != len(external_range): - raise ValueError('Port ranges don\'t match in length') - - return internal_range, external_range +def port_range(start, end, proto, randomly_available_port=False): + if not start: + return start + if not end: + return [start + proto] + if randomly_available_port: + return ['{}-{}'.format(start, end) + proto] + return [str(port) + proto for port in range(int(start), int(end) + 1)] - external_ip, external_port, internal_port = parts - if not internal_port: +def split_port(port): + match = PORT_SPEC.match(port) + if match is None: _raise_invalid_port(port) + parts = match.groupdict() - internal_range = to_port_range(internal_port) - external_range = to_port_range(external_port, len(internal_range) == 1) - - if not external_range: - external_range = [None] * len(internal_range) - - if len(internal_range) != len(external_range): - raise ValueError('Port ranges don\'t match in length') + host = parts['host'] + proto = parts['proto'] or '' + internal = port_range(parts['int'], parts['int_end'], proto) + external = port_range( + parts['ext'], parts['ext_end'], '', len(internal) == 1) - return internal_range, [(external_ip, ex_port or None) - for ex_port in external_range] + if host is None: + if external is not None and len(internal) != len(external): + raise ValueError('Port ranges don\'t match in length') + return internal, external + else: + if not external: + external = [None] * len(internal) + elif len(internal) != len(external): + raise ValueError('Port ranges don\'t match in length') + return internal, [(host, ext_port) for ext_port in external] From 0c1271350db33cb21265309a31da2d1c399b8243 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Mon, 5 Jun 2017 17:57:46 +0200 Subject: [PATCH 13/30] Add a specific unit test for splitting port with IPv6 The test was copied from https://github.com/greybyte/docker-py/commit/ccec87ca2c2aacfcfe3b38c5bc7d59dd73551c51 Signed-off-by: kaiyou --- tests/unit/utils_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 25ed0f9b7..c25881d14 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -552,6 +552,12 @@ def test_split_port_range_with_protocol(self): self.assertEqual(external_port, [("127.0.0.1", "1000"), ("127.0.0.1", "1001")]) + def test_split_port_with_ipv6_address(self): + internal_port, external_port = split_port( + "2001:abcd:ef00::2:1000:2000") + self.assertEqual(internal_port, ["2000"]) + self.assertEqual(external_port, [("2001:abcd:ef00::2", "1000")]) + def test_split_port_invalid(self): self.assertRaises(ValueError, lambda: split_port("0.0.0.0:1000:2000:tcp")) From 612c0f3d0de298884b80766d285f8ad47ad742a0 Mon Sep 17 00:00:00 2001 From: Madhuri Kumari Date: Thu, 1 Jun 2017 16:53:58 +0000 Subject: [PATCH 14/30] Fix test cases for ``runtime`` config Signed-off-by: Madhuri Kumari --- docker/api/container.py | 4 ++-- docker/models/containers.py | 2 +- docker/types/containers.py | 2 ++ docs/change-log.md | 2 -- tests/integration/api_container_test.py | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docker/api/container.py b/docker/api/container.py index 0abfca4f7..2352df9b4 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -417,7 +417,7 @@ def create_container(self, image, command=None, hostname=None, user=None, Default: 10 networking_config (dict): A networking configuration generated by :py:meth:`create_networking_config`. - runtime (str): The name of the runtime tool to create container. + runtime (str): Runtime to use with this container. Returns: A dictionary with an image 'Id' key and a 'Warnings' key. @@ -577,7 +577,7 @@ def create_host_config(self, *args, **kwargs): values are: ``host`` volumes_from (:py:class:`list`): List of container names or IDs to get volumes from. - runtime (str): The name of the runtime tool to manage container. + runtime (str): Runtime to use with this container. Returns: diff --git a/docker/models/containers.py b/docker/models/containers.py index 46f900e06..300c5a9d3 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -659,7 +659,7 @@ def run(self, image, command=None, stdout=True, stderr=False, volumes_from (:py:class:`list`): List of container names or IDs to get volumes from. working_dir (str): Path to the working directory. - runtime (str): The name of the runtime tool to create container. + runtime (str): Runtime to use with this container. Returns: The container logs, either ``STDOUT``, ``STDERR``, or both, diff --git a/docker/types/containers.py b/docker/types/containers.py index f834c7851..6bbb57ae7 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -474,6 +474,8 @@ def __init__(self, version, binds=None, port_bindings=None, self['NanoCpus'] = nano_cpus if runtime: + if version_lt(version, '1.25'): + raise host_config_version_error('runtime', '1.25') self['Runtime'] = runtime diff --git a/docs/change-log.md b/docs/change-log.md index 20bf9e092..3d58f931f 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -122,8 +122,6 @@ Change log * Added support for `force_update` in `TaskTemplate` * Made `name` parameter optional in `APIClient.create_volume` and `DockerClient.volumes.create` -* Added support for `runtime` in `APIClient.create_container` and - `DockerClient.containers.run` ### Bugfixes diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index c499e35e4..de3fe7183 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -1255,14 +1255,14 @@ def test_container_cpuset(self): inspect_data = self.client.inspect_container(container) self.assertEqual(inspect_data['HostConfig']['CpusetCpus'], cpuset_cpus) - - def test_create_with_runtime(self): + @requires_api_version('1.25') + def test_create_with_runtime(self): container = self.client.create_container( BUSYBOX, ['echo', 'test'], runtime='runc' ) self.tmp_containers.append(container['Id']) config = self.client.inspect_container(container) - assert config['Config']['Runtime'] == 'runc' + assert config['HostConfig']['Runtime'] == 'runc' class LinkTest(BaseAPIIntegrationTest): From ee75a1c2e349fccab4a1bcb49142756c9a8495db Mon Sep 17 00:00:00 2001 From: grahamlyons Date: Thu, 8 Jun 2017 14:31:25 +0100 Subject: [PATCH 15/30] Ensure default timeout is used by API Client The `from_env` method on the `docker` module passed `None` as the value for the `timeout` keyword argument which overrode the default value in the initialiser, taken from `constants` module. This sets the default in the initialiser to `None` and adds logic to set that, in the same way that `version` is handled. Signed-off-by: grahamlyons --- docker/api/client.py | 9 ++++++--- tests/unit/client_test.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docker/api/client.py b/docker/api/client.py index 54ec6abb4..6822f7c7b 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -83,8 +83,7 @@ class APIClient( configuration. user_agent (str): Set a custom user agent for requests to the server. """ - def __init__(self, base_url=None, version=None, - timeout=DEFAULT_TIMEOUT_SECONDS, tls=False, + def __init__(self, base_url=None, version=None, timeout=None, tls=False, user_agent=DEFAULT_USER_AGENT, num_pools=DEFAULT_NUM_POOLS): super(APIClient, self).__init__() @@ -94,7 +93,11 @@ def __init__(self, base_url=None, version=None, ) self.base_url = base_url - self.timeout = timeout + if timeout is not None: + self.timeout = timeout + else: + self.timeout = DEFAULT_TIMEOUT_SECONDS + self.headers['User-Agent'] = user_agent self._auth_configs = auth.load_config() diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index b79c68e15..c4996f133 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -1,6 +1,9 @@ import datetime import docker from docker.utils import kwargs_from_env +from docker.constants import ( + DEFAULT_DOCKER_API_VERSION, DEFAULT_TIMEOUT_SECONDS +) import os import unittest @@ -96,3 +99,13 @@ def test_from_env_with_version(self): client = docker.from_env(version='2.32') self.assertEqual(client.api.base_url, "https://192.168.59.103:2376") self.assertEqual(client.api._version, '2.32') + + def test_from_env_without_version_uses_default(self): + client = docker.from_env() + + self.assertEqual(client.api._version, DEFAULT_DOCKER_API_VERSION) + + def test_from_env_without_timeout_uses_default(self): + client = docker.from_env() + + self.assertEqual(client.api.timeout, DEFAULT_TIMEOUT_SECONDS) From ff993dd858ffb3c6367013ed2c468903f0cf4fe9 Mon Sep 17 00:00:00 2001 From: grahamlyons Date: Fri, 9 Jun 2017 09:47:00 +0100 Subject: [PATCH 16/30] Move default `timeout` into `from_env` We'd like to be able to pass `None` as a value for `timeout` because it has meaning to the `requests` library (http://docs.python-requests.org/en/master/user/advanced/#timeouts) Signed-off-by: grahamlyons --- docker/api/client.py | 9 +++------ docker/client.py | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docker/api/client.py b/docker/api/client.py index 6822f7c7b..54ec6abb4 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -83,7 +83,8 @@ class APIClient( configuration. user_agent (str): Set a custom user agent for requests to the server. """ - def __init__(self, base_url=None, version=None, timeout=None, tls=False, + def __init__(self, base_url=None, version=None, + timeout=DEFAULT_TIMEOUT_SECONDS, tls=False, user_agent=DEFAULT_USER_AGENT, num_pools=DEFAULT_NUM_POOLS): super(APIClient, self).__init__() @@ -93,11 +94,7 @@ def __init__(self, base_url=None, version=None, timeout=None, tls=False, ) self.base_url = base_url - if timeout is not None: - self.timeout = timeout - else: - self.timeout = DEFAULT_TIMEOUT_SECONDS - + self.timeout = timeout self.headers['User-Agent'] = user_agent self._auth_configs = auth.load_config() diff --git a/docker/client.py b/docker/client.py index 09abd6332..fcfb01d8b 100644 --- a/docker/client.py +++ b/docker/client.py @@ -1,4 +1,5 @@ from .api.client import APIClient +from .constants import DEFAULT_TIMEOUT_SECONDS from .models.containers import ContainerCollection from .models.images import ImageCollection from .models.networks import NetworkCollection @@ -73,7 +74,7 @@ def from_env(cls, **kwargs): .. _`SSL version`: https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLSv1 """ - timeout = kwargs.pop('timeout', None) + timeout = kwargs.pop('timeout', DEFAULT_TIMEOUT_SECONDS) version = kwargs.pop('version', None) return cls(timeout=timeout, version=version, **kwargs_from_env(**kwargs)) From 234296171f2389b89fa3d4c359b6f6042419b7c7 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Wed, 14 Jun 2017 13:46:21 +0000 Subject: [PATCH 17/30] Only pull the 'latest' tag when testing images Signed-off-by: Bryan Boreham --- tests/integration/api_image_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index 11146a8a0..917bc5055 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -42,7 +42,7 @@ def test_pull(self): self.client.remove_image('hello-world') except docker.errors.APIError: pass - res = self.client.pull('hello-world') + res = self.client.pull('hello-world', tag='latest') self.tmp_imgs.append('hello-world') self.assertEqual(type(res), six.text_type) self.assertGreaterEqual( @@ -56,7 +56,8 @@ def test_pull_streaming(self): self.client.remove_image('hello-world') except docker.errors.APIError: pass - stream = self.client.pull('hello-world', stream=True, decode=True) + stream = self.client.pull( + 'hello-world', tag='latest', stream=True, decode=True) self.tmp_imgs.append('hello-world') for chunk in stream: assert isinstance(chunk, dict) @@ -300,7 +301,7 @@ def test_prune_images(self): ctnr = self.client.create_container(BUSYBOX, ['sleep', '9999']) self.tmp_containers.append(ctnr) - self.client.pull('hello-world') + self.client.pull('hello-world', tag='latest') self.tmp_imgs.append('hello-world') img_id = self.client.inspect_image('hello-world')['Id'] result = self.client.prune_images() From ae1f596d3703c35e7f33abe3c14155382d0bad42 Mon Sep 17 00:00:00 2001 From: Matt Oberle Date: Wed, 14 Jun 2017 14:41:04 -0400 Subject: [PATCH 18/30] excludes requests 2.18.0 from compatible versions The 2.18.0 version of requests breaks compatibility with docker-py: https://github.com/requests/requests/issues/4160 [This block](https://github.com/shazow/urllib3/blob/master/urllib3/connectionpool.py#L292) of code from urllib3 fails: ```python def _get_timeout(self, timeout): """ Helper that always returns a :class:`urllib3.util.Timeout` """ if timeout is _Default: return self.timeout.clone() if isinstance(timeout, Timeout): return timeout.clone() else: # User passed us an int/float. This is for backwards compatibility, # can be removed later return Timeout.from_float(timeout) ``` In the case of requests version 2.18.0: `timeout` was an instance of `urllib3.util.timeout.Timeout` `Timeout` was an instance of `requests.packages.urllib3.util.timeout.Timeout` When the `isinstance(timeout, Timeout)` check fails the `urllib3.util.timeout.Timeout` object is passed as the `connection` argument to `requests.packages.urllib3.util.timeout.Timeout.from_float`. Signed-off-by: Matt Oberle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9fc4ad66e..31180d239 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ SOURCE_DIR = os.path.join(ROOT_DIR) requirements = [ - 'requests >= 2.5.2, != 2.11.0, != 2.12.2', + 'requests >= 2.5.2, != 2.11.0, != 2.12.2, != 2.18.0', 'six >= 1.4.0', 'websocket-client >= 0.32.0', 'docker-pycreds >= 0.2.1' From b0c30c8ac4ddf86ee77dcc69214cc93de4b03543 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 14 Jun 2017 12:20:47 -0700 Subject: [PATCH 19/30] DockerClient.secrets should be a @property Signed-off-by: Joffrey F --- docker/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/client.py b/docker/client.py index 09abd6332..66ef60f3f 100644 --- a/docker/client.py +++ b/docker/client.py @@ -119,6 +119,7 @@ def plugins(self): """ return PluginCollection(client=self) + @property def secrets(self): """ An object for managing secrets on the server. See the From d33e9ad030effb126dea39181ef401c80c442f15 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 15 Jun 2017 18:34:00 -0700 Subject: [PATCH 20/30] Update check_resource decorator to account for new resource names Signed-off-by: Joffrey F --- docker/api/client.py | 2 +- docker/api/container.py | 48 +++++++++++++++++++------------------- docker/api/exec_api.py | 4 ++-- docker/api/image.py | 12 +++++----- docker/api/network.py | 6 +++-- docker/api/plugin.py | 8 +++---- docker/api/secret.py | 4 ++-- docker/api/service.py | 10 ++++---- docker/api/swarm.py | 4 ++-- docker/types/services.py | 2 +- docker/utils/decorators.py | 31 ++++++++++++------------ 11 files changed, 66 insertions(+), 65 deletions(-) diff --git a/docker/api/client.py b/docker/api/client.py index 54ec6abb4..6e567b161 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -248,7 +248,7 @@ def _attach_params(self, override=None): 'stream': 1 } - @check_resource + @check_resource('container') def _attach_websocket(self, container, params=None): url = self._url("/containers/{0}/attach/ws", container) req = requests.Request("POST", url, params=self._attach_params(params)) diff --git a/docker/api/container.py b/docker/api/container.py index 97b540593..f7ff971b7 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -10,7 +10,7 @@ class ContainerApiMixin(object): - @utils.check_resource + @utils.check_resource('container') def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): """ @@ -54,7 +54,7 @@ def attach(self, container, stdout=True, stderr=True, return self._read_from_socket(response, stream) - @utils.check_resource + @utils.check_resource('container') def attach_socket(self, container, params=None, ws=False): """ Like ``attach``, but returns the underlying socket-like object for the @@ -93,7 +93,7 @@ def attach_socket(self, container, params=None, ws=False): ) ) - @utils.check_resource + @utils.check_resource('container') def commit(self, container, repository=None, tag=None, message=None, author=None, changes=None, conf=None): """ @@ -195,7 +195,7 @@ def containers(self, quiet=False, all=False, trunc=False, latest=False, x['Id'] = x['Id'][:12] return res - @utils.check_resource + @utils.check_resource('container') def copy(self, container, resource): """ Identical to the ``docker cp`` command. Get files/folders from the @@ -659,7 +659,7 @@ def create_endpoint_config(self, *args, **kwargs): """ return EndpointConfig(self._version, *args, **kwargs) - @utils.check_resource + @utils.check_resource('container') def diff(self, container): """ Inspect changes on a container's filesystem. @@ -678,7 +678,7 @@ def diff(self, container): self._get(self._url("/containers/{0}/changes", container)), True ) - @utils.check_resource + @utils.check_resource('container') def export(self, container): """ Export the contents of a filesystem as a tar archive. @@ -699,7 +699,7 @@ def export(self, container): self._raise_for_status(res) return res.raw - @utils.check_resource + @utils.check_resource('container') @utils.minimum_version('1.20') def get_archive(self, container, path): """ @@ -730,7 +730,7 @@ def get_archive(self, container, path): utils.decode_json_header(encoded_stat) if encoded_stat else None ) - @utils.check_resource + @utils.check_resource('container') def inspect_container(self, container): """ Identical to the `docker inspect` command, but only for containers. @@ -750,7 +750,7 @@ def inspect_container(self, container): self._get(self._url("/containers/{0}/json", container)), True ) - @utils.check_resource + @utils.check_resource('container') def kill(self, container, signal=None): """ Kill a container or send a signal to a container. @@ -773,7 +773,7 @@ def kill(self, container, signal=None): self._raise_for_status(res) - @utils.check_resource + @utils.check_resource('container') def logs(self, container, stdout=True, stderr=True, stream=False, timestamps=False, tail='all', since=None, follow=None): """ @@ -836,7 +836,7 @@ def logs(self, container, stdout=True, stderr=True, stream=False, logs=True ) - @utils.check_resource + @utils.check_resource('container') def pause(self, container): """ Pauses all processes within a container. @@ -852,7 +852,7 @@ def pause(self, container): res = self._post(url) self._raise_for_status(res) - @utils.check_resource + @utils.check_resource('container') def port(self, container, private_port): """ Lookup the public-facing port that is NAT-ed to ``private_port``. @@ -901,7 +901,7 @@ def port(self, container, private_port): return h_ports - @utils.check_resource + @utils.check_resource('container') @utils.minimum_version('1.20') def put_archive(self, container, path, data): """ @@ -949,7 +949,7 @@ def prune_containers(self, filters=None): url = self._url('/containers/prune') return self._result(self._post(url, params=params), True) - @utils.check_resource + @utils.check_resource('container') def remove_container(self, container, v=False, link=False, force=False): """ Remove a container. Similar to the ``docker rm`` command. @@ -973,7 +973,7 @@ def remove_container(self, container, v=False, link=False, force=False): self._raise_for_status(res) @utils.minimum_version('1.17') - @utils.check_resource + @utils.check_resource('container') def rename(self, container, name): """ Rename a container. Similar to the ``docker rename`` command. @@ -991,7 +991,7 @@ def rename(self, container, name): res = self._post(url, params=params) self._raise_for_status(res) - @utils.check_resource + @utils.check_resource('container') def resize(self, container, height, width): """ Resize the tty session. @@ -1010,7 +1010,7 @@ def resize(self, container, height, width): res = self._post(url, params=params) self._raise_for_status(res) - @utils.check_resource + @utils.check_resource('container') def restart(self, container, timeout=10): """ Restart a container. Similar to the ``docker restart`` command. @@ -1031,7 +1031,7 @@ def restart(self, container, timeout=10): res = self._post(url, params=params) self._raise_for_status(res) - @utils.check_resource + @utils.check_resource('container') def start(self, container, *args, **kwargs): """ Start a container. Similar to the ``docker start`` command, but @@ -1070,7 +1070,7 @@ def start(self, container, *args, **kwargs): self._raise_for_status(res) @utils.minimum_version('1.17') - @utils.check_resource + @utils.check_resource('container') def stats(self, container, decode=None, stream=True): """ Stream statistics for a specific container. Similar to the @@ -1096,7 +1096,7 @@ def stats(self, container, decode=None, stream=True): return self._result(self._get(url, params={'stream': False}), json=True) - @utils.check_resource + @utils.check_resource('container') def stop(self, container, timeout=10): """ Stops a container. Similar to the ``docker stop`` command. @@ -1117,7 +1117,7 @@ def stop(self, container, timeout=10): timeout=(timeout + (self.timeout or 0))) self._raise_for_status(res) - @utils.check_resource + @utils.check_resource('container') def top(self, container, ps_args=None): """ Display the running processes of a container. @@ -1139,7 +1139,7 @@ def top(self, container, ps_args=None): params['ps_args'] = ps_args return self._result(self._get(u, params=params), True) - @utils.check_resource + @utils.check_resource('container') def unpause(self, container): """ Unpause all processes within a container. @@ -1152,7 +1152,7 @@ def unpause(self, container): self._raise_for_status(res) @utils.minimum_version('1.22') - @utils.check_resource + @utils.check_resource('container') def update_container( self, container, blkio_weight=None, cpu_period=None, cpu_quota=None, cpu_shares=None, cpuset_cpus=None, cpuset_mems=None, mem_limit=None, @@ -1217,7 +1217,7 @@ def update_container( res = self._post_json(url, data=data) return self._result(res, True) - @utils.check_resource + @utils.check_resource('container') def wait(self, container, timeout=None): """ Block until a container stops, then return its exit code. Similar to diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py index 3ff65256e..2b407cef4 100644 --- a/docker/api/exec_api.py +++ b/docker/api/exec_api.py @@ -6,7 +6,7 @@ class ExecApiMixin(object): @utils.minimum_version('1.15') - @utils.check_resource + @utils.check_resource('container') def exec_create(self, container, cmd, stdout=True, stderr=True, stdin=False, tty=False, privileged=False, user='', environment=None): @@ -110,7 +110,7 @@ def exec_resize(self, exec_id, height=None, width=None): self._raise_for_status(res) @utils.minimum_version('1.15') - @utils.check_resource + @utils.check_resource('exec_id') def exec_start(self, exec_id, detach=False, tty=False, stream=False, socket=False): """ diff --git a/docker/api/image.py b/docker/api/image.py index 09eb086d7..181c4a1e4 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -12,7 +12,7 @@ class ImageApiMixin(object): - @utils.check_resource + @utils.check_resource('image') def get_image(self, image): """ Get a tarball of an image. Similar to the ``docker save`` command. @@ -39,7 +39,7 @@ def get_image(self, image): self._raise_for_status(res) return res.raw - @utils.check_resource + @utils.check_resource('image') def history(self, image): """ Show the history of an image. @@ -228,7 +228,7 @@ def import_image_from_image(self, image, repository=None, tag=None, image=image, repository=repository, tag=tag, changes=changes ) - @utils.check_resource + @utils.check_resource('image') def insert(self, image, url, path): if utils.compare_version('1.12', self._version) >= 0: raise errors.DeprecatedMethod( @@ -241,7 +241,7 @@ def insert(self, image, url, path): } return self._result(self._post(api_url, params=params)) - @utils.check_resource + @utils.check_resource('image') def inspect_image(self, image): """ Get detailed information about an image. Similar to the ``docker @@ -443,7 +443,7 @@ def push(self, repository, tag=None, stream=False, return self._result(response) - @utils.check_resource + @utils.check_resource('image') def remove_image(self, image, force=False, noprune=False): """ Remove an image. Similar to the ``docker rmi`` command. @@ -477,7 +477,7 @@ def search(self, term): True ) - @utils.check_resource + @utils.check_resource('image') def tag(self, image, repository, tag=None, force=False): """ Tag an image into a repository. Similar to the ``docker tag`` command. diff --git a/docker/api/network.py b/docker/api/network.py index 74f4cd2b3..bd2959fdc 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -156,6 +156,7 @@ def prune_networks(self, filters=None): return self._result(self._post(url, params=params), True) @minimum_version('1.21') + @check_resource('net_id') def remove_network(self, net_id): """ Remove a network. Similar to the ``docker network rm`` command. @@ -168,6 +169,7 @@ def remove_network(self, net_id): self._raise_for_status(res) @minimum_version('1.21') + @check_resource('net_id') def inspect_network(self, net_id, verbose=None): """ Get detailed information about a network. @@ -187,7 +189,7 @@ def inspect_network(self, net_id, verbose=None): res = self._get(url, params=params) return self._result(res, json=True) - @check_resource + @check_resource('image') @minimum_version('1.21') def connect_container_to_network(self, container, net_id, ipv4_address=None, ipv6_address=None, @@ -224,7 +226,7 @@ def connect_container_to_network(self, container, net_id, res = self._post_json(url, data=data) self._raise_for_status(res) - @check_resource + @check_resource('image') @minimum_version('1.21') def disconnect_container_from_network(self, container, net_id, force=False): diff --git a/docker/api/plugin.py b/docker/api/plugin.py index ba40c8829..87520ccee 100644 --- a/docker/api/plugin.py +++ b/docker/api/plugin.py @@ -5,7 +5,7 @@ class PluginApiMixin(object): @utils.minimum_version('1.25') - @utils.check_resource + @utils.check_resource('name') def configure_plugin(self, name, options): """ Configure a plugin. @@ -171,7 +171,7 @@ def plugin_privileges(self, name): return self._result(self._get(url, params=params), True) @utils.minimum_version('1.25') - @utils.check_resource + @utils.check_resource('name') def push_plugin(self, name): """ Push a plugin to the registry. @@ -195,7 +195,7 @@ def push_plugin(self, name): return self._stream_helper(res, decode=True) @utils.minimum_version('1.25') - @utils.check_resource + @utils.check_resource('name') def remove_plugin(self, name, force=False): """ Remove an installed plugin. @@ -215,7 +215,7 @@ def remove_plugin(self, name, force=False): return True @utils.minimum_version('1.26') - @utils.check_resource + @utils.check_resource('name') def upgrade_plugin(self, name, remote, privileges): """ Upgrade an installed plugin. diff --git a/docker/api/secret.py b/docker/api/secret.py index 03534a623..1760a3946 100644 --- a/docker/api/secret.py +++ b/docker/api/secret.py @@ -36,7 +36,7 @@ def create_secret(self, name, data, labels=None): ) @utils.minimum_version('1.25') - @utils.check_resource + @utils.check_resource('id') def inspect_secret(self, id): """ Retrieve secret metadata @@ -54,7 +54,7 @@ def inspect_secret(self, id): return self._result(self._get(url), True) @utils.minimum_version('1.25') - @utils.check_resource + @utils.check_resource('id') def remove_secret(self, id): """ Remove a secret diff --git a/docker/api/service.py b/docker/api/service.py index aea93cbfc..0f14776db 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -113,7 +113,7 @@ def create_service( ) @utils.minimum_version('1.24') - @utils.check_resource + @utils.check_resource('service') def inspect_service(self, service): """ Return information about a service. @@ -132,7 +132,7 @@ def inspect_service(self, service): return self._result(self._get(url), True) @utils.minimum_version('1.24') - @utils.check_resource + @utils.check_resource('task') def inspect_task(self, task): """ Retrieve information about a task. @@ -151,7 +151,7 @@ def inspect_task(self, task): return self._result(self._get(url), True) @utils.minimum_version('1.24') - @utils.check_resource + @utils.check_resource('service') def remove_service(self, service): """ Stop and remove a service. @@ -195,7 +195,7 @@ def services(self, filters=None): return self._result(self._get(url, params=params), True) @utils.minimum_version('1.25') - @utils.check_resource + @utils.check_resource('service') def service_logs(self, service, details=False, follow=False, stdout=False, stderr=False, since=0, timestamps=False, tail='all', is_tty=None): @@ -269,7 +269,7 @@ def tasks(self, filters=None): return self._result(self._get(url, params=params), True) @utils.minimum_version('1.24') - @utils.check_resource + @utils.check_resource('service') def update_service(self, service, version, task_template=None, name=None, labels=None, mode=None, update_config=None, networks=None, endpoint_config=None, diff --git a/docker/api/swarm.py b/docker/api/swarm.py index 88770562f..4fa0c4a12 100644 --- a/docker/api/swarm.py +++ b/docker/api/swarm.py @@ -117,7 +117,7 @@ def inspect_swarm(self): url = self._url('/swarm') return self._result(self._get(url), True) - @utils.check_resource + @utils.check_resource('node_id') @utils.minimum_version('1.24') def inspect_node(self, node_id): """ @@ -228,7 +228,7 @@ def nodes(self, filters=None): return self._result(self._get(url, params=params), True) - @utils.check_resource + @utils.check_resource('node_id') @utils.minimum_version('1.24') def remove_node(self, node_id, force=False): """ diff --git a/docker/types/services.py b/docker/types/services.py index 7456a42ba..9cec34ef8 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -443,7 +443,7 @@ class SecretReference(dict): gid (string): GID of the secret file's group. Default: 0 mode (int): File access mode inside the container. Default: 0o444 """ - @check_resource + @check_resource('secret_id') def __init__(self, secret_id, secret_name, filename=None, uid=None, gid=None, mode=0o444): self['SecretName'] = secret_name diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index 18cde412f..5e195c0ea 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -4,22 +4,21 @@ from . import utils -def check_resource(f): - @functools.wraps(f) - def wrapped(self, resource_id=None, *args, **kwargs): - if resource_id is None: - if kwargs.get('container'): - resource_id = kwargs.pop('container') - elif kwargs.get('image'): - resource_id = kwargs.pop('image') - if isinstance(resource_id, dict): - resource_id = resource_id.get('Id', resource_id.get('ID')) - if not resource_id: - raise errors.NullResource( - 'Resource ID was not provided' - ) - return f(self, resource_id, *args, **kwargs) - return wrapped +def check_resource(resource_name): + def decorator(f): + @functools.wraps(f) + def wrapped(self, resource_id=None, *args, **kwargs): + if resource_id is None and kwargs.get(resource_name): + resource_id = kwargs.pop(resource_name) + if isinstance(resource_id, dict): + resource_id = resource_id.get('Id', resource_id.get('ID')) + if not resource_id: + raise errors.NullResource( + 'Resource ID was not provided' + ) + return f(self, resource_id, *args, **kwargs) + return wrapped + return decorator def minimum_version(version): From c03a009e2dc2548de2ef280752109a9dc0660acb Mon Sep 17 00:00:00 2001 From: Chris Mark Date: Fri, 16 Jun 2017 18:30:24 +0300 Subject: [PATCH 21/30] Raising error in case of invalid value of since kwarg on Container.logs Signed-off-by: Chris Mark --- docker/api/container.py | 5 +++++ tests/unit/api_container_test.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/docker/api/container.py b/docker/api/container.py index 97b540593..7aeea2028 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -825,6 +825,11 @@ def logs(self, container, stdout=True, stderr=True, stream=False, params['since'] = utils.datetime_to_timestamp(since) elif (isinstance(since, int) and since > 0): params['since'] = since + else: + raise errors.InvalidArgument( + 'since value should be datetime or int, not {}'. + format(type(since)) + ) url = self._url("/containers/{0}/logs", container) res = self._get(url, params=params, stream=stream) return self._get_result(container, stream, res) diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index 662d3f590..3b135a813 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -1421,6 +1421,13 @@ def test_log_since_with_datetime(self): stream=False ) + def test_log_since_with_invalid_value_raises_error(self): + with mock.patch('docker.api.client.APIClient.inspect_container', + fake_inspect_container): + with self.assertRaises(docker.errors.InvalidArgument): + self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False, + follow=False, since=42.42) + def test_log_tty(self): m = mock.Mock() with mock.patch('docker.api.client.APIClient.inspect_container', From d638829f736aa7a942a780ff7796facb2713a959 Mon Sep 17 00:00:00 2001 From: Olivier Sallou Date: Fri, 16 Jun 2017 17:49:43 +0200 Subject: [PATCH 22/30] Closes #1588, image.tag does not return anything This patch returns the check made against api when tagging an image as stated in documentation Signed-off-by: Olivier Sallou --- docker/models/images.py | 2 +- tests/integration/models_images_test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/models/images.py b/docker/models/images.py index a9ed65ee3..e8af101d7 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -96,7 +96,7 @@ def tag(self, repository, tag=None, **kwargs): Returns: (bool): ``True`` if successful """ - self.client.api.tag(self.id, repository, tag=tag, **kwargs) + return self.client.api.tag(self.id, repository, tag=tag, **kwargs) class ImageCollection(Collection): diff --git a/tests/integration/models_images_test.py b/tests/integration/models_images_test.py index 6d61e4977..881df0a1e 100644 --- a/tests/integration/models_images_test.py +++ b/tests/integration/models_images_test.py @@ -71,7 +71,8 @@ def test_tag_and_remove(self): client = docker.from_env(version=TEST_API_VERSION) image = client.images.pull('alpine:latest') - image.tag(repo, tag) + result = image.tag(repo, tag) + assert result is True self.tmp_imgs.append(identifier) assert image.id in get_ids(client.images.list(repo)) assert image.id in get_ids(client.images.list(identifier)) From 1ea6618b09e3a74531a0fed1d44a5d698afe2339 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 17 May 2017 18:12:26 -0700 Subject: [PATCH 23/30] Add support for start_period in Healthcheck spec Signed-off-by: Joffrey F --- docker/api/container.py | 2 ++ docker/models/containers.py | 2 ++ docker/types/containers.py | 15 ++++++++++---- docker/types/healthcheck.py | 12 ++++++++++- tests/integration/api_healthcheck_test.py | 25 +++++++++++++++++++---- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docker/api/container.py b/docker/api/container.py index 97a39b65d..5668b43ce 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -418,6 +418,8 @@ def create_container(self, image, command=None, hostname=None, user=None, networking_config (dict): A networking configuration generated by :py:meth:`create_networking_config`. runtime (str): Runtime to use with this container. + healthcheck (dict): Specify a test to perform to check that the + container is healthy. Returns: A dictionary with an image 'Id' key and a 'Warnings' key. diff --git a/docker/models/containers.py b/docker/models/containers.py index 300c5a9d3..cf01b2750 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -516,6 +516,8 @@ def run(self, image, command=None, stdout=True, stderr=False, container, as a mapping of hostname to IP address. group_add (:py:class:`list`): List of additional group names and/or IDs that the container process will run as. + healthcheck (dict): Specify a test to perform to check that the + container is healthy. hostname (str): Optional hostname for the container. init (bool): Run an init inside the container that forwards signals and reaps processes diff --git a/docker/types/containers.py b/docker/types/containers.py index 6bbb57ae7..030e292bc 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -565,10 +565,17 @@ def __init__( '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 healthcheck is not None: + if version_lt(version, '1.24'): + raise errors.InvalidVersion( + 'Health options were only introduced in API version 1.24' + ) + + if version_lt(version, '1.29') and 'StartPeriod' in healthcheck: + raise errors.InvalidVersion( + 'healthcheck start period was introduced in API ' + 'version 1.29' + ) if isinstance(command, six.string_types): command = split_command(command) diff --git a/docker/types/healthcheck.py b/docker/types/healthcheck.py index ba63d21ed..8ea9a35f5 100644 --- a/docker/types/healthcheck.py +++ b/docker/types/healthcheck.py @@ -12,12 +12,14 @@ def __init__(self, **kwargs): interval = kwargs.get('interval', kwargs.get('Interval')) timeout = kwargs.get('timeout', kwargs.get('Timeout')) retries = kwargs.get('retries', kwargs.get('Retries')) + start_period = kwargs.get('start_period', kwargs.get('StartPeriod')) super(Healthcheck, self).__init__({ 'Test': test, 'Interval': interval, 'Timeout': timeout, - 'Retries': retries + 'Retries': retries, + 'StartPeriod': start_period }) @property @@ -51,3 +53,11 @@ def retries(self): @retries.setter def retries(self, value): self['Retries'] = value + + @property + def start_period(self): + return self['StartPeriod'] + + @start_period.setter + def start_period(self, value): + self['StartPeriod'] = value diff --git a/tests/integration/api_healthcheck_test.py b/tests/integration/api_healthcheck_test.py index afe1dea21..211042d48 100644 --- a/tests/integration/api_healthcheck_test.py +++ b/tests/integration/api_healthcheck_test.py @@ -28,8 +28,8 @@ def test_healthcheck_passes(self): container = self.client.create_container( BUSYBOX, 'top', healthcheck=dict( test="true", - interval=1*SECOND, - timeout=1*SECOND, + interval=1 * SECOND, + timeout=1 * SECOND, retries=1, )) self.tmp_containers.append(container) @@ -41,10 +41,27 @@ def test_healthcheck_fails(self): container = self.client.create_container( BUSYBOX, 'top', healthcheck=dict( test="false", - interval=1*SECOND, - timeout=1*SECOND, + interval=1 * SECOND, + timeout=1 * SECOND, retries=1, )) self.tmp_containers.append(container) self.client.start(container) wait_on_health_status(self.client, container, "unhealthy") + + @helpers.requires_api_version('1.29') + def test_healthcheck_start_period(self): + container = self.client.create_container( + BUSYBOX, 'top', healthcheck=dict( + test="echo 'x' >> /counter.txt && " + "test `cat /counter.txt | wc -l` -ge 3", + interval=1 * SECOND, + timeout=1 * SECOND, + retries=1, + start_period=3 * SECOND + ) + ) + + self.tmp_containers.append(container) + self.client.start(container) + wait_on_health_status(self.client, container, "healthy") From 39bb78ac694d8d6e53882d3dbc9ebc3c92f5519d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 19 Jun 2017 15:50:28 -0700 Subject: [PATCH 24/30] Add network_mode support to Client.build Signed-off-by: Joffrey F --- docker/api/build.py | 22 +++++++++++++------ docker/models/images.py | 11 ++++++---- tests/integration/api_build_test.py | 33 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/docker/api/build.py b/docker/api/build.py index f30be4168..cbef4a8b1 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -18,7 +18,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, 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): + labels=None, cache_from=None, target=None, network_mode=None): """ Similar to the ``docker build`` command. Either ``path`` or ``fileobj`` needs to be set. ``path`` can be a local path (to a directory @@ -88,14 +88,16 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, - cpusetcpus (str): CPUs in which to allow execution, e.g., ``"0-3"``, ``"0,1"`` decode (bool): If set to ``True``, the returned stream will be - decoded into dicts on the fly. Default ``False``. + decoded into dicts on the fly. Default ``False`` shmsize (int): Size of `/dev/shm` in bytes. The size must be - greater than 0. If omitted the system uses 64MB. - labels (dict): A dictionary of labels to set on the image. + greater than 0. If omitted the system uses 64MB + labels (dict): A dictionary of labels to set on the image cache_from (list): A list of images used for build cache - resolution. + resolution target (str): Name of the build-stage to build in a multi-stage - Dockerfile. + Dockerfile + network_mode (str): networking mode for the run commands during + build Returns: A generator for the build output. @@ -208,6 +210,14 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, 'target was only introduced in API version 1.29' ) + if network_mode: + if utils.version_gte(self._version, '1.25'): + params.update({'networkmode': network_mode}) + else: + raise errors.InvalidVersion( + 'network_mode was only introduced in API version 1.25' + ) + if context is not None: headers = {'Content-Type': 'application/tar'} if encoding: diff --git a/docker/models/images.py b/docker/models/images.py index 7e999b0c4..d4e24c606 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -144,12 +144,15 @@ def build(self, **kwargs): - cpushares (int): CPU shares (relative weight) - cpusetcpus (str): CPUs in which to allow execution, e.g., ``"0-3"``, ``"0,1"`` - decode (bool): If set to ``True``, the returned stream will be - decoded into dicts on the fly. Default ``False``. + shmsize (int): Size of `/dev/shm` in bytes. The size must be + greater than 0. If omitted the system uses 64MB + labels (dict): A dictionary of labels to set on the image cache_from (list): A list of images used for build cache - resolution. + resolution target (str): Name of the build-stage to build in a multi-stage - Dockerfile. + Dockerfile + network_mode (str): networking mode for the run commands during + build Returns: (:py:class:`Image`): The built image. diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index 623b66093..609964f0b 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -5,6 +5,7 @@ from docker import errors +import pytest import six from .base import BaseAPIIntegrationTest @@ -211,6 +212,38 @@ def test_build_container_with_target(self): info = self.client.inspect_image('build1') self.assertEqual(info['Config']['OnBuild'], []) + @requires_api_version('1.25') + def test_build_with_network_mode(self): + script = io.BytesIO('\n'.join([ + 'FROM busybox', + 'RUN wget http://google.com' + ]).encode('ascii')) + + stream = self.client.build( + fileobj=script, network_mode='bridge', + tag='dockerpytest_bridgebuild' + ) + + self.tmp_imgs.append('dockerpytest_bridgebuild') + for chunk in stream: + pass + + assert self.client.inspect_image('dockerpytest_bridgebuild') + + script.seek(0) + stream = self.client.build( + fileobj=script, network_mode='none', + tag='dockerpytest_nonebuild', nocache=True, decode=True + ) + + self.tmp_imgs.append('dockerpytest_nonebuild') + logs = [chunk for chunk in stream] + assert 'errorDetail' in logs[-1] + assert logs[-1]['errorDetail']['code'] == 1 + + with pytest.raises(errors.NotFound): + self.client.inspect_image('dockerpytest_nonebuild') + def test_build_stderr_data(self): control_chars = ['\x1b[91m', '\x1b[0m'] snippet = 'Ancient Temple (Mystic Oriental Dream ~ Ancient Temple)' From 0165a343d51c8051f1793f62202e2f053ab1b594 Mon Sep 17 00:00:00 2001 From: An Ha Date: Mon, 12 Jun 2017 16:33:56 -0400 Subject: [PATCH 25/30] Add attributes for pickling When using the multiprocessing module, it throws an AttributeError, complaining that the object does not have the attribute used. This adds the missing attributes and allows them to be pickled. Signed-off-by: An Ha --- docker/api/client.py | 6 ++++++ docker/transport/npipeconn.py | 5 +++++ docker/transport/ssladapter.py | 4 ++++ docker/transport/unixconn.py | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/docker/api/client.py b/docker/api/client.py index 6e567b161..65b5baa96 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -83,6 +83,12 @@ class APIClient( configuration. user_agent (str): Set a custom user agent for requests to the server. """ + + __attrs__ = requests.Session.__attrs__ + ['_auth_configs', + '_version', + 'base_url', + 'timeout'] + def __init__(self, base_url=None, version=None, timeout=DEFAULT_TIMEOUT_SECONDS, tls=False, user_agent=DEFAULT_USER_AGENT, num_pools=DEFAULT_NUM_POOLS): diff --git a/docker/transport/npipeconn.py b/docker/transport/npipeconn.py index db059b445..ab9b90480 100644 --- a/docker/transport/npipeconn.py +++ b/docker/transport/npipeconn.py @@ -69,6 +69,11 @@ def _get_conn(self, timeout): class NpipeAdapter(requests.adapters.HTTPAdapter): + + __attrs__ = requests.adapters.HTTPAdapter.__attrs__ + ['npipe_path', + 'pools', + 'timeout'] + def __init__(self, base_url, timeout=60, pool_connections=constants.DEFAULT_NUM_POOLS): self.npipe_path = base_url.replace('npipe://', '') diff --git a/docker/transport/ssladapter.py b/docker/transport/ssladapter.py index 31f45fc45..8fafec355 100644 --- a/docker/transport/ssladapter.py +++ b/docker/transport/ssladapter.py @@ -25,6 +25,10 @@ class SSLAdapter(HTTPAdapter): '''An HTTPS Transport Adapter that uses an arbitrary SSL version.''' + __attrs__ = HTTPAdapter.__attrs__ + ['assert_fingerprint', + 'assert_hostname', + 'ssl_version'] + def __init__(self, ssl_version=None, assert_hostname=None, assert_fingerprint=None, **kwargs): self.ssl_version = ssl_version diff --git a/docker/transport/unixconn.py b/docker/transport/unixconn.py index 978c87a1b..3565cfb62 100644 --- a/docker/transport/unixconn.py +++ b/docker/transport/unixconn.py @@ -50,6 +50,11 @@ def _new_conn(self): class UnixAdapter(requests.adapters.HTTPAdapter): + + __attrs__ = requests.adapters.HTTPAdapter.__attrs__ + ['pools', + 'socket_path', + 'timeout'] + def __init__(self, socket_url, timeout=60, pool_connections=constants.DEFAULT_NUM_POOLS): socket_path = socket_url.replace('http+unix://', '') From 9b9fb0aa0140cae1fc2c6eb549f2615501453eb9 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 20 Jun 2017 16:07:15 -0700 Subject: [PATCH 26/30] Make sure data is written in prune test so space can be reclaimed Signed-off-by: Joffrey F --- tests/integration/api_container_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index de3fe7183..f8b474a11 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -1139,7 +1139,9 @@ def test_pause_unpause(self): class PruneTest(BaseAPIIntegrationTest): @requires_api_version('1.25') def test_prune_containers(self): - container1 = self.client.create_container(BUSYBOX, ['echo', 'hello']) + container1 = self.client.create_container( + BUSYBOX, ['sh', '-c', 'echo hello > /data.txt'] + ) container2 = self.client.create_container(BUSYBOX, ['sleep', '9999']) self.client.start(container1) self.client.start(container2) From 06d2553b9c3a4c91d1ef2110ea120da15605de2f Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 21 Jun 2017 16:29:25 -0700 Subject: [PATCH 27/30] Add support for ContainerSpec.TTY Signed-off-by: Joffrey F --- docker/api/service.py | 5 +++++ docker/models/services.py | 2 ++ docker/types/services.py | 6 +++++- tests/integration/api_service_test.py | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docker/api/service.py b/docker/api/service.py index 0f14776db..cc16cc37d 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -38,6 +38,11 @@ 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 utils.version_lt(version, '1.25'): + raise errors.InvalidVersion( + 'ContainerSpec.TTY is not supported in API version < 1.25' + ) class ServiceApiMixin(object): diff --git a/docker/models/services.py b/docker/models/services.py index c10804ded..e1e2ea6a4 100644 --- a/docker/models/services.py +++ b/docker/models/services.py @@ -146,6 +146,7 @@ def create(self, image, command=None, **kwargs): of the service. Default: ``None`` user (str): User to run commands as. workdir (str): Working directory for commands to run. + tty (boolean): Whether a pseudo-TTY should be allocated. Returns: (:py:class:`Service`) The created service. @@ -212,6 +213,7 @@ def list(self, **kwargs): 'mounts', 'stop_grace_period', 'secrets', + 'tty' ] # kwargs to copy straight over to TaskTemplate diff --git a/docker/types/services.py b/docker/types/services.py index 9cec34ef8..8411b70a4 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -84,10 +84,11 @@ class ContainerSpec(dict): terminate before forcefully killing it. secrets (list of py:class:`SecretReference`): List of secrets to be made available inside the containers. + tty (boolean): Whether a pseudo-TTY should be allocated. """ def __init__(self, image, command=None, args=None, hostname=None, env=None, workdir=None, user=None, labels=None, mounts=None, - stop_grace_period=None, secrets=None): + stop_grace_period=None, secrets=None, tty=None): self['Image'] = image if isinstance(command, six.string_types): @@ -125,6 +126,9 @@ def __init__(self, image, command=None, args=None, hostname=None, env=None, raise TypeError('secrets must be a list') self['Secrets'] = secrets + if tty is not None: + self['TTY'] = tty + class Mount(dict): """ diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index 8ac852d96..54111a7bb 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -359,6 +359,23 @@ def test_create_service_with_env(self): assert 'Env' in con_spec assert con_spec['Env'] == ['DOCKER_PY_TEST=1'] + @requires_api_version('1.25') + def test_create_service_with_tty(self): + container_spec = docker.types.ContainerSpec( + BUSYBOX, ['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 320c81047107a4350bb430f24825d116a91d1d8f Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 22 Jun 2017 11:51:31 -0700 Subject: [PATCH 28/30] Support credHelpers section in config.json Signed-off-by: Joffrey F --- docker/auth.py | 30 ++++++++++++++++++------ tests/unit/auth_test.py | 51 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/docker/auth.py b/docker/auth.py index 7c1ce7618..ec9c45b97 100644 --- a/docker/auth.py +++ b/docker/auth.py @@ -70,6 +70,15 @@ def split_repo_name(repo_name): return tuple(parts) +def get_credential_store(authconfig, registry): + if not registry or registry == INDEX_NAME: + registry = 'https://index.docker.io/v1/' + + return authconfig.get('credHelpers', {}).get(registry) or authconfig.get( + 'credsStore' + ) + + def resolve_authconfig(authconfig, registry=None): """ Returns the authentication data from the given auth configuration for a @@ -77,13 +86,17 @@ def resolve_authconfig(authconfig, registry=None): with full URLs are stripped down to hostnames before checking for a match. Returns None if no match was found. """ - if 'credsStore' in authconfig: - log.debug( - 'Using credentials store "{0}"'.format(authconfig['credsStore']) - ) - return _resolve_authconfig_credstore( - authconfig, registry, authconfig['credsStore'] - ) + + if 'credHelpers' in authconfig or 'credsStore' in authconfig: + store_name = get_credential_store(authconfig, registry) + if store_name is not None: + log.debug( + 'Using credentials store "{0}"'.format(store_name) + ) + return _resolve_authconfig_credstore( + authconfig, registry, store_name + ) + # Default to the public index server registry = resolve_index_name(registry) if registry else INDEX_NAME log.debug("Looking for auth entry for {0}".format(repr(registry))) @@ -274,6 +287,9 @@ def load_config(config_path=None): if data.get('credsStore'): log.debug("Found 'credsStore' section") res.update({'credsStore': data['credsStore']}) + if data.get('credHelpers'): + log.debug("Found 'credHelpers' section") + res.update({'credHelpers': data['credHelpers']}) if res: return res else: diff --git a/tests/unit/auth_test.py b/tests/unit/auth_test.py index f9f6fc146..56fd50c25 100644 --- a/tests/unit/auth_test.py +++ b/tests/unit/auth_test.py @@ -272,6 +272,57 @@ def test_resolve_registry_and_auth_unauthenticated_registry(self): ) +class CredStoreTest(unittest.TestCase): + def test_get_credential_store(self): + auth_config = { + 'credHelpers': { + 'registry1.io': 'truesecret', + 'registry2.io': 'powerlock' + }, + 'credsStore': 'blackbox', + } + + assert auth.get_credential_store( + auth_config, 'registry1.io' + ) == 'truesecret' + assert auth.get_credential_store( + auth_config, 'registry2.io' + ) == 'powerlock' + assert auth.get_credential_store( + auth_config, 'registry3.io' + ) == 'blackbox' + + def test_get_credential_store_no_default(self): + auth_config = { + 'credHelpers': { + 'registry1.io': 'truesecret', + 'registry2.io': 'powerlock' + }, + } + assert auth.get_credential_store( + auth_config, 'registry2.io' + ) == 'powerlock' + assert auth.get_credential_store( + auth_config, 'registry3.io' + ) is None + + def test_get_credential_store_default_index(self): + auth_config = { + 'credHelpers': { + 'https://index.docker.io/v1/': 'powerlock' + }, + 'credsStore': 'truesecret' + } + + assert auth.get_credential_store(auth_config, None) == 'powerlock' + assert auth.get_credential_store( + auth_config, 'docker.io' + ) == 'powerlock' + assert auth.get_credential_store( + auth_config, 'images.io' + ) == 'truesecret' + + class FindConfigFileTest(unittest.TestCase): def tmpdir(self, name): tmpdir = ensuretemp(name) From 015fe1cf5eacf93f965bd68b6e618adf2d9c115a Mon Sep 17 00:00:00 2001 From: Boik Date: Tue, 20 Dec 2016 10:44:00 +0800 Subject: [PATCH 29/30] Correct the description of dns_opt option of create_container Signed-off-by: Boik --- docker/api/container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/api/container.py b/docker/api/container.py index 91084219a..532a9c6d8 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -391,8 +391,6 @@ def create_container(self, image, command=None, hostname=None, user=None, ``{"PASSWORD": "xxx"}``. dns (:py:class:`list`): DNS name servers. Deprecated since API version 1.10. Use ``host_config`` instead. - dns_opt (:py:class:`list`): Additional options to be added to the - container's ``resolv.conf`` file volumes (str or list): List of paths inside the container to use as volumes. volumes_from (:py:class:`list`): List of container names or Ids to @@ -498,6 +496,8 @@ def create_host_config(self, *args, **kwargs): to have read-write access to the host's ``/dev/sda`` via a node named ``/dev/xvda`` inside the container. dns (:py:class:`list`): Set custom DNS servers. + dns_opt (:py:class:`list`): Additional options to be added to the + container's ``resolv.conf`` file dns_search (:py:class:`list`): DNS search domains. extra_hosts (dict): Addtional hostnames to resolve inside the container, as a mapping of hostname to IP address. From 1ad6859600258eca17f91e4f71c1de5788321777 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 22 Jun 2017 17:17:13 -0700 Subject: [PATCH 30/30] Bump 2.4.0 Signed-off-by: Joffrey F --- docker/version.py | 2 +- docs/api.rst | 1 + docs/change-log.md | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/docker/version.py b/docker/version.py index 6979e1bef..8f40f467a 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.4.0-dev" +version = "2.4.0" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) diff --git a/docs/api.rst b/docs/api.rst index 52cd26b2c..0b10f387d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -128,6 +128,7 @@ Configuration types .. autoclass:: DriverConfig .. autoclass:: EndpointSpec .. autoclass:: Mount +.. autoclass:: Placement .. autoclass:: Resources .. autoclass:: RestartPolicy .. autoclass:: SecretReference diff --git a/docs/change-log.md b/docs/change-log.md index 3d58f931f..194f73479 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,44 @@ Change log ========== +2.4.0 +----- + +[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/33?closed=1) + +### Features + +* Added support for the `target` and `network_mode` parameters in + `APIClient.build` and `DockerClient.images.build`. +* Added support for the `runtime` parameter in `APIClient.create_container` + and `DockerClient.containers.run`. +* Added support for the `ingress` parameter in `APIClient.create_network` and + `DockerClient.networks.create`. +* Added support for `placement` configuration in `docker.types.TaskTemplate`. +* Added support for `tty` configuration in `docker.types.ContainerSpec`. +* Added support for `start_period` configuration in `docker.types.Healthcheck`. +* The `credHelpers` section in Docker's configuration file is now recognized. +* Port specifications including IPv6 endpoints are now supported. + +### Bugfixes + +* Fixed a bug where instantiating a `DockerClient` using `docker.from_env` + wouldn't correctly set the default timeout value. +* Fixed a bug where `DockerClient.secrets` was not accessible as a property. +* Fixed a bug where `DockerClient.build` would sometimes return the wrong + image. +* Fixed a bug where values for `HostConfig.nano_cpus` exceeding 2^32 would + raise a type error. +* `Image.tag` now properly returns `True` when the operation is successful. +* `APIClient.logs` and `Container.logs` now raise an exception if the `since` + argument uses an unsupported type instead of ignoring the value. +* Fixed a bug where some methods would raise a `NullResource` exception when + the resource ID was provided using a keyword argument. + +### Miscellaneous + +* `APIClient` instances can now be pickled. + 2.3.0 -----