Merge pull request #1615 from docker/service-placement

Add support for placement preferences and platforms in TaskTemplate
This commit is contained in:
Joffrey F 2017-05-17 15:25:40 -07:00 committed by GitHub
commit fb16d14544
4 changed files with 115 additions and 32 deletions

View File

@ -3,6 +3,43 @@ from .. import auth, errors, utils
from ..types import ServiceMode 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): class ServiceApiMixin(object):
@utils.minimum_version('1.24') @utils.minimum_version('1.24')
def create_service( def create_service(
@ -43,6 +80,8 @@ class ServiceApiMixin(object):
) )
endpoint_spec = endpoint_config endpoint_spec = endpoint_config
_check_api_features(self._version, task_template, update_config)
url = self._url('/services/create') url = self._url('/services/create')
headers = {} headers = {}
image = task_template.get('ContainerSpec', {}).get('Image', None) image = task_template.get('ContainerSpec', {}).get('Image', None)
@ -67,17 +106,6 @@ class ServiceApiMixin(object):
} }
if update_config is not None: 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 data['UpdateConfig'] = update_config
return self._result( return self._result(
@ -282,6 +310,8 @@ class ServiceApiMixin(object):
) )
endpoint_spec = endpoint_config endpoint_spec = endpoint_config
_check_api_features(self._version, task_template, update_config)
url = self._url('/services/{0}/update', service) url = self._url('/services/{0}/update', service)
data = {} data = {}
headers = {} headers = {}
@ -294,12 +324,6 @@ class ServiceApiMixin(object):
mode = ServiceMode(mode) mode = ServiceMode(mode)
data['Mode'] = mode data['Mode'] = mode
if task_template is not None: 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) image = task_template.get('ContainerSpec', {}).get('Image', None)
if image is not None: if image is not None:
registry, repo_name = auth.resolve_repository_name(image) registry, repo_name = auth.resolve_repository_name(image)
@ -308,17 +332,6 @@ class ServiceApiMixin(object):
headers['X-Registry-Auth'] = auth_header headers['X-Registry-Auth'] = auth_header
data['TaskTemplate'] = task_template data['TaskTemplate'] = task_template
if update_config is not None: 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 data['UpdateConfig'] = update_config
if networks is not None: if networks is not None:

View File

@ -3,7 +3,7 @@ from .containers import ContainerConfig, HostConfig, LogConfig, Ulimit
from .healthcheck import Healthcheck from .healthcheck import Healthcheck
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
from .services import ( from .services import (
ContainerSpec, DriverConfig, EndpointSpec, Mount, Resources, RestartPolicy, ContainerSpec, DriverConfig, EndpointSpec, Mount, Placement, Resources,
SecretReference, ServiceMode, TaskTemplate, UpdateConfig RestartPolicy, SecretReference, ServiceMode, TaskTemplate, UpdateConfig
) )
from .swarm import SwarmSpec, SwarmExternalCA from .swarm import SwarmSpec, SwarmExternalCA

View File

@ -20,7 +20,9 @@ class TaskTemplate(dict):
individual container created as part of the service. individual container created as part of the service.
restart_policy (RestartPolicy): Specification for the restart policy restart_policy (RestartPolicy): Specification for the restart policy
which applies to containers created as part of this service. 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 force_update (int): A counter that triggers an update even if no
relevant parameters have been changed. relevant parameters have been changed.
""" """
@ -33,7 +35,7 @@ class TaskTemplate(dict):
self['RestartPolicy'] = restart_policy self['RestartPolicy'] = restart_policy
if placement: if placement:
if isinstance(placement, list): if isinstance(placement, list):
placement = {'Constraints': placement} placement = Placement(constraints=placement)
self['Placement'] = placement self['Placement'] = placement
if log_driver: if log_driver:
self['LogDriver'] = log_driver self['LogDriver'] = log_driver
@ -452,3 +454,28 @@ class SecretReference(dict):
'GID': gid or '0', 'GID': gid or '0',
'Mode': mode '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]
})

View File

@ -270,6 +270,49 @@ class ServiceTest(BaseAPIIntegrationTest):
assert (svc_info['Spec']['TaskTemplate']['Placement'] == assert (svc_info['Spec']['TaskTemplate']['Placement'] ==
{'Constraints': ['node.id=={}'.format(node_id)]}) {'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): def test_create_service_with_endpoint_spec(self):
container_spec = docker.types.ContainerSpec(BUSYBOX, ['true']) container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
task_tmpl = docker.types.TaskTemplate(container_spec) task_tmpl = docker.types.TaskTemplate(container_spec)