From 9cc021dfa684ab1a614d473e78f9c4c0fc960585 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 May 2017 19:05:32 -0700 Subject: [PATCH] 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 4972c16d..aea93cbf 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -3,6 +3,43 @@ from .. import auth, errors, utils 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 @@ class ServiceApiMixin(object): ) 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 @@ class ServiceApiMixin(object): } 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 @@ class ServiceApiMixin(object): ) 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 @@ 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) @@ -308,17 +332,6 @@ 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: diff --git a/docker/types/__init__.py b/docker/types/__init__.py index 0e887760..edc919df 100644 --- a/docker/types/__init__.py +++ b/docker/types/__init__.py @@ -3,7 +3,7 @@ from .containers import ContainerConfig, HostConfig, LogConfig, Ulimit 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 012f7b01..7456a42b 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 @@ class TaskTemplate(dict): 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 @@ class SecretReference(dict): '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 914e516b..8ac852d9 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -270,6 +270,49 @@ class ServiceTest(BaseAPIIntegrationTest): 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)