Skip to content

Commit fb16d14

Browse files
authored
Merge pull request #1615 from docker/service-placement
Add support for placement preferences and platforms in TaskTemplate
2 parents 7af7e1b + 9cc021d commit fb16d14

File tree

4 files changed

+115
-32
lines changed

4 files changed

+115
-32
lines changed

docker/api/service.py

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,43 @@
33
from ..types import ServiceMode
44

55

6+
def _check_api_features(version, task_template, update_config):
7+
if update_config is not None:
8+
if utils.version_lt(version, '1.25'):
9+
if 'MaxFailureRatio' in update_config:
10+
raise errors.InvalidVersion(
11+
'UpdateConfig.max_failure_ratio is not supported in'
12+
' API version < 1.25'
13+
)
14+
if 'Monitor' in update_config:
15+
raise errors.InvalidVersion(
16+
'UpdateConfig.monitor is not supported in'
17+
' API version < 1.25'
18+
)
19+
20+
if task_template is not None:
21+
if 'ForceUpdate' in task_template and utils.version_lt(
22+
version, '1.25'):
23+
raise errors.InvalidVersion(
24+
'force_update is not supported in API version < 1.25'
25+
)
26+
27+
if task_template.get('Placement'):
28+
if utils.version_lt(version, '1.30'):
29+
if task_template['Placement'].get('Platforms'):
30+
raise errors.InvalidVersion(
31+
'Placement.platforms is not supported in'
32+
' API version < 1.30'
33+
)
34+
35+
if utils.version_lt(version, '1.27'):
36+
if task_template['Placement'].get('Preferences'):
37+
raise errors.InvalidVersion(
38+
'Placement.preferences is not supported in'
39+
' API version < 1.27'
40+
)
41+
42+
643
class ServiceApiMixin(object):
744
@utils.minimum_version('1.24')
845
def create_service(
@@ -43,6 +80,8 @@ def create_service(
4380
)
4481
endpoint_spec = endpoint_config
4582

83+
_check_api_features(self._version, task_template, update_config)
84+
4685
url = self._url('/services/create')
4786
headers = {}
4887
image = task_template.get('ContainerSpec', {}).get('Image', None)
@@ -67,17 +106,6 @@ def create_service(
67106
}
68107

69108
if update_config is not None:
70-
if utils.version_lt(self._version, '1.25'):
71-
if 'MaxFailureRatio' in update_config:
72-
raise errors.InvalidVersion(
73-
'UpdateConfig.max_failure_ratio is not supported in'
74-
' API version < 1.25'
75-
)
76-
if 'Monitor' in update_config:
77-
raise errors.InvalidVersion(
78-
'UpdateConfig.monitor is not supported in'
79-
' API version < 1.25'
80-
)
81109
data['UpdateConfig'] = update_config
82110

83111
return self._result(
@@ -282,6 +310,8 @@ def update_service(self, service, version, task_template=None, name=None,
282310
)
283311
endpoint_spec = endpoint_config
284312

313+
_check_api_features(self._version, task_template, update_config)
314+
285315
url = self._url('/services/{0}/update', service)
286316
data = {}
287317
headers = {}
@@ -294,12 +324,6 @@ def update_service(self, service, version, task_template=None, name=None,
294324
mode = ServiceMode(mode)
295325
data['Mode'] = mode
296326
if task_template is not None:
297-
if 'ForceUpdate' in task_template and utils.version_lt(
298-
self._version, '1.25'):
299-
raise errors.InvalidVersion(
300-
'force_update is not supported in API version < 1.25'
301-
)
302-
303327
image = task_template.get('ContainerSpec', {}).get('Image', None)
304328
if image is not None:
305329
registry, repo_name = auth.resolve_repository_name(image)
@@ -308,17 +332,6 @@ def update_service(self, service, version, task_template=None, name=None,
308332
headers['X-Registry-Auth'] = auth_header
309333
data['TaskTemplate'] = task_template
310334
if update_config is not None:
311-
if utils.version_lt(self._version, '1.25'):
312-
if 'MaxFailureRatio' in update_config:
313-
raise errors.InvalidVersion(
314-
'UpdateConfig.max_failure_ratio is not supported in'
315-
' API version < 1.25'
316-
)
317-
if 'Monitor' in update_config:
318-
raise errors.InvalidVersion(
319-
'UpdateConfig.monitor is not supported in'
320-
' API version < 1.25'
321-
)
322335
data['UpdateConfig'] = update_config
323336

324337
if networks is not None:

docker/types/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .healthcheck import Healthcheck
44
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
55
from .services import (
6-
ContainerSpec, DriverConfig, EndpointSpec, Mount, Resources, RestartPolicy,
7-
SecretReference, ServiceMode, TaskTemplate, UpdateConfig
6+
ContainerSpec, DriverConfig, EndpointSpec, Mount, Placement, Resources,
7+
RestartPolicy, SecretReference, ServiceMode, TaskTemplate, UpdateConfig
88
)
99
from .swarm import SwarmSpec, SwarmExternalCA

docker/types/services.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ class TaskTemplate(dict):
2020
individual container created as part of the service.
2121
restart_policy (RestartPolicy): Specification for the restart policy
2222
which applies to containers created as part of this service.
23-
placement (:py:class:`list`): A list of constraints.
23+
placement (Placement): Placement instructions for the scheduler.
24+
If a list is passed instead, it is assumed to be a list of
25+
constraints as part of a :py:class:`Placement` object.
2426
force_update (int): A counter that triggers an update even if no
2527
relevant parameters have been changed.
2628
"""
@@ -33,7 +35,7 @@ def __init__(self, container_spec, resources=None, restart_policy=None,
3335
self['RestartPolicy'] = restart_policy
3436
if placement:
3537
if isinstance(placement, list):
36-
placement = {'Constraints': placement}
38+
placement = Placement(constraints=placement)
3739
self['Placement'] = placement
3840
if log_driver:
3941
self['LogDriver'] = log_driver
@@ -452,3 +454,28 @@ def __init__(self, secret_id, secret_name, filename=None, uid=None,
452454
'GID': gid or '0',
453455
'Mode': mode
454456
}
457+
458+
459+
class Placement(dict):
460+
"""
461+
Placement constraints to be used as part of a :py:class:`TaskTemplate`
462+
463+
Args:
464+
constraints (list): A list of constraints
465+
preferences (list): Preferences provide a way to make the
466+
scheduler aware of factors such as topology. They are provided
467+
in order from highest to lowest precedence.
468+
platforms (list): A list of platforms expressed as ``(arch, os)``
469+
tuples
470+
"""
471+
def __init__(self, constraints=None, preferences=None, platforms=None):
472+
if constraints is not None:
473+
self['Constraints'] = constraints
474+
if preferences is not None:
475+
self['Preferences'] = preferences
476+
if platforms:
477+
self['Platforms'] = []
478+
for plat in platforms:
479+
self['Platforms'].append({
480+
'Architecture': plat[0], 'OS': plat[1]
481+
})

tests/integration/api_service_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,49 @@ def test_create_service_with_placement(self):
270270
assert (svc_info['Spec']['TaskTemplate']['Placement'] ==
271271
{'Constraints': ['node.id=={}'.format(node_id)]})
272272

273+
def test_create_service_with_placement_object(self):
274+
node_id = self.client.nodes()[0]['ID']
275+
container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
276+
placemt = docker.types.Placement(
277+
constraints=['node.id=={}'.format(node_id)]
278+
)
279+
task_tmpl = docker.types.TaskTemplate(
280+
container_spec, placement=placemt
281+
)
282+
name = self.get_service_name()
283+
svc_id = self.client.create_service(task_tmpl, name=name)
284+
svc_info = self.client.inspect_service(svc_id)
285+
assert 'Placement' in svc_info['Spec']['TaskTemplate']
286+
assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt
287+
288+
@requires_api_version('1.30')
289+
def test_create_service_with_placement_platform(self):
290+
container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
291+
placemt = docker.types.Placement(platforms=[('x86_64', 'linux')])
292+
task_tmpl = docker.types.TaskTemplate(
293+
container_spec, placement=placemt
294+
)
295+
name = self.get_service_name()
296+
svc_id = self.client.create_service(task_tmpl, name=name)
297+
svc_info = self.client.inspect_service(svc_id)
298+
assert 'Placement' in svc_info['Spec']['TaskTemplate']
299+
assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt
300+
301+
@requires_api_version('1.27')
302+
def test_create_service_with_placement_preferences(self):
303+
container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
304+
placemt = docker.types.Placement(preferences=[
305+
{'Spread': {'SpreadDescriptor': 'com.dockerpy.test'}}
306+
])
307+
task_tmpl = docker.types.TaskTemplate(
308+
container_spec, placement=placemt
309+
)
310+
name = self.get_service_name()
311+
svc_id = self.client.create_service(task_tmpl, name=name)
312+
svc_info = self.client.inspect_service(svc_id)
313+
assert 'Placement' in svc_info['Spec']['TaskTemplate']
314+
assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt
315+
273316
def test_create_service_with_endpoint_spec(self):
274317
container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
275318
task_tmpl = docker.types.TaskTemplate(container_spec)

0 commit comments

Comments
 (0)