docker_container: add port range and IPv6 support for published_ports (#46596)

* Allow port ranges.

* Adding IPv6 support for published_ports.

* Die when hostname is passed instead of IP address.

* Added changelog.
pull/42757/merge
Felix Fontein 6 years ago committed by John R Barker
parent ccfa6ff011
commit a74774488d

@ -0,0 +1,2 @@
minor_changes:
- "docker_container - published_ports now supports port ranges, IPv6 addresses, and no longer accepts hostnames, which were never used correctly anyway."

@ -301,6 +301,11 @@ options:
- List of ports to publish from the container to the host. - List of ports to publish from the container to the host.
- "Use docker CLI syntax: C(8000), C(9000:8000), or C(0.0.0.0:9000:8000), where 8000 is a - "Use docker CLI syntax: C(8000), C(9000:8000), or C(0.0.0.0:9000:8000), where 8000 is a
container port, 9000 is a host port, and 0.0.0.0 is a host interface." container port, 9000 is a host port, and 0.0.0.0 is a host interface."
- Port ranges can be used for source and destination ports. If two ranges with
different lengths are specified, the shorter range will be used.
- "Bind addresses must be either IPv4 or IPv6 addresses. Hostnames are I(not) allowed. This
is different from the C(docker) command line utility. Use the L(dig lookup,../lookup/dig.html)
to resolve hostnames."
- Container ports must be exposed either in the Dockerfile or via the C(expose) option. - Container ports must be exposed either in the Dockerfile or via the C(expose) option.
- A value of C(all) will publish all exposed container ports to random host ports, ignoring - A value of C(all) will publish all exposed container ports to random host ports, ignoring
any other mappings. any other mappings.
@ -736,6 +741,51 @@ def is_volume_permissions(input):
return True return True
def parse_port_range(range_or_port, module):
'''
Parses a string containing either a single port or a range of ports.
Returns a list of integers for each port in the list.
'''
if '-' in range_or_port:
start, end = [int(port) for port in range_or_port.split('-')]
if end < start:
module.fail_json(msg='Invalid port range: {0}'.format(range_or_port))
return list(range(start, end + 1))
else:
return [int(range_or_port)]
def split_colon_ipv6(input, module):
'''
Split string by ':', while keeping IPv6 addresses in square brackets in one component.
'''
if '[' not in input:
return input.split(':')
start = 0
result = []
while start < len(input):
i = input.find('[', start)
if i < 0:
result.extend(input[start:].split(':'))
break
j = input.find(']', i)
if j < 0:
module.fail_json(msg='Cannot find closing "]" in input "{0}" for opening "[" at index {1}!'.format(input, i + 1))
result.extend(input[start:i].split(':'))
k = input.find(':', j)
if k < 0:
result[-1] += input[i:]
start = len(input)
else:
result[-1] += input[i:k]
if k == len(input):
result.append('')
break
start = k + 1
return result
class TaskParameters(DockerBaseClass): class TaskParameters(DockerBaseClass):
''' '''
Access and parse module parameters Access and parse module parameters
@ -1107,27 +1157,38 @@ class TaskParameters(DockerBaseClass):
binds = {} binds = {}
for port in self.published_ports: for port in self.published_ports:
parts = str(port).split(':') parts = split_colon_ipv6(str(port), self.client.module)
container_port = parts[-1] container_port = parts[-1]
if '/' not in container_port: protocol = ''
container_port = int(parts[-1]) if '/' in container_port:
container_port, protocol = parts[-1].split('/')
container_ports = parse_port_range(container_port, self.client.module)
p_len = len(parts) p_len = len(parts)
if p_len == 1: if p_len == 1:
bind = (default_ip,) port_binds = len(container_ports) * [(default_ip,)]
elif p_len == 2: elif p_len == 2:
bind = (default_ip, int(parts[0])) port_binds = [(default_ip, port) for port in parse_port_range(parts[0], self.client.module)]
elif p_len == 3: elif p_len == 3:
bind = (parts[0], int(parts[1])) if parts[1] else (parts[0],) # We only allow IPv4 and IPv6 addresses for the bind address
if not re.match(r'^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$', parts[0]) and not re.match(r'^\[[0-9a-fA-F:]+\]$', parts[0]):
if container_port in binds: self.fail(('Bind addresses for published ports must be IPv4 or IPv6 addresses, not hostnames. '
old_bind = binds[container_port] 'Use the dig lookup to resolve hostnames. (Found hostname: {0})').format(parts[0]))
if isinstance(old_bind, list): if parts[1]:
old_bind.append(bind) port_binds = [(parts[0], port) for port in parse_port_range(parts[1], self.client.module)]
else: else:
binds[container_port] = [binds[container_port], bind] port_binds = len(container_ports) * [(parts[0],)]
else:
binds[container_port] = bind for bind, container_port in zip(port_binds, container_ports):
idx = '{0}/{1}'.format(container_port, protocol) if protocol else container_port
if idx in binds:
old_bind = binds[idx]
if isinstance(old_bind, list):
old_bind.append(bind)
else:
binds[idx] = [old_bind, bind]
else:
binds[idx] = bind
return binds return binds
def _get_volume_binds(self, volumes): def _get_volume_binds(self, volumes):

@ -92,3 +92,126 @@
- published_ports_3 is changed - published_ports_3 is changed
- published_ports_4 is not changed - published_ports_4 is not changed
- published_ports_5 is changed - published_ports_5 is changed
####################################################################
## published_ports: port range #####################################
####################################################################
- name: published_ports -- port range
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
exposed_ports:
- 8080
- 5000-5040
published_ports:
- 8080:8080
- 5000-5040:5000-5040
stop_timeout: 1
register: published_ports_1
- name: published_ports -- port range (idempotency)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
exposed_ports:
- 8080
- 5000-5040
published_ports:
- 8080:8080
- 5000-5040:5000-5040
stop_timeout: 1
register: published_ports_2
- name: published_ports -- port range (different range)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
exposed_ports:
- 8080
- 5000-5040
published_ports:
- 8080:8080
- 5010-5050:5010-5050
stop_timeout: 1
register: published_ports_3
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- published_ports_1 is changed
- published_ports_2 is not changed
- published_ports_3 is changed
####################################################################
## published_ports: IPv6 addresses #################################
####################################################################
- name: published_ports -- IPv6
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
published_ports:
- "[::1]:8080:8080"
stop_timeout: 1
register: published_ports_1
- name: published_ports -- IPv6 (idempotency)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
published_ports:
- "[::1]:8080:8080"
stop_timeout: 1
register: published_ports_2
- name: published_ports -- IPv6 (different IP)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
published_ports:
- 127.0.0.1:8080:8080
stop_timeout: 1
register: published_ports_3
- name: published_ports -- IPv6 (hostname)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
published_ports:
- localhost:8080:8080
stop_timeout: 1
register: published_ports_4
ignore_errors: yes
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- published_ports_1 is changed
- published_ports_2 is not changed
- published_ports_3 is changed
- published_ports_4 is failed

Loading…
Cancel
Save