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.




Changed files:
tests/integration/models_services_test.py, docker/models/services.py, docker/api/service.py, tests/unit/models_services_test.py, docker/types/services.py, tests/integration/api_service_test.py

diff --git a/docker/api/service.py b/docker/api/service.py
index 4c10ef8e..66d98c78 100644
--- a/docker/api/service.py
+++ b/docker/api/service.py
@@ -62,6 +62,21 @@ def _check_api_features(version, task_template, update_config):
                     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 @@ class ServiceApiMixin(object):
     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 @@ class ServiceApiMixin(object):
                 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 @@ class ServiceApiMixin(object):
 
         _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
index 6fc5c2a5..009e4551 100644
--- a/docker/models/services.py
+++ b/docker/models/services.py
@@ -251,6 +251,7 @@ CONTAINER_SPEC_KWARGS = [
 
 # kwargs to copy straight over to TaskTemplate
 TASK_TEMPLATE_KWARGS = [
+    'networks',
     'resources',
     'restart_policy',
 ]
@@ -261,7 +262,6 @@ CREATE_SERVICE_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
index 9031e609..109b22e5 100644
--- a/docker/types/services.py
+++ b/docker/types/services.py
@@ -4,7 +4,7 @@ from .. import errors
 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 @@ class TaskTemplate(dict):
             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):
diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py
index b9311549..a35d3a57 100644
--- a/tests/integration/api_service_test.py
+++ b/tests/integration/api_service_test.py
@@ -710,3 +710,455 @@ class ServiceTest(BaseAPIIntegrationTest):
             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
index 6b5dab53..ca8be48d 100644
--- 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 @@ class ServiceTest(unittest.TestCase):
         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 @@ class ServiceTest(unittest.TestCase):
         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 @@ class ServiceTest(unittest.TestCase):
         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
index e7e317d5..247bb4a4 100644
--- a/tests/unit/models_services_test.py
+++ b/tests/unit/models_services_test.py
@@ -35,18 +35,18 @@ class CreateServiceKwargsTest(unittest.TestCase):
             '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'