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.
- Maps docker service --network option.
publish:
default: []
type: list
required: false
default: []
description:
- 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
- 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:
required: false
default: -1
@ -470,6 +498,7 @@ EXAMPLES = '''
'''
import time
import operator
from ansible.module_utils.docker_common import (
DockerBaseClass,
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._text import to_text
try:
from distutils.version import LooseVersion
from docker import types
@ -623,8 +651,8 @@ class DockerService(DockerBaseClass):
s.publish = []
for param_p in ap['publish']:
service_p = {}
service_p['protocol'] = param_p.get('protocol', 'tcp')
service_p['mode'] = param_p.get('mode', None)
service_p['protocol'] = param_p['protocol']
service_p['mode'] = param_p['mode']
service_p['published_port'] = int(param_p['published_port'])
service_p['target_port'] = int(param_p['target_port'])
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)
if self.container_labels != 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)
if self.restart_policy != 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
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):
return str({
'mode': self.mode,
@ -1137,7 +1186,12 @@ def main():
force_update=dict(default=False, type='bool'),
log_driver=dict(default="json-file", type='str'),
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'),
tty=dict(default=False, type='bool'),
dns=dict(default=[], type='list'),

View file

@ -954,14 +954,6 @@
## 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
docker_swarm_service:
name: "{{ service_name }}"
@ -975,37 +967,68 @@
target_port: 60002
register: publish_1
#- name: publish (idempotency)
# docker_swarm_service:
# name: "{{ service_name }}"
# image: alpine:3.8
# publish:
# - protocol: tcp
# published_port: 60001
# target_port: 60001
# - protocol: udp
# published_port: 60001
# target_port: 60001
# register: publish_2
#
#- name: publish (change)
# docker_swarm_service:
# name: "{{ service_name }}"
# image: alpine:3.8
# publish:
# - protocol: tcp
# published_port: 60002
# target_port: 60001
# - protocol: udp
# published_port: 60001
# target_port: 60001
# register: publish_3
#
- name: publish (idempotency)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
publish:
- protocol: udp
published_port: 60002
target_port: 60002
- published_port: 60001
target_port: 60001
register: publish_2
- name: publish (change)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
publish:
- protocol: tcp
published_port: 60002
target_port: 60003
- protocol: udp
published_port: 60001
target_port: 60001
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:
that:
- publish_1 is changed
# - publish_2 is not changed
# - publish_3 is changed
- publish_2 is not changed
- publish_3 is changed
- publish_4 is changed
- publish_5 is not changed
when: docker_api_version is version('1.25', '>=')
- assert:
that: