docker_swarm_service: Fix publish idempotency when mode is None (#50882)

* Fix publish idempotency when mode is None

* Add changelog fragment

* Python 2.6 compat

* Use self.publish

* Check length of publish before comparing content

* Sort publish lists before zipping

* Enable publish tests

* python3 compat

* Don’t sort by mode as it is not safe

* Document publish suboptions and add them to args

* Add type to publish documentation

* Add choices to publish argument_spec suboptions

* Make tcp the default protocol

* Make documentation reflect protocol default

* Simplify setting mode

* Remove redundant string quoting

* Test order of publish

* Add comment about publish change detection
This commit is contained in:
Hannes Ljungberg 2019-01-21 20:02:08 +01:00 committed by ansibot
parent 420c24ea55
commit 7ceb2ac95a
3 changed files with 123 additions and 44 deletions

View file

@ -0,0 +1,2 @@
bugfixes:
- "docker_swarm_service - fixing falsely reporting ``publish`` as changed when ``publish.mode`` is not set."

View file

@ -196,13 +196,41 @@ options:
- List of the service networks names. - List of the service networks names.
- Maps docker service --network option. - Maps docker service --network option.
publish: publish:
default: [] type: list
required: false required: false
default: []
description: description:
- List of dictionaries describing the service published ports. - List of dictionaries describing the service published ports.
- Every item must be a dictionary exposing the keys published_port, target_port, protocol (defaults to 'tcp')
- Only used with api_version >= 1.25 - Only used with api_version >= 1.25
- If API version >= 1.32 and docker python library >= 3.0.0 attribute 'mode' can be set to 'ingress' or 'host' (default 'ingress'). suboptions:
published_port:
type: int
required: true
description:
- The port to make externally available.
target_port:
type: int
required: true
description:
- The port inside the container to expose.
protocol:
type: str
required: false
default: tcp
description:
- What protocol to use.
choices:
- tcp
- udp
mode:
type: str
required: false
description:
- What publish mode to use.
- Requires API version >= 1.32 and docker python library >= 3.0.0
choices:
- ingress
- host
replicas: replicas:
required: false required: false
default: -1 default: -1
@ -470,6 +498,7 @@ EXAMPLES = '''
''' '''
import time import time
import operator
from ansible.module_utils.docker_common import ( from ansible.module_utils.docker_common import (
DockerBaseClass, DockerBaseClass,
AnsibleDockerClient, AnsibleDockerClient,
@ -478,7 +507,6 @@ from ansible.module_utils.docker_common import (
from ansible.module_utils.basic import human_to_bytes from ansible.module_utils.basic import human_to_bytes
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
try: try:
from distutils.version import LooseVersion from distutils.version import LooseVersion
from docker import types from docker import types
@ -623,8 +651,8 @@ class DockerService(DockerBaseClass):
s.publish = [] s.publish = []
for param_p in ap['publish']: for param_p in ap['publish']:
service_p = {} service_p = {}
service_p['protocol'] = param_p.get('protocol', 'tcp') service_p['protocol'] = param_p['protocol']
service_p['mode'] = param_p.get('mode', None) service_p['mode'] = param_p['mode']
service_p['published_port'] = int(param_p['published_port']) service_p['published_port'] = int(param_p['published_port'])
service_p['target_port'] = int(param_p['target_port']) service_p['target_port'] = int(param_p['target_port'])
if service_p['protocol'] not in ['tcp', 'udp']: if service_p['protocol'] not in ['tcp', 'udp']:
@ -710,7 +738,7 @@ class DockerService(DockerBaseClass):
differences.add('reserve_memory', parameter=self.reserve_memory, active=os.reserve_memory) differences.add('reserve_memory', parameter=self.reserve_memory, active=os.reserve_memory)
if self.container_labels != os.container_labels: if self.container_labels != os.container_labels:
differences.add('container_labels', parameter=self.container_labels, active=os.container_labels) differences.add('container_labels', parameter=self.container_labels, active=os.container_labels)
if self.publish != os.publish: if self.has_publish_changed(os.publish):
differences.add('publish', parameter=self.publish, active=os.publish) differences.add('publish', parameter=self.publish, active=os.publish)
if self.restart_policy != os.restart_policy: if self.restart_policy != os.restart_policy:
differences.add('restart_policy', parameter=self.restart_policy, active=os.restart_policy) differences.add('restart_policy', parameter=self.restart_policy, active=os.restart_policy)
@ -750,6 +778,27 @@ class DockerService(DockerBaseClass):
force_update = True force_update = True
return not differences.empty or force_update, differences, needs_rebuild, force_update return not differences.empty or force_update, differences, needs_rebuild, force_update
def has_publish_changed(self, old_publish):
if len(self.publish) != len(old_publish):
return True
publish_sorter = operator.itemgetter('published_port', 'target_port', 'protocol')
publish = sorted(self.publish, key=publish_sorter)
old_publish = sorted(old_publish, key=publish_sorter)
for publish_item, old_publish_item in zip(publish, old_publish):
ignored_keys = set()
if not publish_item.get('mode'):
ignored_keys.add('mode')
# Create copies of publish_item dicts where keys specified in ignored_keys are left out
filtered_old_publish_item = dict(
(k, v) for k, v in old_publish_item.items() if k not in ignored_keys
)
filtered_publish_item = dict(
(k, v) for k, v in publish_item.items() if k not in ignored_keys
)
if filtered_publish_item != filtered_old_publish_item:
return True
return False
def __str__(self): def __str__(self):
return str({ return str({
'mode': self.mode, 'mode': self.mode,
@ -1137,7 +1186,12 @@ def main():
force_update=dict(default=False, type='bool'), force_update=dict(default=False, type='bool'),
log_driver=dict(default="json-file", type='str'), log_driver=dict(default="json-file", type='str'),
log_driver_options=dict(default={}, type='dict'), log_driver_options=dict(default={}, type='dict'),
publish=dict(default=[], type='list'), publish=dict(default=[], type='list', elements='dict', options=dict(
published_port=dict(type='int', required=True),
target_port=dict(type='int', required=True),
protocol=dict(default='tcp', type='str', required=False, choices=('tcp', 'udp')),
mode=dict(type='str', required=False, choices=('ingress', 'host')),
)),
constraints=dict(default=[], type='list'), constraints=dict(default=[], type='list'),
tty=dict(default=False, type='bool'), tty=dict(default=False, type='bool'),
dns=dict(default=[], type='list'), dns=dict(default=[], type='list'),

View file

@ -954,14 +954,6 @@
## publish ######################################################### ## publish #########################################################
#################################################################### ####################################################################
# FIXME: publish_2 is not marked as changed
#fatal: [testhost]: FAILED! => {
# "assertion": "publish_2 is not changed",
# "changed": false,
# "evaluated_to": false,
# "msg": "Assertion failed"
#}
- name: publish - name: publish
docker_swarm_service: docker_swarm_service:
name: "{{ service_name }}" name: "{{ service_name }}"
@ -975,37 +967,68 @@
target_port: 60002 target_port: 60002
register: publish_1 register: publish_1
#- name: publish (idempotency) - name: publish (idempotency)
# docker_swarm_service: docker_swarm_service:
# name: "{{ service_name }}" name: "{{ service_name }}"
# image: alpine:3.8 image: alpine:3.8
# publish: publish:
# - protocol: tcp - protocol: udp
# published_port: 60001 published_port: 60002
# target_port: 60001 target_port: 60002
# - protocol: udp - published_port: 60001
# published_port: 60001 target_port: 60001
# target_port: 60001 register: publish_2
# register: publish_2
# - name: publish (change)
#- name: publish (change) docker_swarm_service:
# docker_swarm_service: name: "{{ service_name }}"
# name: "{{ service_name }}" image: alpine:3.8
# image: alpine:3.8 publish:
# publish: - protocol: tcp
# - protocol: tcp published_port: 60002
# published_port: 60002 target_port: 60003
# target_port: 60001 - protocol: udp
# - protocol: udp published_port: 60001
# published_port: 60001 target_port: 60001
# target_port: 60001 register: publish_3
# register: publish_3
# - name: publish (mode)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
publish:
- protocol: tcp
published_port: 60002
target_port: 60003
mode: host
- protocol: udp
published_port: 60001
target_port: 60001
mode: host
register: publish_4
- name: publish (mode idempotency)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
publish:
- protocol: udp
published_port: 60001
target_port: 60001
mode: host
- protocol: tcp
published_port: 60002
target_port: 60003
mode: host
register: publish_5
- assert: - assert:
that: that:
- publish_1 is changed - publish_1 is changed
# - publish_2 is not changed - publish_2 is not changed
# - publish_3 is changed - publish_3 is changed
- publish_4 is changed
- publish_5 is not changed
when: docker_api_version is version('1.25', '>=') when: docker_api_version is version('1.25', '>=')
- assert: - assert:
that: that: