Repository: docker/docker-py

Issue:
Unable to create Service in `global` mode
Attempting to create a service with `mode="global"` returns an error from the Docker API:

```
cannot unmarshal string into Go value of type swarm.ServiceMode
```


Discussion:
From the Docker API code, the workaround is to pass a dict:

```
mode={"global": {}}
```
Thanks for the report!

Resolving patch:
diff --git a/docker/api/service.py b/docker/api/service.py
--- a/docker/api/service.py
+++ b/docker/api/service.py
@@ -1,5 +1,6 @@
 import warnings
 from .. import auth, errors, utils
+from ..types import ServiceMode
 
 
 class ServiceApiMixin(object):
@@ -18,8 +19,8 @@ def create_service(
             name (string): User-defined name for the service. Optional.
             labels (dict): A map of labels to associate with the service.
                 Optional.
-            mode (string): Scheduling mode for the service (``replicated`` or
-                ``global``). Defaults to ``replicated``.
+            mode (ServiceMode): Scheduling mode for the service (replicated
+                or global). Defaults to replicated.
             update_config (UpdateConfig): Specification for the update strategy
                 of the service. Default: ``None``
             networks (:py:class:`list`): List of network names or IDs to attach
@@ -49,6 +50,9 @@ def create_service(
             raise errors.DockerException(
                 'Missing mandatory Image key in ContainerSpec'
             )
+        if mode and not isinstance(mode, dict):
+            mode = ServiceMode(mode)
+
         registry, repo_name = auth.resolve_repository_name(image)
         auth_header = auth.get_config_header(self, registry)
         if auth_header:
@@ -191,8 +195,8 @@ def update_service(self, service, version, task_template=None, name=None,
             name (string): New name for the service. Optional.
             labels (dict): A map of labels to associate with the service.
                 Optional.
-            mode (string): Scheduling mode for the service (``replicated`` or
-                ``global``). Defaults to ``replicated``.
+            mode (ServiceMode): Scheduling mode for the service (replicated
+                or global). Defaults to replicated.
             update_config (UpdateConfig): Specification for the update strategy
                 of the service. Default: ``None``.
             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,
         if labels is not None:
             data['Labels'] = labels
         if mode is not None:
+            if not isinstance(mode, dict):
+                mode = ServiceMode(mode)
             data['Mode'] = mode
         if task_template is not None:
             image = task_template.get('ContainerSpec', {}).get('Image', None)
diff --git a/docker/types/__init__.py b/docker/types/__init__.py
--- a/docker/types/__init__.py
+++ b/docker/types/__init__.py
@@ -4,6 +4,6 @@
 from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
 from .services import (
     ContainerSpec, DriverConfig, EndpointSpec, Mount, Resources, RestartPolicy,
-    TaskTemplate, UpdateConfig
+    ServiceMode, TaskTemplate, UpdateConfig
 )
 from .swarm import SwarmSpec, SwarmExternalCA
diff --git a/docker/types/services.py b/docker/types/services.py
--- a/docker/types/services.py
+++ b/docker/types/services.py
@@ -348,3 +348,38 @@ def convert_service_ports(ports):
 
         result.append(port_spec)
     return result
+
+
+class ServiceMode(dict):
+    """
+        Indicate whether a service should be deployed as a replicated or global
+        service, and associated parameters
+
+        Args:
+            mode (string): Can be either ``replicated`` or ``global``
+            replicas (int): Number of replicas. For replicated services only.
+    """
+    def __init__(self, mode, replicas=None):
+        if mode not in ('replicated', 'global'):
+            raise errors.InvalidArgument(
+                'mode must be either "replicated" or "global"'
+            )
+        if mode != 'replicated' and replicas is not None:
+            raise errors.InvalidArgument(
+                'replicas can only be used for replicated mode'
+            )
+        self[mode] = {}
+        if replicas:
+            self[mode]['Replicas'] = replicas
+
+    @property
+    def mode(self):
+        if 'global' in self:
+            return 'global'
+        return 'replicated'
+
+    @property
+    def replicas(self):
+        if self.mode != 'replicated':
+            return None
+        return self['replicated'].get('Replicas')


Test patch:
diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py
--- a/tests/integration/api_service_test.py
+++ b/tests/integration/api_service_test.py
@@ -251,3 +251,31 @@ def test_create_service_with_env(self):
         con_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec']
         assert 'Env' in con_spec
         assert con_spec['Env'] == ['DOCKER_PY_TEST=1']
+
+    def test_create_service_global_mode(self):
+        container_spec = docker.types.ContainerSpec(
+            'busybox', ['echo', 'hello']
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        name = self.get_service_name()
+        svc_id = self.client.create_service(
+            task_tmpl, name=name, mode='global'
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Mode' in svc_info['Spec']
+        assert 'Global' in svc_info['Spec']['Mode']
+
+    def test_create_service_replicated_mode(self):
+        container_spec = docker.types.ContainerSpec(
+            'busybox', ['echo', 'hello']
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        name = self.get_service_name()
+        svc_id = self.client.create_service(
+            task_tmpl, name=name,
+            mode=docker.types.ServiceMode('replicated', 5)
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Mode' in svc_info['Spec']
+        assert 'Replicated' in svc_info['Spec']['Mode']
+        assert svc_info['Spec']['Mode']['Replicated'] == {'Replicas': 5}
diff --git a/tests/unit/dockertypes_test.py b/tests/unit/dockertypes_test.py
--- a/tests/unit/dockertypes_test.py
+++ b/tests/unit/dockertypes_test.py
@@ -7,7 +7,8 @@
 from docker.constants import DEFAULT_DOCKER_API_VERSION
 from docker.errors import InvalidArgument, InvalidVersion
 from docker.types import (
-    EndpointConfig, HostConfig, IPAMConfig, IPAMPool, LogConfig, Mount, Ulimit,
+    EndpointConfig, HostConfig, IPAMConfig, IPAMPool, LogConfig, Mount,
+    ServiceMode, Ulimit,
 )
 
 try:
@@ -260,7 +261,35 @@ def test_create_ipam_config(self):
         })
 
 
-class TestMounts(unittest.TestCase):
+class ServiceModeTest(unittest.TestCase):
+    def test_replicated_simple(self):
+        mode = ServiceMode('replicated')
+        assert mode == {'replicated': {}}
+        assert mode.mode == 'replicated'
+        assert mode.replicas is None
+
+    def test_global_simple(self):
+        mode = ServiceMode('global')
+        assert mode == {'global': {}}
+        assert mode.mode == 'global'
+        assert mode.replicas is None
+
+    def test_global_replicas_error(self):
+        with pytest.raises(InvalidArgument):
+            ServiceMode('global', 21)
+
+    def test_replicated_replicas(self):
+        mode = ServiceMode('replicated', 21)
+        assert mode == {'replicated': {'Replicas': 21}}
+        assert mode.mode == 'replicated'
+        assert mode.replicas == 21
+
+    def test_invalid_mode(self):
+        with pytest.raises(InvalidArgument):
+            ServiceMode('foobar')
+
+
+class MountTest(unittest.TestCase):
     def test_parse_mount_string_ro(self):
         mount = Mount.parse_mount_string("/foo/bar:/baz:ro")
         assert mount['Source'] == "/foo/bar"