diff --git a/docker/api/service.py b/docker/api/service.py index d2621e68..0b2abdc9 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -62,10 +62,24 @@ class ServiceApiMixin(object): 'Labels': labels, 'TaskTemplate': task_template, 'Mode': mode, - 'UpdateConfig': update_config, 'Networks': utils.convert_service_networks(networks), 'EndpointSpec': endpoint_spec } + + 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( self._post_json(url, data=data, headers=headers), True ) @@ -230,6 +244,12 @@ class ServiceApiMixin(object): 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) @@ -238,7 +258,19 @@ class ServiceApiMixin(object): 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: data['Networks'] = utils.convert_service_networks(networks) if endpoint_spec is not None: diff --git a/docker/types/services.py b/docker/types/services.py index ec0fcb15..5f7b2fb0 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -21,9 +21,11 @@ class TaskTemplate(dict): 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. + force_update (int): A counter that triggers an update even if no + relevant parameters have been changed. """ def __init__(self, container_spec, resources=None, restart_policy=None, - placement=None, log_driver=None): + placement=None, log_driver=None, force_update=None): self['ContainerSpec'] = container_spec if resources: self['Resources'] = resources @@ -36,6 +38,11 @@ class TaskTemplate(dict): if log_driver: self['LogDriver'] = log_driver + if force_update is not None: + if not isinstance(force_update, int): + raise TypeError('force_update must be an integer') + self['ForceUpdate'] = force_update + @property def container_spec(self): return self.get('ContainerSpec') @@ -233,8 +240,14 @@ class UpdateConfig(dict): failure_action (string): Action to take if an updated task fails to run, or stops running during the update. Acceptable values are ``continue`` and ``pause``. Default: ``continue`` + monitor (int): Amount of time to monitor each updated task for + failures, in nanoseconds. + max_failure_ratio (float): The fraction of tasks that may fail during + an update before the failure action is invoked, specified as a + floating point number between 0 and 1. Default: 0 """ - def __init__(self, parallelism=0, delay=None, failure_action='continue'): + def __init__(self, parallelism=0, delay=None, failure_action='continue', + monitor=None, max_failure_ratio=None): self['Parallelism'] = parallelism if delay is not None: self['Delay'] = delay @@ -244,6 +257,20 @@ class UpdateConfig(dict): ) self['FailureAction'] = failure_action + if monitor is not None: + if not isinstance(monitor, int): + raise TypeError('monitor must be an integer') + self['Monitor'] = monitor + + if max_failure_ratio is not None: + if not isinstance(max_failure_ratio, (float, int)): + raise TypeError('max_failure_ratio must be a float') + if max_failure_ratio > 1 or max_failure_ratio < 0: + raise errors.InvalidArgument( + 'max_failure_ratio must be a number between 0 and 1' + ) + self['MaxFailureRatio'] = max_failure_ratio + class RestartConditionTypesEnum(object): _values = ( diff --git a/tests/helpers.py b/tests/helpers.py index b742c960..e8ba4d6b 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -70,7 +70,9 @@ def force_leave_swarm(client): occasionally throws "context deadline exceeded" errors when leaving.""" while True: try: - return client.swarm.leave(force=True) + if isinstance(client, docker.DockerClient): + return client.swarm.leave(force=True) + return client.leave_swarm(force=True) # elif APIClient except docker.errors.APIError as e: if e.explanation == "context deadline exceeded": continue diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index 77d7d28f..46b0a79e 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -2,14 +2,14 @@ import random import docker -from ..helpers import requires_api_version +from ..helpers import force_leave_swarm, requires_api_version from .base import BaseAPIIntegrationTest class ServiceTest(BaseAPIIntegrationTest): def setUp(self): super(ServiceTest, self).setUp() - self.client.leave_swarm(force=True) + force_leave_swarm(self.client) self.init_swarm() def tearDown(self): @@ -19,7 +19,7 @@ class ServiceTest(BaseAPIIntegrationTest): self.client.remove_service(service['ID']) except docker.errors.APIError: pass - self.client.leave_swarm(force=True) + force_leave_swarm(self.client) def get_service_name(self): return 'dockerpytest_{0:x}'.format(random.getrandbits(64)) @@ -155,6 +155,23 @@ class ServiceTest(BaseAPIIntegrationTest): assert update_config['Delay'] == uc['Delay'] assert update_config['FailureAction'] == uc['FailureAction'] + @requires_api_version('1.25') + def test_create_service_with_update_config_monitor(self): + container_spec = docker.types.ContainerSpec('busybox', ['true']) + task_tmpl = docker.types.TaskTemplate(container_spec) + update_config = docker.types.UpdateConfig( + monitor=300000000, max_failure_ratio=0.4 + ) + name = self.get_service_name() + svc_id = self.client.create_service( + task_tmpl, update_config=update_config, name=name + ) + svc_info = self.client.inspect_service(svc_id) + assert 'UpdateConfig' in svc_info['Spec'] + uc = svc_info['Spec']['UpdateConfig'] + assert update_config['Monitor'] == uc['Monitor'] + assert update_config['MaxFailureRatio'] == uc['MaxFailureRatio'] + def test_create_service_with_restart_policy(self): container_spec = docker.types.ContainerSpec('busybox', ['true']) policy = docker.types.RestartPolicy( @@ -279,3 +296,24 @@ class ServiceTest(BaseAPIIntegrationTest): assert 'Mode' in svc_info['Spec'] assert 'Replicated' in svc_info['Spec']['Mode'] assert svc_info['Spec']['Mode']['Replicated'] == {'Replicas': 5} + + @requires_api_version('1.25') + def test_update_service_force_update(self): + container_spec = docker.types.ContainerSpec( + 'busybox', ['echo', 'hello'] + ) + 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 'ForceUpdate' in svc_info['Spec']['TaskTemplate'] + assert svc_info['Spec']['TaskTemplate']['ForceUpdate'] == 0 + version_index = svc_info['Version']['Index'] + + task_tmpl = docker.types.TaskTemplate(container_spec, force_update=10) + self.client.update_service(name, version_index, task_tmpl, name=name) + svc_info = self.client.inspect_service(svc_id) + new_index = svc_info['Version']['Index'] + assert new_index > version_index + assert svc_info['Spec']['TaskTemplate']['ForceUpdate'] == 10 diff --git a/tests/integration/api_swarm_test.py b/tests/integration/api_swarm_test.py index a8f439c8..d06cac21 100644 --- a/tests/integration/api_swarm_test.py +++ b/tests/integration/api_swarm_test.py @@ -2,18 +2,18 @@ import copy import docker import pytest -from ..helpers import requires_api_version +from ..helpers import force_leave_swarm, requires_api_version from .base import BaseAPIIntegrationTest class SwarmTest(BaseAPIIntegrationTest): def setUp(self): super(SwarmTest, self).setUp() - self.client.leave_swarm(force=True) + force_leave_swarm(self.client) def tearDown(self): super(SwarmTest, self).tearDown() - self.client.leave_swarm(force=True) + force_leave_swarm(self.client) @requires_api_version('1.24') def test_init_swarm_simple(self):