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.
This commit is contained in:
parent
9efc3dc761
commit
84682464c7
3 changed files with 540 additions and 3 deletions
|
@ -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.
|
||||
A command may be either a string or a list.
|
||||
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:
|
||||
description:
|
||||
- Limit CPU CFS (Completely Fair Scheduler) period
|
||||
|
@ -135,6 +156,7 @@ options:
|
|||
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
|
||||
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
|
||||
default: 'no'
|
||||
version_added: "2.2"
|
||||
|
@ -587,6 +609,31 @@ EXAMPLES = '''
|
|||
- sys_time
|
||||
cap_drop:
|
||||
- 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 = '''
|
||||
|
@ -649,7 +696,7 @@ try:
|
|||
else:
|
||||
from docker.utils.types import Ulimit, LogConfig
|
||||
from ansible.module_utils.docker_common import docker_version
|
||||
except:
|
||||
except Exception as dummy:
|
||||
# missing docker-py handled in ansible.module_utils.docker
|
||||
pass
|
||||
|
||||
|
@ -2125,7 +2172,7 @@ class ContainerManager(DockerBaseClass):
|
|||
|
||||
class AnsibleDockerClientContainer(AnsibleDockerClient):
|
||||
|
||||
def _setup_comparisons(self):
|
||||
def _parse_comparisons(self):
|
||||
comparisons = {}
|
||||
comp_aliases = {}
|
||||
# Put in defaults
|
||||
|
@ -2139,7 +2186,11 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
|
|||
etc_hosts='set',
|
||||
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():
|
||||
all_options.add(option)
|
||||
for alias in data.get('aliases', []):
|
||||
all_options.add(alias)
|
||||
# Ignore options which aren't used as container properties
|
||||
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',
|
||||
|
@ -2168,6 +2219,42 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
|
|||
# Process legacy ignore options
|
||||
if self.module.params['ignore_image']:
|
||||
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
|
||||
|
||||
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:
|
||||
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():
|
||||
|
@ -2216,6 +2303,7 @@ def main():
|
|||
cap_drop=dict(type='list'),
|
||||
cleanup=dict(type='bool', default=False),
|
||||
command=dict(type='raw'),
|
||||
comparisons=dict(type='dict'),
|
||||
cpu_period=dict(type='int'),
|
||||
cpu_quota=dict(type='int'),
|
||||
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…
Reference in a new issue