Repository: docker/docker-py

Issue:
Unable to subsequently update Docker Service when using 'networks'.
Since a few weeks it's no longer possible to perform a call to update() when networks are being defined. For instance:

service.update( name = service.name
                         , image = updated_image
                         , networks = networks
                         #  ... omitted other parameters.
                         )

Previously would refresh the service, only modifying the image. Now it throws the following error:
"rpc error: code = 12 desc = networks must be migrated to TaskSpec before being changed"

Please note, that the first update() works, this only occurs after performing an update() again on a service which was previously updated using the update() function.

For us this is critical, as currently rolling deployments (nightly builds) are no longer being deployed automatically.





Discussion:
Docker version: 17.05.0-ce
Docker Python version: 2.3.0
From swarmkit code: https://github.com/docker/swarmkit/blob/master/manager/controlapi/service.go

line 755-763:

               // It's not okay to update Service.Spec.Networks on its own.
		// However, if Service.Spec.Task.Networks is also being
		// updated, that's okay (for example when migrating from the
		// deprecated Spec.Networks field to Spec.Task.Networks).
		if (len(request.Spec.Networks) != 0 || len(service.Spec.Networks) != 0) &&
			!reflect.DeepEqual(request.Spec.Networks, service.Spec.Networks) &&
			reflect.DeepEqual(request.Spec.Task.Networks, service.Spec.Task.Networks) {
			return grpc.Errorf(codes.Unimplemented, errNetworkUpdateNotSupported.Error())
}
Further investigation confirm that the spec defined by docker itself differs from what the Docker Python API does.

Services created/updated by the Docker CLI tools result in having networks under Spec/TaskTemplate/Networks, whereas updating a Service through the Docker Python API still results in defining it as Spec/Networks.


Any update on this one?
The Network configuration was moved to the TaskSpec structure in https://github.com/docker/engine-api/pull/375 , which was Aug 2016. It would probably be a good idea if docker-py also did this, that would mean we conform to the actual current API.
Keep in mind that the actual behavior is not as lenient as the commit message makes you believe.
If you create a Service using the new TaskSpec structure and then update (well, re-create really) the service using docker-py it will just blatantly fail -- see OP.
If you use the new format, then you can change whatever you like. If you use the old format, then yes it break on any changes.

ISTM there is no reason to use the old format except for backward compatibility.
I'm trying to point out that this is what the docker-py API currently breaks.

1. Create a Docker Service using the docker CLI, this will be created in the new format.
2. Then attempt to update this service using docker-py (service.update()) - this will work the first time, but "re-create" the service using the old format.
3. Attempting to update the service again will now fail.

I think `update_service` needs work across the board. Thank you for bringing this particular issue to my attention!

Resolving patch:
diff --git a/docker/api/service.py b/docker/api/service.py
--- a/docker/api/service.py
+++ b/docker/api/service.py
@@ -62,6 +62,21 @@ def raise_version_error(param, min_version):
                     raise_version_error('ContainerSpec.privileges', '1.30')
 
 
