Skip to content

Commit 5347c16

Browse files
committed
Add support for publish mode for endpointspec ports
Signed-off-by: Joffrey F <[email protected]>
1 parent 0750337 commit 5347c16

File tree

4 files changed

+118
-7
lines changed

4 files changed

+118
-7
lines changed

docker/api/service.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from ..types import ServiceMode
44

55

6-
def _check_api_features(version, task_template, update_config):
6+
def _check_api_features(version, task_template, update_config, endpoint_spec):
77

88
def raise_version_error(param, min_version):
99
raise errors.InvalidVersion(
@@ -23,6 +23,11 @@ def raise_version_error(param, min_version):
2323
if 'Order' in update_config:
2424
raise_version_error('UpdateConfig.order', '1.29')
2525

26+
if endpoint_spec is not None:
27+
if utils.version_lt(version, '1.32') and 'Ports' in endpoint_spec:
28+
if any(p.get('PublishMode') for p in endpoint_spec['Ports']):
29+
raise_version_error('EndpointSpec.Ports[].mode', '1.32')
30+
2631
if task_template is not None:
2732
if 'ForceUpdate' in task_template and utils.version_lt(
2833
version, '1.25'):
@@ -125,7 +130,9 @@ def create_service(
125130
)
126131
endpoint_spec = endpoint_config
127132

128-
_check_api_features(self._version, task_template, update_config)
133+
_check_api_features(
134+
self._version, task_template, update_config, endpoint_spec
135+
)
129136

130137
url = self._url('/services/create')
131138
headers = {}
@@ -370,7 +377,9 @@ def update_service(self, service, version, task_template=None, name=None,
370377
)
371378
endpoint_spec = endpoint_config
372379

373-
_check_api_features(self._version, task_template, update_config)
380+
_check_api_features(
381+
self._version, task_template, update_config, endpoint_spec
382+
)
374383

375384
if fetch_current_spec:
376385
inspect_defaults = True

docker/types/services.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,9 @@ class EndpointSpec(dict):
450450
``'vip'`` if not provided.
451451
ports (dict): Exposed ports that this service is accessible on from the
452452
outside, in the form of ``{ published_port: target_port }`` or
453-
``{ published_port: (target_port, protocol) }``. Ports can only be
454-
provided if the ``vip`` resolution mode is used.
453+
``{ published_port: <port_config_tuple> }``. Port config tuple format
454+
is ``(target_port [, protocol [, publish_mode]])``.
455+
Ports can only be provided if the ``vip`` resolution mode is used.
455456
"""
456457
def __init__(self, mode=None, ports=None):
457458
if ports:
@@ -477,8 +478,15 @@ def convert_service_ports(ports):
477478

478479
if isinstance(v, tuple):
479480
port_spec['TargetPort'] = v[0]
480-
if len(v) == 2:
481+
if len(v) >= 2 and v[1] is not None:
481482
port_spec['Protocol'] = v[1]
483+
if len(v) == 3:
484+
port_spec['PublishMode'] = v[2]
485+
if len(v) > 3:
486+
raise ValueError(
487+
'Service port configuration can have at most 3 elements: '
488+
'(target_port, protocol, mode)'
489+
)
482490
else:
483491
port_spec['TargetPort'] = v
484492

tests/integration/api_service_test.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,6 @@ def test_create_service_with_endpoint_spec(self):
353353
task_tmpl, name=name, endpoint_spec=endpoint_spec
354354
)
355355
svc_info = self.client.inspect_service(svc_id)
356-
print(svc_info)
357356
ports = svc_info['Spec']['EndpointSpec']['Ports']
358357
for port in ports:
359358
if port['PublishedPort'] == 12562:
@@ -370,6 +369,26 @@ def test_create_service_with_endpoint_spec(self):
370369

371370
assert len(ports) == 3
372371

372+
@requires_api_version('1.32')
373+
def test_create_service_with_endpoint_spec_host_publish_mode(self):
374+
container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
375+
task_tmpl = docker.types.TaskTemplate(container_spec)
376+
name = self.get_service_name()
377+
endpoint_spec = docker.types.EndpointSpec(ports={
378+
12357: (1990, None, 'host'),
379+
})
380+
svc_id = self.client.create_service(
381+
task_tmpl, name=name, endpoint_spec=endpoint_spec
382+
)
383+
svc_info = self.client.inspect_service(svc_id)
384+
ports = svc_info['Spec']['EndpointSpec']['Ports']
385+
assert len(ports) == 1
386+
port = ports[0]
387+
assert port['PublishedPort'] == 12357
388+
assert port['TargetPort'] == 1990
389+
assert port['Protocol'] == 'tcp'
390+
assert port['PublishMode'] == 'host'
391+
373392
def test_create_service_with_env(self):
374393
container_spec = docker.types.ContainerSpec(
375394
BUSYBOX, ['true'], env={'DOCKER_PY_TEST': 1}

tests/unit/dockertypes_test.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ContainerConfig, ContainerSpec, EndpointConfig, HostConfig, IPAMConfig,
1212
IPAMPool, LogConfig, Mount, ServiceMode, Ulimit,
1313
)
14+
from docker.types.services import convert_service_ports
1415

1516
try:
1617
from unittest import mock
@@ -423,3 +424,77 @@ def test_parse_mount_bind_windows(self):
423424
assert mount['Source'] == "C:/foo/bar"
424425
assert mount['Target'] == "/baz"
425426
assert mount['Type'] == 'bind'
427+
428+
429+
class ServicePortsTest(unittest.TestCase):
430+
def test_convert_service_ports_simple(self):
431+
ports = {8080: 80}
432+
assert convert_service_ports(ports) == [{
433+
'Protocol': 'tcp',
434+
'PublishedPort': 8080,
435+
'TargetPort': 80,
436+
}]
437+
438+
def test_convert_service_ports_with_protocol(self):
439+
ports = {8080: (80, 'udp')}
440+
441+
assert convert_service_ports(ports) == [{
442+
'Protocol': 'udp',
443+
'PublishedPort': 8080,
444+
'TargetPort': 80,
445+
}]
446+
447+
def test_convert_service_ports_with_protocol_and_mode(self):
448+
ports = {8080: (80, 'udp', 'ingress')}
449+
450+
assert convert_service_ports(ports) == [{
451+
'Protocol': 'udp',
452+
'PublishedPort': 8080,
453+
'TargetPort': 80,
454+
'PublishMode': 'ingress',
455+
}]
456+
457+
def test_convert_service_ports_invalid(self):
458+
ports = {8080: ('way', 'too', 'many', 'items', 'here')}
459+
460+
with pytest.raises(ValueError):
461+
convert_service_ports(ports)
462+
463+
def test_convert_service_ports_no_protocol_and_mode(self):
464+
ports = {8080: (80, None, 'host')}
465+
466+
assert convert_service_ports(ports) == [{
467+
'Protocol': 'tcp',
468+
'PublishedPort': 8080,
469+
'TargetPort': 80,
470+
'PublishMode': 'host',
471+
}]
472+
473+
def test_convert_service_ports_multiple(self):
474+
ports = {
475+
8080: (80, None, 'host'),
476+
9999: 99,
477+
2375: (2375,)
478+
}
479+
480+
converted_ports = convert_service_ports(ports)
481+
assert {
482+
'Protocol': 'tcp',
483+
'PublishedPort': 8080,
484+
'TargetPort': 80,
485+
'PublishMode': 'host',
486+
} in converted_ports
487+
488+
assert {
489+
'Protocol': 'tcp',
490+
'PublishedPort': 9999,
491+
'TargetPort': 99,
492+
} in converted_ports
493+
494+
assert {
495+
'Protocol': 'tcp',
496+
'PublishedPort': 2375,
497+
'TargetPort': 2375,
498+
} in converted_ports
499+
500+
assert len(converted_ports) == 3

0 commit comments

Comments
 (0)