Skip to content

Commit ef76c85

Browse files
authored
Merge pull request docker#1239 from docker/1212-fix_create_service
Fix several create_service arguments
2 parents 65d900e + 3ac73a2 commit ef76c85

File tree

7 files changed

+157
-11
lines changed

7 files changed

+157
-11
lines changed

docker/api/service.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
from .. import errors
24
from .. import utils
35
from ..auth import auth
@@ -7,8 +9,16 @@ class ServiceApiMixin(object):
79
@utils.minimum_version('1.24')
810
def create_service(
911
self, task_template, name=None, labels=None, mode=None,
10-
update_config=None, networks=None, endpoint_config=None
12+
update_config=None, networks=None, endpoint_config=None,
13+
endpoint_spec=None
1114
):
15+
if endpoint_config is not None:
16+
warnings.warn(
17+
'endpoint_config has been renamed to endpoint_spec.',
18+
DeprecationWarning
19+
)
20+
endpoint_spec = endpoint_config
21+
1222
url = self._url('/services/create')
1323
headers = {}
1424
image = task_template.get('ContainerSpec', {}).get('Image', None)
@@ -26,8 +36,8 @@ def create_service(
2636
'TaskTemplate': task_template,
2737
'Mode': mode,
2838
'UpdateConfig': update_config,
29-
'Networks': networks,
30-
'Endpoint': endpoint_config
39+
'Networks': utils.convert_service_networks(networks),
40+
'EndpointSpec': endpoint_spec
3141
}
3242
return self._result(
3343
self._post_json(url, data=data, headers=headers), True
@@ -73,7 +83,16 @@ def tasks(self, filters=None):
7383
@utils.check_resource
7484
def update_service(self, service, version, task_template=None, name=None,
7585
labels=None, mode=None, update_config=None,
76-
networks=None, endpoint_config=None):
86+
networks=None, endpoint_config=None,
87+
endpoint_spec=None):
88+
89+
if endpoint_config is not None:
90+
warnings.warn(
91+
'endpoint_config has been renamed to endpoint_spec.',
92+
DeprecationWarning
93+
)
94+
endpoint_spec = endpoint_config
95+
7796
url = self._url('/services/{0}/update', service)
7897
data = {}
7998
headers = {}
@@ -94,9 +113,9 @@ def update_service(self, service, version, task_template=None, name=None,
94113
if update_config is not None:
95114
data['UpdateConfig'] = update_config
96115
if networks is not None:
97-
data['Networks'] = networks
98-
if endpoint_config is not None:
99-
data['Endpoint'] = endpoint_config
116+
data['Networks'] = utils.convert_service_networks(networks)
117+
if endpoint_spec is not None:
118+
data['EndpointSpec'] = endpoint_spec
100119

101120
resp = self._post_json(
102121
url, data=data, params={'version': version}, headers=headers

docker/types/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# flake8: noqa
22
from .containers import LogConfig, Ulimit
33
from .services import (
4-
ContainerSpec, DriverConfig, Mount, Resources, RestartPolicy, TaskTemplate,
5-
UpdateConfig
4+
ContainerSpec, DriverConfig, EndpointSpec, Mount, Resources, RestartPolicy,
5+
TaskTemplate, UpdateConfig
66
)
77
from .swarm import SwarmSpec, SwarmExternalCA

docker/types/services.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def __init__(self, container_spec, resources=None, restart_policy=None,
1212
if restart_policy:
1313
self['RestartPolicy'] = restart_policy
1414
if placement:
15+
if isinstance(placement, list):
16+
placement = {'Constraints': placement}
1517
self['Placement'] = placement
1618
if log_driver:
1719
self['LogDriver'] = log_driver
@@ -179,3 +181,37 @@ def __init__(self, name, options=None):
179181
self['Name'] = name
180182
if options:
181183
self['Options'] = options
184+
185+
186+
class EndpointSpec(dict):
187+
def __init__(self, mode=None, ports=None):
188+
if ports:
189+
self['Ports'] = convert_service_ports(ports)
190+
if mode:
191+
self['Mode'] = mode
192+
193+
194+
def convert_service_ports(ports):
195+
if isinstance(ports, list):
196+
return ports
197+
if not isinstance(ports, dict):
198+
raise TypeError(
199+
'Invalid type for ports, expected dict or list'
200+
)
201+
202+
result = []
203+
for k, v in six.iteritems(ports):
204+
port_spec = {
205+
'Protocol': 'tcp',
206+
'PublishedPort': k
207+
}
208+
209+
if isinstance(v, tuple):
210+
port_spec['TargetPort'] = v[0]
211+
if len(v) == 2:
212+
port_spec['Protocol'] = v[1]
213+
else:
214+
port_spec['TargetPort'] = v
215+
216+
result.append(port_spec)
217+
return result

docker/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
create_host_config, create_container_config, parse_bytes, ping_registry,
77
parse_env_file, version_lt, version_gte, decode_json_header, split_command,
88
create_ipam_config, create_ipam_pool, parse_devices, normalize_links,
9+
convert_service_networks,
910
)
1011

1112
from ..types import LogConfig, Ulimit

docker/utils/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,20 @@ def convert_tmpfs_mounts(tmpfs):
376376
return result
377377

378378

379+
def convert_service_networks(networks):
380+
if not networks:
381+
return networks
382+
if not isinstance(networks, list):
383+
raise TypeError('networks parameter must be a list.')
384+
385+
result = []
386+
for n in networks:
387+
if isinstance(n, six.string_types):
388+
n = {'Target': n}
389+
result.append(n)
390+
return result
391+
392+
379393
def parse_repository_tag(repo_name):
380394
parts = repo_name.rsplit('@', 1)
381395
if len(parts) == 2:

docs/services.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Create a service.
8282
See the [UpdateConfig class](#UpdateConfig) for details. Default: `None`.
8383
* networks (list): List of network names or IDs to attach the service to.
8484
Default: `None`.
85-
* endpoint_config (dict): Properties that can be configured to access and load
85+
* endpoint_spec (dict): Properties that can be configured to access and load
8686
balance a service. Default: `None`.
8787

8888
**Returns:** A dictionary containing an `ID` key for the newly created service.
@@ -137,7 +137,7 @@ Update a service.
137137
See the [UpdateConfig class](#UpdateConfig) for details. Default: `None`.
138138
* networks (list): List of network names or IDs to attach the service to.
139139
Default: `None`.
140-
* endpoint_config (dict): Properties that can be configured to access and load
140+
* endpoint_spec (dict): Properties that can be configured to access and load
141141
balance a service. Default: `None`.
142142

143143
**Returns:** `True` if successful. Raises an `APIError` otherwise.
@@ -174,6 +174,20 @@ and for the `driver_config` in a volume `Mount`.
174174
* name (string): Name of the logging driver to use.
175175
* options (dict): Driver-specific options. Default: `None`.
176176

177+
#### EndpointSpec
178+
179+
An `EndpointSpec` object describes properties to access and load-balance a
180+
service.
181+
182+
**Params:**
183+
184+
* mode (string): The mode of resolution to use for internal load balancing
185+
between tasks (`'vip'` or `'dnsrr'`). Defaults to `'vip'` if not provided.
186+
* ports (dict): Exposed ports that this service is accessible on from the
187+
outside, in the form of `{ target_port: published_port }` or
188+
`{ target_port: (published_port, protocol) }`. Ports can only be provided if
189+
the `vip` resolution mode is used.
190+
177191
#### Mount
178192

179193
A `Mount` object describes a mounted folder's configuration inside a

tests/integration/service_test.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,65 @@ def test_create_service_with_restart_policy(self):
169169
svc_info = self.client.inspect_service(svc_id)
170170
assert 'RestartPolicy' in svc_info['Spec']['TaskTemplate']
171171
assert policy == svc_info['Spec']['TaskTemplate']['RestartPolicy']
172+
173+
def test_create_service_with_custom_networks(self):
174+
net1 = self.client.create_network(
175+
'dockerpytest_1', driver='overlay', ipam={'Driver': 'default'}
176+
)
177+
self.tmp_networks.append(net1['Id'])
178+
net2 = self.client.create_network(
179+
'dockerpytest_2', driver='overlay', ipam={'Driver': 'default'}
180+
)
181+
self.tmp_networks.append(net2['Id'])
182+
container_spec = docker.types.ContainerSpec('busybox', ['true'])
183+
task_tmpl = docker.types.TaskTemplate(container_spec)
184+
name = self.get_service_name()
185+
svc_id = self.client.create_service(
186+
task_tmpl, name=name, networks=[
187+
'dockerpytest_1', {'Target': 'dockerpytest_2'}
188+
]
189+
)
190+
svc_info = self.client.inspect_service(svc_id)
191+
assert 'Networks' in svc_info['Spec']
192+
assert svc_info['Spec']['Networks'] == [
193+
{'Target': net1['Id']}, {'Target': net2['Id']}
194+
]
195+
196+
def test_create_service_with_placement(self):
197+
node_id = self.client.nodes()[0]['ID']
198+
container_spec = docker.types.ContainerSpec('busybox', ['true'])
199+
task_tmpl = docker.types.TaskTemplate(
200+
container_spec, placement=['node.id=={}'.format(node_id)]
201+
)
202+
name = self.get_service_name()
203+
svc_id = self.client.create_service(task_tmpl, name=name)
204+
svc_info = self.client.inspect_service(svc_id)
205+
assert 'Placement' in svc_info['Spec']['TaskTemplate']
206+
assert (svc_info['Spec']['TaskTemplate']['Placement'] ==
207+
{'Constraints': ['node.id=={}'.format(node_id)]})
208+
209+
def test_create_service_with_endpoint_spec(self):
210+
container_spec = docker.types.ContainerSpec('busybox', ['true'])
211+
task_tmpl = docker.types.TaskTemplate(container_spec)
212+
name = self.get_service_name()
213+
endpoint_spec = docker.types.EndpointSpec(ports={
214+
12357: (1990, 'udp'),
215+
12562: (678,),
216+
53243: 8080,
217+
})
218+
svc_id = self.client.create_service(
219+
task_tmpl, name=name, endpoint_spec=endpoint_spec
220+
)
221+
svc_info = self.client.inspect_service(svc_id)
222+
print(svc_info)
223+
ports = svc_info['Spec']['EndpointSpec']['Ports']
224+
assert {
225+
'PublishedPort': 12562, 'TargetPort': 678, 'Protocol': 'tcp'
226+
} in ports
227+
assert {
228+
'PublishedPort': 53243, 'TargetPort': 8080, 'Protocol': 'tcp'
229+
} in ports
230+
assert {
231+
'PublishedPort': 12357, 'TargetPort': 1990, 'Protocol': 'udp'
232+
} in ports
233+
assert len(ports) == 3

0 commit comments

Comments
 (0)