+def _merge_task_template(current, override):
+    merged = current.copy()
+    if override is not None:
+        for ts_key, ts_value in override.items():
+            if ts_key == 'ContainerSpec':
+                if 'ContainerSpec' not in merged:
+                    merged['ContainerSpec'] = {}
+                for cs_key, cs_value in override['ContainerSpec'].items():
+                    if cs_value is not None:
+                        merged['ContainerSpec'][cs_key] = cs_value
+            elif ts_value is not None:
+                merged[ts_key] = ts_value
+    return merged
+
+
 class ServiceApiMixin(object):
     @utils.minimum_version('1.24')
     def create_service(
@@ -306,7 +321,7 @@ def tasks(self, filters=None):
     def update_service(self, service, version, task_template=None, name=None,
                        labels=None, mode=None, update_config=None,
                        networks=None, endpoint_config=None,
-                       endpoint_spec=None):
+                       endpoint_spec=None, fetch_current_spec=False):
         """
         Update a service.
 
@@ -328,6 +343,8 @@ def update_service(self, service, version, task_template=None, name=None,
                 the service to. Default: ``None``.
             endpoint_spec (EndpointSpec): Properties that can be configured to
                 access and load balance a service. Default: ``None``.
+            fetch_current_spec (boolean): Use the undefined settings from the
+                current specification of the service. Default: ``False``
 
         Returns:
             ``True`` if successful.
@@ -345,32 +362,64 @@ def update_service(self, service, version, task_template=None, name=None,
 
         _check_api_features(self._version, task_template, update_config)
 
+        if fetch_current_spec:
+            inspect_defaults = True
+            if utils.version_lt(self._version, '1.29'):
+                inspect_defaults = None
+            current = self.inspect_service(
+                service, insert_defaults=inspect_defaults
+            )['Spec']
+
+        else:
+            current = {}
+
         url = self._url('/services/{0}/update', service)
         data = {}
         headers = {}
-        if name is not None:
-            data['Name'] = name
-        if labels is not None:
-            data['Labels'] = labels
+
+        data['Name'] = current.get('Name') if name is None else name
+
+        data['Labels'] = current.get('Labels') if labels is None else 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)
-            if image is not None:
-                registry, repo_name = auth.resolve_repository_name(image)
-                auth_header = auth.get_config_header(self, registry)
-                if auth_header:
-                    headers['X-Registry-Auth'] = auth_header
-            data['TaskTemplate'] = task_template
+        else:
+            data['Mode'] = current.get('Mode')
+
+        data['TaskTemplate'] = _merge_task_template(
+            current.get('TaskTemplate', {}), task_template
+        )
+
+        container_spec = data['TaskTemplate'].get('ContainerSpec', {})
+        image = container_spec.get('Image', None)
+        if image is not None:
+            registry, repo_name = auth.resolve_repository_name(image)
+            auth_header = auth.get_config_header(self, registry)
+            if auth_header:
+                headers['X-Registry-Auth'] = auth_header
+
         if update_config is not None:
             data['UpdateConfig'] = update_config
+        else:
+            data['UpdateConfig'] = current.get('UpdateConfig')
 
         if networks is not None:
-            data['Networks'] = utils.convert_service_networks(networks)
+            converted_networks = utils.convert_service_networks(networks)
+            data['TaskTemplate']['Networks'] = converted_networks
+        elif data['TaskTemplate'].get('Networks') is None:
+            current_task_template = current.get('TaskTemplate', {})
+            current_networks = current_task_template.get('Networks')
+            if current_networks is None:
+                current_networks = current.get('Networks')
+            if current_networks is not None:
+                data['TaskTemplate']['Networks'] = current_networks
+
         if endpoint_spec is not None:
             data['EndpointSpec'] = endpoint_spec
+        else:
+            data['EndpointSpec'] = current.get('EndpointSpec')
 
         resp = self._post_json(
             url, data=data, params={'version': version}, headers=headers
diff --git a/docker/models/services.py b/docker/models/services.py
--- a/docker/models/services.py
+++ b/docker/models/services.py
@@ -251,6 +251,7 @@ def list(self, **kwargs):
 
 # kwargs to copy straight over to TaskTemplate
 TASK_TEMPLATE_KWARGS = [
+    'networks',
     'resources',
     'restart_policy',
 ]
@@ -261,7 +262,6 @@ def list(self, **kwargs):
     'labels',
     'mode',
     'update_config',
-    'networks',
     'endpoint_spec',
 ]
 
@@ -295,6 +295,15 @@ def _get_create_service_kwargs(func_name, kwargs):
             'Options': kwargs.pop('log_driver_options', {})
         }
 
+    if func_name == 'update':
+        if 'force_update' in kwargs:
+            task_template_kwargs['force_update'] = kwargs.pop('force_update')
+
+        # fetch the current spec by default if updating the service
+        # through the model
+        fetch_current_spec = kwargs.pop('fetch_current_spec', True)
+        create_kwargs['fetch_current_spec'] = fetch_current_spec
+
     # All kwargs should have been consumed by this point, so raise
     # error if any are left
     if kwargs:
diff --git a/docker/types/services.py b/docker/types/services.py
--- a/docker/types/services.py
+++ b/docker/types/services.py
@@ -4,7 +4,7 @@
 from ..constants import IS_WINDOWS_PLATFORM
 from ..utils import (
     check_resource, format_environment, format_extra_hosts, parse_bytes,
-    split_command,
+    split_command, convert_service_networks,
 )
 
 
@@ -26,11 +26,14 @@ class TaskTemplate(dict):
         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.
+        networks (:py:class:`list`): List of network names or IDs to attach
+            the containers to.
         force_update (int): A counter that triggers an update even if no
             relevant parameters have been changed.
     """
     def __init__(self, container_spec, resources=None, restart_policy=None,
-                 placement=None, log_driver=None, force_update=None):
+                 placement=None, log_driver=None, networks=None,
+                 force_update=None):
         self['ContainerSpec'] = container_spec
         if resources:
             self['Resources'] = resources
