Skip to content

Commit ac33843

Browse files
committed
Convert mode argument to valid structure in create_service
Signed-off-by: Joffrey F <[email protected]>
1 parent 045bad2 commit ac33843

File tree

6 files changed

+106
-7
lines changed

6 files changed

+106
-7
lines changed

docker/api/service.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import warnings
22
from .. import auth, errors, utils
3+
from ..types import ServiceMode
34

45

56
class ServiceApiMixin(object):
@@ -18,8 +19,8 @@ def create_service(
1819
name (string): User-defined name for the service. Optional.
1920
labels (dict): A map of labels to associate with the service.
2021
Optional.
21-
mode (string): Scheduling mode for the service (``replicated`` or
22-
``global``). Defaults to ``replicated``.
22+
mode (ServiceMode): Scheduling mode for the service (replicated
23+
or global). Defaults to replicated.
2324
update_config (UpdateConfig): Specification for the update strategy
2425
of the service. Default: ``None``
2526
networks (:py:class:`list`): List of network names or IDs to attach
@@ -49,6 +50,9 @@ def create_service(
4950
raise errors.DockerException(
5051
'Missing mandatory Image key in ContainerSpec'
5152
)
53+
if mode and not isinstance(mode, dict):
54+
mode = ServiceMode(mode)
55+
5256
registry, repo_name = auth.resolve_repository_name(image)
5357
auth_header = auth.get_config_header(self, registry)
5458
if auth_header:
@@ -191,8 +195,8 @@ def update_service(self, service, version, task_template=None, name=None,
191195
name (string): New name for the service. Optional.
192196
labels (dict): A map of labels to associate with the service.
193197
Optional.
194-
mode (string): Scheduling mode for the service (``replicated`` or
195-
``global``). Defaults to ``replicated``.
198+
mode (ServiceMode): Scheduling mode for the service (replicated
199+
or global). Defaults to replicated.
196200
update_config (UpdateConfig): Specification for the update strategy
197201
of the service. Default: ``None``.
198202
networks (:py:class:`list`): List of network names or IDs to attach
@@ -222,6 +226,8 @@ def update_service(self, service, version, task_template=None, name=None,
222226
if labels is not None:
223227
data['Labels'] = labels
224228
if mode is not None:
229+
if not isinstance(mode, dict):
230+
mode = ServiceMode(mode)
225231
data['Mode'] = mode
226232
if task_template is not None:
227233
image = task_template.get('ContainerSpec', {}).get('Image', None)

docker/types/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
55
from .services import (
66
ContainerSpec, DriverConfig, EndpointSpec, Mount, Resources, RestartPolicy,
7-
TaskTemplate, UpdateConfig
7+
ServiceMode, TaskTemplate, UpdateConfig
88
)
99
from .swarm import SwarmSpec, SwarmExternalCA

docker/types/services.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,38 @@ def convert_service_ports(ports):
348348

349349
result.append(port_spec)
350350
return result
351+
352+
353+
class ServiceMode(dict):
354+
"""
355+
Indicate whether a service should be deployed as a replicated or global
356+
service, and associated parameters
357+
358+
Args:
359+
mode (string): Can be either ``replicated`` or ``global``
360+
replicas (int): Number of replicas. For replicated services only.
361+
"""
362+
def __init__(self, mode, replicas=None):
363+
if mode not in ('replicated', 'global'):
364+
raise errors.InvalidArgument(
365+
'mode must be either "replicated" or "global"'
366+
)
367+
if mode != 'replicated' and replicas is not None:
368+
raise errors.InvalidArgument(
369+
'replicas can only be used for replicated mode'
370+
)
371+
self[mode] = {}
372+
if replicas:
373+
self[mode]['Replicas'] = replicas
374+
375+
@property
376+
def mode(self):
377+
if 'global' in self:
378+
return 'global'
379+
return 'replicated'
380+
381+
@property
382+
def replicas(self):
383+
if self.mode != 'replicated':
384+
return None
385+
return self['replicated'].get('Replicas')

docs/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,6 @@ Configuration types
110110
.. autoclass:: Mount
111111
.. autoclass:: Resources
112112
.. autoclass:: RestartPolicy
113+
.. autoclass:: ServiceMode
113114
.. autoclass:: TaskTemplate
114115
.. autoclass:: UpdateConfig

tests/integration/api_service_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,31 @@ def test_create_service_with_env(self):
251251
con_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec']
252252
assert 'Env' in con_spec
253253
assert con_spec['Env'] == ['DOCKER_PY_TEST=1']
254+
255+
def test_create_service_global_mode(self):
256+
container_spec = docker.types.ContainerSpec(
257+
'busybox', ['echo', 'hello']
258+
)
259+
task_tmpl = docker.types.TaskTemplate(container_spec)
260+
name = self.get_service_name()
261+
svc_id = self.client.create_service(
262+
task_tmpl, name=name, mode='global'
263+
)
264+
svc_info = self.client.inspect_service(svc_id)
265+
assert 'Mode' in svc_info['Spec']
266+
assert 'Global' in svc_info['Spec']['Mode']
267+
268+
def test_create_service_replicated_mode(self):
269+
container_spec = docker.types.ContainerSpec(
270+
'busybox', ['echo', 'hello']
271+
)
272+
task_tmpl = docker.types.TaskTemplate(container_spec)
273+
name = self.get_service_name()
274+
svc_id = self.client.create_service(
275+
task_tmpl, name=name,
276+
mode=docker.types.ServiceMode('replicated', 5)
277+
)
278+
svc_info = self.client.inspect_service(svc_id)
279+
assert 'Mode' in svc_info['Spec']
280+
assert 'Replicated' in svc_info['Spec']['Mode']
281+
assert svc_info['Spec']['Mode']['Replicated'] == {'Replicas': 5}

tests/unit/dockertypes_test.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from docker.constants import DEFAULT_DOCKER_API_VERSION
88
from docker.errors import InvalidArgument, InvalidVersion
99
from docker.types import (
10-
EndpointConfig, HostConfig, IPAMConfig, IPAMPool, LogConfig, Mount, Ulimit,
10+
EndpointConfig, HostConfig, IPAMConfig, IPAMPool, LogConfig, Mount,
11+
ServiceMode, Ulimit,
1112
)
1213

1314
try:
@@ -260,7 +261,35 @@ def test_create_ipam_config(self):
260261
})
261262

262263

263-
class TestMounts(unittest.TestCase):
264+
class ServiceModeTest(unittest.TestCase):
265+
def test_replicated_simple(self):
266+
mode = ServiceMode('replicated')
267+
assert mode == {'replicated': {}}
268+
assert mode.mode == 'replicated'
269+
assert mode.replicas is None
270+
271+
def test_global_simple(self):
272+
mode = ServiceMode('global')
273+
assert mode == {'global': {}}
274+
assert mode.mode == 'global'
275+
assert mode.replicas is None
276+
277+
def test_global_replicas_error(self):
278+
with pytest.raises(InvalidArgument):
279+
ServiceMode('global', 21)
280+
281+
def test_replicated_replicas(self):
282+
mode = ServiceMode('replicated', 21)
283+
assert mode == {'replicated': {'Replicas': 21}}
284+
assert mode.mode == 'replicated'
285+
assert mode.replicas == 21
286+
287+
def test_invalid_mode(self):
288+
with pytest.raises(InvalidArgument):
289+
ServiceMode('foobar')
290+
291+
292+
class MountTest(unittest.TestCase):
264293
def test_parse_mount_string_ro(self):
265294
mount = Mount.parse_mount_string("/foo/bar:/baz:ro")
266295
assert mount['Source'] == "/foo/bar"

0 commit comments

Comments
 (0)