docker_container: allow to configure comparison for existing containers (#44789)

* Added comparison configuration.

* Improving user feedback on specifying a wrong option.

* Avoid bare except.

* Added basic integration tests.

* Adding wildcard support.

* Warn if ignore_image=yes is overridden.

* Added changelog fragment.
pull/46250/head
Felix Fontein 6 years ago committed by John R Barker
parent 9efc3dc761
commit 84682464c7

@ -0,0 +1,2 @@
minor_changes:
- "The restart/idempotency behavior of docker_container can now be controlled with the new comparisons parameter."

@ -52,6 +52,27 @@ options:
- Command to execute when the container starts. - Command to execute when the container starts.
A command may be either a string or a list. A command may be either a string or a list.
Prior to version 2.4, strings were split on commas. Prior to version 2.4, strings were split on commas.
comparisons:
type: dict
description:
- Allows to specify how properties of existing containers are compared with
module options to decide whether the container should be recreated / updated
or not. Only options which correspond to the state of a container as handled
by the Docker daemon can be specified.
- Must be a dictionary specifying for an option one of the keys C(strict), C(ignore)
and C(allow_more_present).
- If C(strict) is specified, values are tested for equality, and changes always
result in updating or restarting. If C(ignore) is specified, changes are ignored.
- C(allow_more_present) is allowed only for lists, sets and dicts. If it is
specified for lists or sets, the container will only be updated or restarted if
the module option contains a value which is not present in the container's
options. If the option is specified for a dict, the container will only be updated
or restarted if the module option contains a key which isn't present in the
container's option, or if the value of a key present differs.
- The wildcard option C(*) can be used to set one of the default values C(strict)
or C(ignore) to I(all) comparisons.
- See the examples for details.
version_added: "2.8"
cpu_period: cpu_period:
description: description:
- Limit CPU CFS (Completely Fair Scheduler) period - Limit CPU CFS (Completely Fair Scheduler) period
@ -135,6 +156,7 @@ options:
container to requested configuration. The evaluation includes the image version. If container to requested configuration. The evaluation includes the image version. If
the image version in the registry does not match the container, the container will be the image version in the registry does not match the container, the container will be
recreated. Stop this behavior by setting C(ignore_image) to I(True). recreated. Stop this behavior by setting C(ignore_image) to I(True).
- I(Warning:) This option is ignored if C(image) or C(*) is used for the C(comparisons) option.
type: bool type: bool
default: 'no' default: 'no'
version_added: "2.2" version_added: "2.2"
@ -587,6 +609,31 @@ EXAMPLES = '''
- sys_time - sys_time
cap_drop: cap_drop:
- all - all
- name: Finer container restart/update control
docker_container:
name: test
image: ubuntu:18.04
env:
- arg1: true
- arg2: whatever
volumes:
- /tmp:/tmp
comparisons:
image: ignore # don't restart containers with older versions of the image
env: strict # we want precisely this environment
volumes: allow_more_present # if there are more volumes, that's ok, as long as `/tmp:/tmp` is there
- name: Finer container restart/update control II
docker_container:
name: test
image: ubuntu:18.04
env:
- arg1: true
- arg2: whatever
comparisons:
'*': ignore # by default, ignore *all* options (including image)
env: strict # except for environment variables; there, we want to be strict
''' '''
RETURN = ''' RETURN = '''
@ -649,7 +696,7 @@ try:
else: else:
from docker.utils.types import Ulimit, LogConfig from docker.utils.types import Ulimit, LogConfig
from ansible.module_utils.docker_common import docker_version from ansible.module_utils.docker_common import docker_version
except: except Exception as dummy:
# missing docker-py handled in ansible.module_utils.docker # missing docker-py handled in ansible.module_utils.docker
pass pass
@ -2125,7 +2172,7 @@ class ContainerManager(DockerBaseClass):
class AnsibleDockerClientContainer(AnsibleDockerClient): class AnsibleDockerClientContainer(AnsibleDockerClient):
def _setup_comparisons(self): def _parse_comparisons(self):
comparisons = {} comparisons = {}
comp_aliases = {} comp_aliases = {}
# Put in defaults # Put in defaults
@ -2139,7 +2186,11 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
etc_hosts='set', etc_hosts='set',
ulimits='set(dict)', ulimits='set(dict)',
) )
all_options = set() # this is for improving user feedback when a wrong option was specified for comparison
for option, data in self.module.argument_spec.items(): for option, data in self.module.argument_spec.items():
all_options.add(option)
for alias in data.get('aliases', []):
all_options.add(alias)
# Ignore options which aren't used as container properties # Ignore options which aren't used as container properties
if option in ('docker_host', 'tls_hostname', 'api_version', 'timeout', 'cacert_path', 'cert_path', if option in ('docker_host', 'tls_hostname', 'api_version', 'timeout', 'cacert_path', 'cert_path',
'key_path', 'ssl_version', 'tls', 'tls_verify', 'debug', 'env_file', 'force_kill', 'key_path', 'ssl_version', 'tls', 'tls_verify', 'debug', 'env_file', 'force_kill',
@ -2168,6 +2219,42 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
# Process legacy ignore options # Process legacy ignore options
if self.module.params['ignore_image']: if self.module.params['ignore_image']:
comparisons['image']['comparison'] = 'ignore' comparisons['image']['comparison'] = 'ignore'
# Process options
if self.module.params.get('comparisons'):
# If '*' appears in comparisons, process it first
if '*' in self.module.params['comparisons']:
value = self.module.params['comparisons']['*']
if value not in ('strict', 'ignore'):
self.fail("The wildcard can only be used with comparison modes 'strict' and 'ignore'!")
for dummy, v in comparisons.items():
v['comparison'] = value
# Now process all other comparisons.
comp_aliases_used = {}
for key, value in self.module.params['comparisons'].items():
if key == '*':
continue
# Find main key
key_main = comp_aliases.get(key)
if key_main is None:
if key_main in all_options:
self.fail(("The module option '%s' cannot be specified in the comparisons dict," +
" since it does not correspond to container's state!") % key)
self.fail("Unknown module option '%s' in comparisons dict!" % key)
if key_main in comp_aliases_used:
self.fail("Both '%s' and '%s' (aliases of %s) are specified in comparisons dict!" % (key, comp_aliases_used[key_main], key_main))
comp_aliases_used[key_main] = key
# Check value and update accordingly
if value in ('strict', 'ignore'):
comparisons[key_main]['comparison'] = value
elif value == 'allow_more_present':
if comparisons[key_main]['type'] == 'value':
self.fail("Option '%s' is a value and not a set/list/dict, so its comparison cannot be %s" % (key, value))
comparisons[key_main]['comparison'] = value
else:
self.fail("Unknown comparison mode '%s'!" % value)
# Check legacy values
if self.module.params['ignore_image'] and comparisons['image']['comparison'] != 'ignore':
self.module.warn('The ignore_image option has been overridden by the comparisons option!')
self.comparisons = comparisons self.comparisons = comparisons
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -2205,7 +2292,7 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
if self.module.params.get('auto_remove') and not self.HAS_AUTO_REMOVE_OPT: if self.module.params.get('auto_remove') and not self.HAS_AUTO_REMOVE_OPT:
self.fail("'auto_remove' is not compatible with the 'docker-py' Python package. It requires the newer 'docker' Python package.") self.fail("'auto_remove' is not compatible with the 'docker-py' Python package. It requires the newer 'docker' Python package.")
self._setup_comparisons() self._parse_comparisons()
def main(): def main():
@ -2216,6 +2303,7 @@ def main():
cap_drop=dict(type='list'), cap_drop=dict(type='list'),
cleanup=dict(type='bool', default=False), cleanup=dict(type='bool', default=False),
command=dict(type='raw'), command=dict(type='raw'),
comparisons=dict(type='dict'),
cpu_period=dict(type='int'), cpu_period=dict(type='int'),
cpu_quota=dict(type='int'), cpu_quota=dict(type='int'),
cpuset_cpus=dict(type='str'), cpuset_cpus=dict(type='str'),

@ -0,0 +1,447 @@
---
- name: Registering container name
set_fact:
cname: "{{ cname_prefix ~ '-comparisons' }}"
- name: Registering container name
set_fact:
cnames: "{{ cnames }} + [cname]"
####################################################################
## value ###########################################################
####################################################################
- name: value
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
hostname: example.com
register: value_1
- name: value (change, ignore)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
hostname: example.org
comparisons:
hostname: ignore
register: value_2
- name: value (change, strict)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
hostname: example.org
stop_timeout: 1
comparisons:
hostname: strict
register: value_3
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- value_1 is changed
- value_2 is not changed
- value_3 is changed
####################################################################
## list ############################################################
####################################################################
- name: list
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
dns_servers:
- 1.1.1.1
- 8.8.8.8
register: list_1
- name: list (change, ignore)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
dns_servers:
- 9.9.9.9
comparisons:
dns_servers: ignore
register: list_2
- name: list (change, strict)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
dns_servers:
- 9.9.9.9
comparisons:
dns_servers: strict
stop_timeout: 1
register: list_3
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- list_1 is changed
- list_2 is not changed
- list_3 is changed
####################################################################
## set #############################################################
####################################################################
- name: set
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
groups:
- 1010
- 1011
register: set_1
- name: set (change, ignore)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
groups:
- 1010
- 1011
- 1012
comparisons:
groups: ignore
register: set_2
- name: set (change, allow_more_present)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
groups:
- 1010
- 1011
- 1012
comparisons:
groups: allow_more_present
stop_timeout: 1
register: set_3
- name: set (change, allow_more_present)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
groups:
- 1010
- 1012
comparisons:
groups: allow_more_present
stop_timeout: 1
register: set_4
- name: set (change, strict)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
groups:
- 1010
- 1012
comparisons:
groups: strict
stop_timeout: 1
register: set_5
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- set_1 is changed
- set_2 is not changed
- set_3 is changed
- set_4 is not changed
- set_5 is changed
####################################################################
## set(dict) #######################################################
####################################################################
- name: set(dict)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
devices:
- "/dev/random:/dev/virt-random:rwm"
- "/dev/urandom:/dev/virt-urandom:rwm"
register: set_dict_1
- name: set(dict) (change, ignore)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
devices:
- "/dev/random:/dev/virt-random:rwm"
- "/dev/urandom:/dev/virt-urandom:rwm"
- "/dev/null:/dev/virt-null:rwm"
comparisons:
devices: ignore
register: set_dict_2
- name: set(dict) (change, allow_more_present)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
devices:
- "/dev/random:/dev/virt-random:rwm"
- "/dev/urandom:/dev/virt-urandom:rwm"
- "/dev/null:/dev/virt-null:rwm"
comparisons:
devices: allow_more_present
stop_timeout: 1
register: set_dict_3
- name: set(dict) (change, allow_more_present)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
devices:
- "/dev/random:/dev/virt-random:rwm"
- "/dev/null:/dev/virt-null:rwm"
comparisons:
devices: allow_more_present
stop_timeout: 1
register: set_dict_4
- name: set(dict) (change, strict)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
devices:
- "/dev/random:/dev/virt-random:rwm"
- "/dev/null:/dev/virt-null:rwm"
comparisons:
devices: strict
stop_timeout: 1
register: set_dict_5
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- set_dict_1 is changed
- set_dict_2 is not changed
- set_dict_3 is changed
- set_dict_4 is not changed
- set_dict_5 is changed
####################################################################
## dict ############################################################
####################################################################
- name: dict
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
labels:
ansible.test.1: hello
ansible.test.2: world
register: dict_1
- name: dict (change, ignore)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
labels:
ansible.test.1: hello
ansible.test.2: world
ansible.test.3: ansible
comparisons:
labels: ignore
register: dict_2
- name: dict (change, allow_more_present)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
labels:
ansible.test.1: hello
ansible.test.2: world
ansible.test.3: ansible
comparisons:
labels: allow_more_present
stop_timeout: 1
register: dict_3
- name: dict (change, allow_more_present)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
labels:
ansible.test.1: hello
ansible.test.3: ansible
comparisons:
labels: allow_more_present
stop_timeout: 1
register: dict_4
- name: dict (change, strict)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
labels:
ansible.test.1: hello
ansible.test.3: ansible
comparisons:
labels: strict
stop_timeout: 1
register: dict_5
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- dict_1 is changed
- dict_2 is not changed
- dict_3 is changed
- dict_4 is not changed
- dict_5 is changed
####################################################################
## wildcard ########################################################
####################################################################
- name: Pull hello-world image to make sure wildcard_2 test succeeds
# If the image isn't there, it will pull it and return 'changed'.
docker_image:
name: hello-world
pull: true
- name: wildcard
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
hostname: example.com
labels:
ansible.test.1: hello
ansible.test.2: world
ansible.test.3: ansible
register: wildcard_1
- name: wildcard (change, ignore)
docker_container:
image: hello-world
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
hostname: example.org
labels:
ansible.test.1: hello
ansible.test.4: ignore
comparisons:
'*': ignore
register: wildcard_2
- name: wildcard (change, strict)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
hostname: example.org
stop_timeout: 1
labels:
ansible.test.1: hello
ansible.test.2: world
ansible.test.3: ansible
comparisons:
'*': strict
register: wildcard_3
- name: wildcard (no change, strict)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
hostname: example.org
stop_timeout: 1
labels:
ansible.test.1: hello
ansible.test.2: world
ansible.test.3: ansible
comparisons:
'*': strict
register: wildcard_4
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- wildcard_1 is changed
- wildcard_2 is not changed
- wildcard_3 is changed
- wildcard_4 is not changed
Loading…
Cancel
Save