@@ -42,6 +45,8 @@ def __init__(self, container_spec, resources=None, restart_policy=None,
             self['Placement'] = placement
         if log_driver:
             self['LogDriver'] = log_driver
+        if networks:
+            self['Networks'] = convert_service_networks(networks)
 
         if force_update is not None:
             if not isinstance(force_update, int):


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
@@ -710,3 +710,455 @@ def test_create_service_with_privileges(self):
             svc_info['Spec']['TaskTemplate']['ContainerSpec']['Privileges']
         )
         assert privileges['SELinuxContext']['Disable'] is True
+
+    @requires_api_version('1.25')
+    def test_update_service_with_defaults_name(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)
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Name' in svc_info['Spec']
+        assert svc_info['Spec']['Name'] == name
+        version_index = svc_info['Version']['Index']
+
+        task_tmpl = docker.types.TaskTemplate(container_spec, force_update=10)
+        self._update_service(
+            svc_id, name, version_index, task_tmpl, fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        assert 'Name' in svc_info['Spec']
+        assert svc_info['Spec']['Name'] == name
+
+    @requires_api_version('1.25')
+    def test_update_service_with_defaults_labels(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, labels={'service.label': 'SampleLabel'}
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Labels' in svc_info['Spec']
+        assert 'service.label' in svc_info['Spec']['Labels']
+        assert svc_info['Spec']['Labels']['service.label'] == 'SampleLabel'
+        version_index = svc_info['Version']['Index']
+
+        task_tmpl = docker.types.TaskTemplate(container_spec, force_update=10)
+        self._update_service(
+            svc_id, name, version_index, task_tmpl, name=name,
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        assert 'Labels' in svc_info['Spec']
+        assert 'service.label' in svc_info['Spec']['Labels']
+        assert svc_info['Spec']['Labels']['service.label'] == 'SampleLabel'
+
+    def test_update_service_with_defaults_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(mode='replicated', replicas=2)
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Mode' in svc_info['Spec']
+        assert 'Replicated' in svc_info['Spec']['Mode']
+        assert 'Replicas' in svc_info['Spec']['Mode']['Replicated']
+        assert svc_info['Spec']['Mode']['Replicated']['Replicas'] == 2
+        version_index = svc_info['Version']['Index']
+
+        self._update_service(
+            svc_id, name, version_index, labels={'force': 'update'},
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        assert 'Mode' in svc_info['Spec']
+        assert 'Replicated' in svc_info['Spec']['Mode']
+        assert 'Replicas' in svc_info['Spec']['Mode']['Replicated']
+        assert svc_info['Spec']['Mode']['Replicated']['Replicas'] == 2
+
+    def test_update_service_with_defaults_container_labels(self):
+        container_spec = docker.types.ContainerSpec(
+            'busybox', ['echo', 'hello'],
+            labels={'container.label': 'SampleLabel'}
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        name = self.get_service_name()
+        svc_id = self.client.create_service(
+            task_tmpl, name=name, labels={'service.label': 'SampleLabel'}
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'TaskTemplate' in svc_info['Spec']
+        assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+        assert 'Labels' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+        labels = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Labels']
+        assert labels['container.label'] == 'SampleLabel'
+        version_index = svc_info['Version']['Index']
+
+        self._update_service(
+            svc_id, name, version_index, labels={'force': 'update'},
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        assert 'TaskTemplate' in svc_info['Spec']
+        assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+        assert 'Labels' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+        labels = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Labels']
+        assert labels['container.label'] == 'SampleLabel'
+
+        container_spec = docker.types.ContainerSpec(
+            'busybox', ['echo', 'hello']
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        self._update_service(
+            svc_id, name, new_index, task_tmpl, fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        newer_index = svc_info['Version']['Index']
+        assert newer_index > new_index
+        assert 'TaskTemplate' in svc_info['Spec']
+        assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+        assert 'Labels' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+        labels = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Labels']
+        assert labels['container.label'] == 'SampleLabel'
+
+    def test_update_service_with_defaults_update_config(self):
+        container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        update_config = docker.types.UpdateConfig(
+            parallelism=10, delay=5, failure_action='pause'
+        )
+        name = self.get_service_name()
+        svc_id = self.client.create_service(
+            task_tmpl, update_config=update_config, name=name
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'UpdateConfig' in svc_info['Spec']
+        uc = svc_info['Spec']['UpdateConfig']
+        assert update_config['Parallelism'] == uc['Parallelism']
+        assert update_config['Delay'] == uc['Delay']
+        assert update_config['FailureAction'] == uc['FailureAction']
+        version_index = svc_info['Version']['Index']
+
+        self._update_service(
+            svc_id, name, version_index, labels={'force': 'update'},
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        assert 'UpdateConfig' in svc_info['Spec']
+        uc = svc_info['Spec']['UpdateConfig']
+        assert update_config['Parallelism'] == uc['Parallelism']
+        assert update_config['Delay'] == uc['Delay']
+        assert update_config['FailureAction'] == uc['FailureAction']
+
+    def test_update_service_with_defaults_networks(self):
+        net1 = self.client.create_network(
+            'dockerpytest_1', driver='overlay', ipam={'Driver': 'default'}
+        )
+        self.tmp_networks.append(net1['Id'])
+        net2 = self.client.create_network(
+            'dockerpytest_2', driver='overlay', ipam={'Driver': 'default'}
+        )
+        self.tmp_networks.append(net2['Id'])
+        container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        name = self.get_service_name()
+        svc_id = self.client.create_service(
+            task_tmpl, name=name, networks=[
+                'dockerpytest_1', {'Target': 'dockerpytest_2'}
+            ]
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Networks' in svc_info['Spec']
+        assert svc_info['Spec']['Networks'] == [
+            {'Target': net1['Id']}, {'Target': net2['Id']}
+        ]
+
+        version_index = svc_info['Version']['Index']
+
+        self._update_service(
+            svc_id, name, version_index, labels={'force': 'update'},
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        assert 'Networks' in svc_info['Spec']['TaskTemplate']
+        assert svc_info['Spec']['TaskTemplate']['Networks'] == [
+            {'Target': net1['Id']}, {'Target': net2['Id']}
+        ]
+
+        self._update_service(
+            svc_id, name, new_index, networks=[net1['Id']],
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Networks' in svc_info['Spec']['TaskTemplate']
+        assert svc_info['Spec']['TaskTemplate']['Networks'] == [
+            {'Target': net1['Id']}
+        ]
+
+    def test_update_service_with_defaults_endpoint_spec(self):
+        container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        name = self.get_service_name()
+        endpoint_spec = docker.types.EndpointSpec(ports={
+            12357: (1990, 'udp'),
+            12562: (678,),
+            53243: 8080,
+        })
+        svc_id = self.client.create_service(
+            task_tmpl, name=name, endpoint_spec=endpoint_spec
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        print(svc_info)
+        ports = svc_info['Spec']['EndpointSpec']['Ports']
+        for port in ports:
+            if port['PublishedPort'] == 12562:
+                assert port['TargetPort'] == 678
+                assert port['Protocol'] == 'tcp'
+            elif port['PublishedPort'] == 53243:
+                assert port['TargetPort'] == 8080
+                assert port['Protocol'] == 'tcp'
+            elif port['PublishedPort'] == 12357:
+                assert port['TargetPort'] == 1990
+                assert port['Protocol'] == 'udp'
+            else:
+                self.fail('Invalid port specification: {0}'.format(port))
+
+        assert len(ports) == 3
+
+        svc_info = self.client.inspect_service(svc_id)
+        version_index = svc_info['Version']['Index']
+
+        self._update_service(
+            svc_id, name, version_index, labels={'force': 'update'},
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+
+        ports = svc_info['Spec']['EndpointSpec']['Ports']
+        for port in ports:
+            if port['PublishedPort'] == 12562:
+                assert port['TargetPort'] == 678
+                assert port['Protocol'] == 'tcp'
+            elif port['PublishedPort'] == 53243:
+                assert port['TargetPort'] == 8080
+                assert port['Protocol'] == 'tcp'
+            elif port['PublishedPort'] == 12357:
+                assert port['TargetPort'] == 1990
+                assert port['Protocol'] == 'udp'
+            else:
+                self.fail('Invalid port specification: {0}'.format(port))
+
+        assert len(ports) == 3
+
+    @requires_api_version('1.25')
+    def test_update_service_remove_healthcheck(self):
+        second = 1000000000
+        hc = docker.types.Healthcheck(
+            test='true', retries=3, timeout=1 * second,
+            start_period=3 * second, interval=int(second / 2),
+        )
+        container_spec = docker.types.ContainerSpec(
+            BUSYBOX, ['sleep', '999'], healthcheck=hc
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        name = self.get_service_name()
+        svc_id = self.client.create_service(task_tmpl, name=name)
+        svc_info = self.client.inspect_service(svc_id)
+        assert (
+            'Healthcheck' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+        )
+        assert (
+            hc ==
+            svc_info['Spec']['TaskTemplate']['ContainerSpec']['Healthcheck']
+        )
+
+        container_spec = docker.types.ContainerSpec(
+            BUSYBOX, ['sleep', '999'], healthcheck={}
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+
+        version_index = svc_info['Version']['Index']
+
+        self._update_service(
+            svc_id, name, version_index, task_tmpl, fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        container_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec']
+        assert (
+            'Healthcheck' not in container_spec or
+            not container_spec['Healthcheck']
+        )
+
+    def test_update_service_remove_labels(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, labels={'service.label': 'SampleLabel'}
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Labels' in svc_info['Spec']
+        assert 'service.label' in svc_info['Spec']['Labels']
+        assert svc_info['Spec']['Labels']['service.label'] == 'SampleLabel'
+        version_index = svc_info['Version']['Index']
+
+        self._update_service(
+            svc_id, name, version_index, labels={}, fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        assert not svc_info['Spec'].get('Labels')
+
+    def test_update_service_remove_container_labels(self):
+        container_spec = docker.types.ContainerSpec(
+            'busybox', ['echo', 'hello'],
+            labels={'container.label': 'SampleLabel'}
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        name = self.get_service_name()
+        svc_id = self.client.create_service(
+            task_tmpl, name=name, labels={'service.label': 'SampleLabel'}
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'TaskTemplate' in svc_info['Spec']
+        assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+        assert 'Labels' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+        labels = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Labels']
+        assert labels['container.label'] == 'SampleLabel'
+        version_index = svc_info['Version']['Index']
+
+        container_spec = docker.types.ContainerSpec(
+            'busybox', ['echo', 'hello'],
+            labels={}
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        self._update_service(
+            svc_id, name, version_index, task_tmpl, fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+        assert 'TaskTemplate' in svc_info['Spec']
+        assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+        container_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec']
+        assert not container_spec.get('Labels')
+
+    @requires_api_version('1.29')
+    def test_update_service_with_network_change(self):
+        container_spec = docker.types.ContainerSpec(
+            'busybox', ['echo', 'hello']
+        )
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        net1 = self.client.create_network(
+            'dockerpytest_1', driver='overlay', ipam={'Driver': 'default'}
+        )
+        self.tmp_networks.append(net1['Id'])
+        net2 = self.client.create_network(
+            'dockerpytest_2', driver='overlay', ipam={'Driver': 'default'}
+        )
+        self.tmp_networks.append(net2['Id'])
+        name = self.get_service_name()
+        svc_id = self.client.create_service(
+            task_tmpl, name=name, networks=[net1['Id']]
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'Networks' in svc_info['Spec']
+        assert len(svc_info['Spec']['Networks']) > 0
+        assert svc_info['Spec']['Networks'][0]['Target'] == net1['Id']
+
+        svc_info = self.client.inspect_service(svc_id)
+        version_index = svc_info['Version']['Index']
+
+        task_tmpl = docker.types.TaskTemplate(container_spec)
+        self._update_service(
+            svc_id, name, version_index, task_tmpl, name=name,
+            networks=[net2['Id']], fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        task_template = svc_info['Spec']['TaskTemplate']
+        assert 'Networks' in task_template
+        assert len(task_template['Networks']) > 0
+        assert task_template['Networks'][0]['Target'] == net2['Id']
+
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+        assert new_index > version_index
+
+        self._update_service(
+            svc_id, name, new_index, name=name, networks=[net1['Id']],
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        task_template = svc_info['Spec']['TaskTemplate']
+        assert 'ContainerSpec' in task_template
+        new_spec = task_template['ContainerSpec']
+        assert 'Image' in new_spec
+        assert new_spec['Image'].split(':')[0] == 'busybox'
+        assert 'Command' in new_spec
+        assert new_spec['Command'] == ['echo', 'hello']
+        assert 'Networks' in task_template
+        assert len(task_template['Networks']) > 0
+        assert task_template['Networks'][0]['Target'] == net1['Id']
+
+        svc_info = self.client.inspect_service(svc_id)
+        new_index = svc_info['Version']['Index']
+
+        task_tmpl = docker.types.TaskTemplate(
+            container_spec, networks=[net2['Id']]
+        )
+        self._update_service(
+            svc_id, name, new_index, task_tmpl, name=name,
+            fetch_current_spec=True
+        )
+        svc_info = self.client.inspect_service(svc_id)
+        task_template = svc_info['Spec']['TaskTemplate']
+        assert 'Networks' in task_template
+        assert len(task_template['Networks']) > 0
+        assert task_template['Networks'][0]['Target'] == net2['Id']
+
+    def _update_service(self, svc_id, *args, **kwargs):
+        # service update tests seem to be a bit flaky
+        # give them a chance to retry the update with a new version index
+        try:
+            self.client.update_service(*args, **kwargs)
+        except docker.errors.APIError as e:
+            if e.explanation.endswith("update out of sequence"):
+                svc_info = self.client.inspect_service(svc_id)
+                version_index = svc_info['Version']['Index']
+
+                if len(args) > 1:
+                    args = (args[0], version_index) + args[2:]
+                else:
+                    kwargs['version'] = version_index
+
+                self.client.update_service(*args, **kwargs)
+            else:
+                raise
diff --git a/tests/integration/models_services_test.py b/tests/integration/models_services_test.py
--- a/tests/integration/models_services_test.py
+++ b/tests/integration/models_services_test.py
@@ -1,7 +1,6 @@
 import unittest
 
 import docker
-import pytest
 
 from .. import helpers
 from .base import TEST_API_VERSION
@@ -36,6 +35,25 @@ def test_create(self):
         assert "alpine" in container_spec['Image']
         assert container_spec['Labels'] == {'container': 'label'}
 
+    def test_create_with_network(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        name = helpers.random_name()
+        network = client.networks.create(
+            helpers.random_name(), driver='overlay'
+        )
+        service = client.services.create(
+            # create arguments
+            name=name,
+            # ContainerSpec arguments
+            image="alpine",
+            command="sleep 300",
+            networks=[network.id]
+        )
+        assert 'Networks' in service.attrs['Spec']['TaskTemplate']
+        networks = service.attrs['Spec']['TaskTemplate']['Networks']
+        assert len(networks) == 1
+        assert networks[0]['Target'] == network.id
+
     def test_get(self):
         client = docker.from_env(version=TEST_API_VERSION)
         name = helpers.random_name()
@@ -82,7 +100,6 @@ def test_tasks(self):
         assert len(tasks) == 1
         assert tasks[0]['ServiceID'] == service2.id
 
-    @pytest.mark.skip(reason="Makes Swarm unstable?")
     def test_update(self):
         client = docker.from_env(version=TEST_API_VERSION)
         service = client.services.create(
@@ -101,3 +118,109 @@ def test_update(self):
         service.reload()
         container_spec = service.attrs['Spec']['TaskTemplate']['ContainerSpec']
         assert container_spec['Command'] == ["sleep", "600"]
+
+    def test_update_retains_service_labels(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        service = client.services.create(
+            # create arguments
+            name=helpers.random_name(),
+            labels={'service.label': 'SampleLabel'},
+            # ContainerSpec arguments
+            image="alpine",
+            command="sleep 300"
+        )
+        service.update(
+            # create argument
+            name=service.name,
+            # ContainerSpec argument
+            command="sleep 600"
+        )
+        service.reload()
+        labels = service.attrs['Spec']['Labels']
+        assert labels == {'service.label': 'SampleLabel'}
+
+    def test_update_retains_container_labels(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        service = client.services.create(
+            # create arguments
+            name=helpers.random_name(),
+            # ContainerSpec arguments
+            image="alpine",
+            command="sleep 300",
+            container_labels={'container.label': 'SampleLabel'}
+        )
+        service.update(
+            # create argument
+            name=service.name,
+            # ContainerSpec argument
+            command="sleep 600"
+        )
+        service.reload()
+        container_spec = service.attrs['Spec']['TaskTemplate']['ContainerSpec']
+        assert container_spec['Labels'] == {'container.label': 'SampleLabel'}
+
+    def test_update_remove_service_labels(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        service = client.services.create(
+            # create arguments
+            name=helpers.random_name(),
+            labels={'service.label': 'SampleLabel'},
+            # ContainerSpec arguments
+            image="alpine",
+            command="sleep 300"
+        )
+        service.update(
+            # create argument
+            name=service.name,
+            labels={},
+            # ContainerSpec argument
+            command="sleep 600"
+        )
+        service.reload()
+        assert not service.attrs['Spec'].get('Labels')
+
+    def test_scale_service(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        service = client.services.create(
+            # create arguments
+            name=helpers.random_name(),
+            # ContainerSpec arguments
+            image="alpine",
+            command="sleep 300"
+        )
+        tasks = []
+        while len(tasks) == 0:
+            tasks = service.tasks()
+        assert len(tasks) == 1
+        service.update(
+            mode=docker.types.ServiceMode('replicated', replicas=2),
+        )
+        while len(tasks) == 1:
+            tasks = service.tasks()
+        assert len(tasks) >= 2
+        # check that the container spec is not overridden with None
+        service.reload()
+        spec = service.attrs['Spec']['TaskTemplate']['ContainerSpec']
+        assert spec.get('Command') == ['sleep', '300']
+
+    @helpers.requires_api_version('1.25')
+    def test_restart_service(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        service = client.services.create(
+            # create arguments
+            name=helpers.random_name(),
+            # ContainerSpec arguments
+            image="alpine",
+            command="sleep 300"
+        )
+        initial_version = service.version
+        service.update(
+            # create argument
+            name=service.name,
+            # task template argument
+            force_update=10,
+            # ContainerSpec argument
+            command="sleep 600"
+        )
+        service.reload()
+        assert service.version > initial_version
diff --git a/tests/unit/models_services_test.py b/tests/unit/models_services_test.py
--- a/tests/unit/models_services_test.py
+++ b/tests/unit/models_services_test.py
@@ -35,18 +35,18 @@ def test_get_create_service_kwargs(self):
             'labels': {'key': 'value'},
             'mode': 'global',
             'update_config': {'update': 'config'},
-            'networks': ['somenet'],
             'endpoint_spec': {'blah': 'blah'},
         }
         assert set(task_template.keys()) == set([
             'ContainerSpec', 'Resources', 'RestartPolicy', 'Placement',
-            'LogDriver'
+            'LogDriver', 'Networks'
         ])
         assert task_template['Placement'] == {'Constraints': ['foo=bar']}
         assert task_template['LogDriver'] == {
             'Name': 'logdriver',
             'Options': {'foo': 'bar'}
         }
+        assert task_template['Networks'] == [{'Target': 'somenet'}]
         assert set(task_template['ContainerSpec'].keys()) == set([
             'Image', 'Command', 'Args', 'Hostname', 'Env', 'Dir', 'User',
             'Labels', 'Mounts', 'StopGracePeriod'