Skip to content

Commit 6640243

Browse files
authored
Support global-job and replicated-job modes in Docker Swarm (#3016)
Add `global-job` and `replicated-job` modes Fixes #2829. Signed-off-by: Leonard Kinday <[email protected]>
1 parent 4278981 commit 6640243

File tree

5 files changed

+119
-25
lines changed

5 files changed

+119
-25
lines changed

docker/models/images.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,10 @@ def build(self, **kwargs):
224224
Build an image and return it. Similar to the ``docker build``
225225
command. Either ``path`` or ``fileobj`` must be set.
226226
227-
If you already have a tar file for the Docker build context (including a
228-
Dockerfile), pass a readable file-like object to ``fileobj``
229-
and also pass ``custom_context=True``. If the stream is also compressed,
230-
set ``encoding`` to the correct value (e.g ``gzip``).
227+
If you already have a tar file for the Docker build context (including
228+
a Dockerfile), pass a readable file-like object to ``fileobj``
229+
and also pass ``custom_context=True``. If the stream is also
230+
compressed, set ``encoding`` to the correct value (e.g ``gzip``).
231231
232232
If you want to get the raw output of the build, use the
233233
:py:meth:`~docker.api.build.BuildApiMixin.build` method in the

docker/types/services.py

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class TaskTemplate(dict):
2929
force_update (int): A counter that triggers an update even if no
3030
relevant parameters have been changed.
3131
"""
32+
3233
def __init__(self, container_spec, resources=None, restart_policy=None,
3334
placement=None, log_driver=None, networks=None,
3435
force_update=None):
@@ -115,6 +116,7 @@ class ContainerSpec(dict):
115116
cap_drop (:py:class:`list`): A list of kernel capabilities to drop from
116117
the default set for the container.
117118
"""
119+
118120
def __init__(self, image, command=None, args=None, hostname=None, env=None,
119121
workdir=None, user=None, labels=None, mounts=None,
120122
stop_grace_period=None, secrets=None, tty=None, groups=None,
@@ -231,6 +233,7 @@ class Mount(dict):
231233
tmpfs_size (int or string): The size for the tmpfs mount in bytes.
232234
tmpfs_mode (int): The permission mode for the tmpfs mount.
233235
"""
236+
234237
def __init__(self, target, source, type='volume', read_only=False,
235238
consistency=None, propagation=None, no_copy=False,
236239
labels=None, driver_config=None, tmpfs_size=None,
@@ -331,6 +334,7 @@ class Resources(dict):
331334
``{ resource_name: resource_value }``. Alternatively, a list of
332335
of resource specifications as defined by the Engine API.
333336
"""
337+
334338
def __init__(self, cpu_limit=None, mem_limit=None, cpu_reservation=None,
335339
mem_reservation=None, generic_resources=None):
336340
limits = {}
@@ -401,6 +405,7 @@ class UpdateConfig(dict):
401405
order (string): Specifies the order of operations when rolling out an
402406
updated task. Either ``start-first`` or ``stop-first`` are accepted.
403407
"""
408+
404409
def __init__(self, parallelism=0, delay=None, failure_action='continue',
405410
monitor=None, max_failure_ratio=None, order=None):
406411
self['Parallelism'] = parallelism
@@ -512,6 +517,7 @@ class DriverConfig(dict):
512517
name (string): Name of the driver to use.
513518
options (dict): Driver-specific options. Default: ``None``.
514519
"""
520+
515521
def __init__(self, name, options=None):
516522
self['Name'] = name
517523
if options:
@@ -533,6 +539,7 @@ class EndpointSpec(dict):
533539
is ``(target_port [, protocol [, publish_mode]])``.
534540
Ports can only be provided if the ``vip`` resolution mode is used.
535541
"""
542+
536543
def __init__(self, mode=None, ports=None):
537544
if ports:
538545
self['Ports'] = convert_service_ports(ports)
@@ -575,37 +582,70 @@ def convert_service_ports(ports):
575582

576583
class ServiceMode(dict):
577584
"""
578-
Indicate whether a service should be deployed as a replicated or global
579-
service, and associated parameters
585+
Indicate whether a service or a job should be deployed as a replicated
586+
or global service, and associated parameters
580587
581588
Args:
582-
mode (string): Can be either ``replicated`` or ``global``
589+
mode (string): Can be either ``replicated``, ``global``,
590+
``replicated-job`` or ``global-job``
583591
replicas (int): Number of replicas. For replicated services only.
592+
concurrency (int): Number of concurrent jobs. For replicated job
593+
services only.
584594
"""
585-
def __init__(self, mode, replicas=None):
586-
if mode not in ('replicated', 'global'):
587-
raise errors.InvalidArgument(
588-
'mode must be either "replicated" or "global"'
589-
)
590-
if mode != 'replicated' and replicas is not None:
595+
596+
def __init__(self, mode, replicas=None, concurrency=None):
597+
replicated_modes = ('replicated', 'replicated-job')
598+
supported_modes = replicated_modes + ('global', 'global-job')
599+
600+
if mode not in supported_modes:
591601
raise errors.InvalidArgument(
592-
'replicas can only be used for replicated mode'
602+
'mode must be either "replicated", "global", "replicated-job"'
603+
' or "global-job"'
593604
)
594-
self[mode] = {}
605+
606+
if mode not in replicated_modes:
607+
if replicas is not None:
608+
raise errors.InvalidArgument(
609+
'replicas can only be used for "replicated" or'
610+
' "replicated-job" mode'
611+
)
612+
613+
if concurrency is not None:
614+
raise errors.InvalidArgument(
615+
'concurrency can only be used for "replicated-job" mode'
616+
)
617+
618+
service_mode = self._convert_mode(mode)
619+
self.mode = service_mode
620+
self[service_mode] = {}
621+
595622
if replicas is not None:
596-
self[mode]['Replicas'] = replicas
623+
if mode == 'replicated':
624+
self[service_mode]['Replicas'] = replicas
597625

598-
@property
599-
def mode(self):
600-
if 'global' in self:
601-
return 'global'
602-
return 'replicated'
626+
if mode == 'replicated-job':
627+
self[service_mode]['MaxConcurrent'] = concurrency or 1
628+
self[service_mode]['TotalCompletions'] = replicas
629+
630+
@staticmethod
631+
def _convert_mode(original_mode):
632+
if original_mode == 'global-job':
633+
return 'GlobalJob'
634+
635+
if original_mode == 'replicated-job':
636+
return 'ReplicatedJob'
637+
638+
return original_mode
603639

604640
@property
605641
def replicas(self):
606-
if self.mode != 'replicated':
607-
return None
608-
return self['replicated'].get('Replicas')
642+
if 'replicated' in self:
643+
return self['replicated'].get('Replicas')
644+
645+
if 'ReplicatedJob' in self:
646+
return self['ReplicatedJob'].get('TotalCompletions')
647+
648+
return None
609649

610650

611651
class SecretReference(dict):
@@ -679,6 +719,7 @@ class Placement(dict):
679719
platforms (:py:class:`list` of tuple): A list of platforms
680720
expressed as ``(arch, os)`` tuples
681721
"""
722+
682723
def __init__(self, constraints=None, preferences=None, platforms=None,
683724
maxreplicas=None):
684725
if constraints is not None:
@@ -711,6 +752,7 @@ class PlacementPreference(dict):
711752
the scheduler will try to spread tasks evenly over groups of
712753
nodes identified by this label.
713754
"""
755+
714756
def __init__(self, strategy, descriptor):
715757
if strategy != 'spread':
716758
raise errors.InvalidArgument(
@@ -732,6 +774,7 @@ class DNSConfig(dict):
732774
options (:py:class:`list`): A list of internal resolver variables
733775
to be modified (e.g., ``debug``, ``ndots:3``, etc.).
734776
"""
777+
735778
def __init__(self, nameservers=None, search=None, options=None):
736779
self['Nameservers'] = nameservers
737780
self['Search'] = search
@@ -762,6 +805,7 @@ class Privileges(dict):
762805
selinux_type (string): SELinux type label
763806
selinux_level (string): SELinux level label
764807
"""
808+
765809
def __init__(self, credentialspec_file=None, credentialspec_registry=None,
766810
selinux_disable=None, selinux_user=None, selinux_role=None,
767811
selinux_type=None, selinux_level=None):
@@ -804,6 +848,7 @@ class NetworkAttachmentConfig(dict):
804848
options (:py:class:`dict`): Driver attachment options for the
805849
network target.
806850
"""
851+
807852
def __init__(self, target, aliases=None, options=None):
808853
self['Target'] = target
809854
self['Aliases'] = aliases

tests/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,4 @@ def ctrl_with(char):
143143
if re.match('[a-z]', char):
144144
return chr(ord(char) - ord('a') + 1).encode('ascii')
145145
else:
146-
raise(Exception('char must be [a-z]'))
146+
raise Exception('char must be [a-z]')

tests/integration/api_service_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,39 @@ def test_create_service_replicated_mode(self):
626626
assert 'Replicated' in svc_info['Spec']['Mode']
627627
assert svc_info['Spec']['Mode']['Replicated'] == {'Replicas': 5}
628628

629+
@requires_api_version('1.41')
630+
def test_create_service_global_job_mode(self):
631+
container_spec = docker.types.ContainerSpec(
632+
TEST_IMG, ['echo', 'hello']
633+
)
634+
task_tmpl = docker.types.TaskTemplate(container_spec)
635+
name = self.get_service_name()
636+
svc_id = self.client.create_service(
637+
task_tmpl, name=name, mode='global-job'
638+
)
639+
svc_info = self.client.inspect_service(svc_id)
640+
assert 'Mode' in svc_info['Spec']
641+
assert 'GlobalJob' in svc_info['Spec']['Mode']
642+
643+
@requires_api_version('1.41')
644+
def test_create_service_replicated_job_mode(self):
645+
container_spec = docker.types.ContainerSpec(
646+
TEST_IMG, ['echo', 'hello']
647+
)
648+
task_tmpl = docker.types.TaskTemplate(container_spec)
649+
name = self.get_service_name()
650+
svc_id = self.client.create_service(
651+
task_tmpl, name=name,
652+
mode=docker.types.ServiceMode('replicated-job', 5)
653+
)
654+
svc_info = self.client.inspect_service(svc_id)
655+
assert 'Mode' in svc_info['Spec']
656+
assert 'ReplicatedJob' in svc_info['Spec']['Mode']
657+
assert svc_info['Spec']['Mode']['ReplicatedJob'] == {
658+
'MaxConcurrent': 1,
659+
'TotalCompletions': 5
660+
}
661+
629662
@requires_api_version('1.25')
630663
def test_update_service_force_update(self):
631664
container_spec = docker.types.ContainerSpec(

tests/unit/dockertypes_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,26 @@ def test_global_simple(self):
325325
assert mode.mode == 'global'
326326
assert mode.replicas is None
327327

328+
def test_replicated_job_simple(self):
329+
mode = ServiceMode('replicated-job')
330+
assert mode == {'ReplicatedJob': {}}
331+
assert mode.mode == 'ReplicatedJob'
332+
assert mode.replicas is None
333+
334+
def test_global_job_simple(self):
335+
mode = ServiceMode('global-job')
336+
assert mode == {'GlobalJob': {}}
337+
assert mode.mode == 'GlobalJob'
338+
assert mode.replicas is None
339+
328340
def test_global_replicas_error(self):
329341
with pytest.raises(InvalidArgument):
330342
ServiceMode('global', 21)
331343

344+
def test_global_job_replicas_simple(self):
345+
with pytest.raises(InvalidArgument):
346+
ServiceMode('global-job', 21)
347+
332348
def test_replicated_replicas(self):
333349
mode = ServiceMode('replicated', 21)
334350
assert mode == {'replicated': {'Replicas': 21}}

0 commit comments

Comments
 (0)