Implement rolling_versions for docker_config (#295)

This commit is contained in:
András Maróy 2022-02-12 07:30:09 +00:00 committed by GitHub
parent b481fa4801
commit 1d062dad5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 209 additions and 15 deletions

View File

@ -0,0 +1,2 @@
minor_changes:
- docker_config - add support for rolling update, set ``rolling_versions`` to ``true`` to enable (https://github.com/ansible-collections/community.docker/pull/295, https://github.com/ansible-collections/community.docker/issues/109).

View File

@ -51,6 +51,21 @@ options:
- If C(true), an existing config will be replaced, even if it has not been changed. - If C(true), an existing config will be replaced, even if it has not been changed.
type: bool type: bool
default: no default: no
rolling_versions:
description:
- If set to C(true), configs are created with an increasing version number appended to their name.
- Adds a label containing the version number to the managed configs with the name C(ansible_version).
type: bool
default: false
version_added: 2.2.0
versions_to_keep:
description:
- When using I(rolling_versions), the number of old versions of the config to keep.
- Extraneous old configs are deleted after the new one is created.
- Set to C(-1) to keep everything or to C(0) or C(1) to keep only the current one.
type: int
default: 5
version_added: 2.2.0
name: name:
description: description:
- The name of the config. - The name of the config.
@ -156,6 +171,13 @@ config_id:
returned: success and I(state) is C(present) returned: success and I(state) is C(present)
type: str type: str
sample: 'hzehrmyjigmcp2gb6nlhmjqcv' sample: 'hzehrmyjigmcp2gb6nlhmjqcv'
config_name:
description:
- The name of the created config object.
returned: success and I(state) is C(present)
type: str
sample: 'awesome_config'
version_added: 2.2.0
''' '''
import base64 import base64
@ -205,15 +227,36 @@ class ConfigManager(DockerBaseClass):
self.client.fail('Error while reading {src}: {error}'.format(src=data_src, error=to_native(exc))) self.client.fail('Error while reading {src}: {error}'.format(src=data_src, error=to_native(exc)))
self.labels = parameters.get('labels') self.labels = parameters.get('labels')
self.force = parameters.get('force') self.force = parameters.get('force')
self.rolling_versions = parameters.get('rolling_versions')
self.versions_to_keep = parameters.get('versions_to_keep')
if self.rolling_versions:
self.version = 0
self.data_key = None self.data_key = None
self.configs = []
def __call__(self): def __call__(self):
self.get_config()
if self.state == 'present': if self.state == 'present':
self.data_key = hashlib.sha224(self.data).hexdigest() self.data_key = hashlib.sha224(self.data).hexdigest()
self.present() self.present()
self.remove_old_versions()
elif self.state == 'absent': elif self.state == 'absent':
self.absent() self.absent()
def get_version(self, config):
try:
return int(config.get('Spec', {}).get('Labels', {}).get('ansible_version', 0))
except ValueError:
return 0
def remove_old_versions(self):
if not self.rolling_versions or self.versions_to_keep < 0:
return
if not self.check_mode:
while len(self.configs) > max(self.versions_to_keep, 1):
self.remove_config(self.configs.pop(0))
def get_config(self): def get_config(self):
''' Find an existing config. ''' ''' Find an existing config. '''
try: try:
@ -221,10 +264,17 @@ class ConfigManager(DockerBaseClass):
except APIError as exc: except APIError as exc:
self.client.fail("Error accessing config %s: %s" % (self.name, to_native(exc))) self.client.fail("Error accessing config %s: %s" % (self.name, to_native(exc)))
for config in configs: if self.rolling_versions:
if config['Spec']['Name'] == self.name: self.configs = [
return config config
return None for config in configs
if config['Spec']['Name'].startswith('{name}_v'.format(name=self.name))
]
self.configs.sort(key=self.get_version)
else:
self.configs = [
config for config in configs if config['Spec']['Name'] == self.name
]
def create_config(self): def create_config(self):
''' Create a new config ''' ''' Create a new config '''
@ -233,12 +283,17 @@ class ConfigManager(DockerBaseClass):
labels = { labels = {
'ansible_key': self.data_key 'ansible_key': self.data_key
} }
if self.rolling_versions:
self.version += 1
labels['ansible_version'] = str(self.version)
self.name = '{name}_v{version}'.format(name=self.name, version=self.version)
if self.labels: if self.labels:
labels.update(self.labels) labels.update(self.labels)
try: try:
if not self.check_mode: if not self.check_mode:
config_id = self.client.create_config(self.name, self.data, labels=labels) config_id = self.client.create_config(self.name, self.data, labels=labels)
self.configs += self.client.configs(filters={'id': config_id})
except APIError as exc: except APIError as exc:
self.client.fail("Error creating config: %s" % to_native(exc)) self.client.fail("Error creating config: %s" % to_native(exc))
@ -247,36 +302,48 @@ class ConfigManager(DockerBaseClass):
return config_id return config_id
def remove_config(self, config):
try:
if not self.check_mode:
self.client.remove_config(config['ID'])
except APIError as exc:
self.client.fail("Error removing config %s: %s" % (config['Spec']['Name'], to_native(exc)))
def present(self): def present(self):
''' Handles state == 'present', creating or updating the config ''' ''' Handles state == 'present', creating or updating the config '''
config = self.get_config() if self.configs:
if config: config = self.configs[-1]
self.results['config_id'] = config['ID'] self.results['config_id'] = config['ID']
self.results['config_name'] = config['Spec']['Name']
data_changed = False data_changed = False
attrs = config.get('Spec', {}) attrs = config.get('Spec', {})
if attrs.get('Labels', {}).get('ansible_key'): if attrs.get('Labels', {}).get('ansible_key'):
if attrs['Labels']['ansible_key'] != self.data_key: if attrs['Labels']['ansible_key'] != self.data_key:
data_changed = True data_changed = True
else:
if not self.force:
self.client.module.warn("'ansible_key' label not found. Config will not be changed unless the force parameter is set to 'yes'")
labels_changed = not compare_generic(self.labels, attrs.get('Labels'), 'allow_more_present', 'dict') labels_changed = not compare_generic(self.labels, attrs.get('Labels'), 'allow_more_present', 'dict')
if self.rolling_versions:
self.version = self.get_version(config)
if data_changed or labels_changed or self.force: if data_changed or labels_changed or self.force:
# if something changed or force, delete and re-create the config # if something changed or force, delete and re-create the config
if not self.rolling_versions:
self.absent() self.absent()
config_id = self.create_config() config_id = self.create_config()
self.results['changed'] = True self.results['changed'] = True
self.results['config_id'] = config_id self.results['config_id'] = config_id
self.results['config_name'] = self.name
else: else:
self.results['changed'] = True self.results['changed'] = True
self.results['config_id'] = self.create_config() self.results['config_id'] = self.create_config()
self.results['config_name'] = self.name
def absent(self): def absent(self):
''' Handles state == 'absent', removing the config ''' ''' Handles state == 'absent', removing the config '''
config = self.get_config() if self.configs:
if config: for config in self.configs:
try: self.remove_config(config)
if not self.check_mode:
self.client.remove_config(config['ID'])
except APIError as exc:
self.client.fail("Error removing config %s: %s" % (self.name, to_native(exc)))
self.results['changed'] = True self.results['changed'] = True
@ -288,7 +355,9 @@ def main():
data_is_b64=dict(type='bool', default=False), data_is_b64=dict(type='bool', default=False),
data_src=dict(type='path'), data_src=dict(type='path'),
labels=dict(type='dict'), labels=dict(type='dict'),
force=dict(type='bool', default=False) force=dict(type='bool', default=False),
rolling_versions=dict(type='bool', default=False),
versions_to_keep=dict(type='int', default=5),
) )
required_if = [ required_if = [

View File

@ -150,6 +150,82 @@
that: that:
- not output.changed - not output.changed
# Rolling update
- name: Create rolling config
docker_config:
name: rolling_password
data: opensesame!
rolling_versions: true
state: present
register: original_output
- name: Create variable config_id
set_fact:
config_id: "{{ original_output.config_id }}"
- name: Inspect config
command: "docker config inspect {{ config_id }}"
register: inspect
ignore_errors: yes
- debug: var=inspect
- name: assert config creation succeeded
assert:
that:
- "'rolling_password' in inspect.stdout"
- "'ansible_key' in inspect.stdout"
- "'ansible_version' in inspect.stdout"
- original_output.config_name == 'rolling_password_v1'
when: inspect is not failed
- assert:
that:
- "'is too new. Maximum supported API version is' in inspect.stderr"
when: inspect is failed
- name: Create config again
docker_config:
name: rolling_password
data: newpassword!
rolling_versions: true
state: present
register: new_output
- name: assert that new version is created
assert:
that:
- new_output.changed
- new_output.config_id != original_output.config_id
- new_output.config_name != original_output.config_name
- new_output.config_name == 'rolling_password_v2'
- name: Remove rolling configs
docker_config:
name: rolling_password
rolling_versions: true
state: absent
- name: Check that config is removed
command: "docker config inspect {{ original_output.config_id }}"
register: output
ignore_errors: yes
- name: assert config was removed
assert:
that:
- output.failed
- name: Check that config is removed
command: "docker config inspect {{ new_output.config_id }}"
register: output
ignore_errors: yes
- name: assert config was removed
assert:
that:
- output.failed
always: always:
- name: Remove a Swarm cluster - name: Remove a Swarm cluster
docker_swarm: docker_swarm:

View File

@ -5,6 +5,7 @@
service_name: "{{ name_prefix ~ '-configs' }}" service_name: "{{ name_prefix ~ '-configs' }}"
config_name_1: "{{ name_prefix ~ '-configs-1' }}" config_name_1: "{{ name_prefix ~ '-configs-1' }}"
config_name_2: "{{ name_prefix ~ '-configs-2' }}" config_name_2: "{{ name_prefix ~ '-configs-2' }}"
config_name_3: "{{ name_prefix ~ '-configs-3' }}"
- name: Registering container name - name: Registering container name
set_fact: set_fact:
@ -24,6 +25,14 @@
register: "config_result_2" register: "config_result_2"
when: docker_api_version is version('1.30', '>=') and docker_py_version is version('2.6.0', '>=') when: docker_api_version is version('1.30', '>=') and docker_py_version is version('2.6.0', '>=')
- docker_config:
name: "{{ config_name_3 }}"
data: "config3"
state: present
rolling_versions: true
register: "config_result_3"
when: docker_api_version is version('1.30', '>=') and docker_py_version is version('2.6.0', '>=')
#################################################################### ####################################################################
## configs ######################################################### ## configs #########################################################
#################################################################### ####################################################################
@ -131,6 +140,40 @@
register: configs_8 register: configs_8
ignore_errors: yes ignore_errors: yes
- name: rolling configs
docker_swarm_service:
name: "{{ service_name }}"
image: "{{ docker_test_image_alpine }}"
resolve_image: no
command: '/bin/sh -v -c "sleep 10m"'
configs:
- config_name: "{{ config_name_3 }}_v1"
filename: "/run/configs/{{ config_name_3 }}.txt"
register: configs_9
ignore_errors: yes
- name: update rolling config
docker_config:
name: "{{ config_name_3 }}"
data: "newconfig3"
state: "present"
rolling_versions: true
register: configs_10
when: docker_api_version is version('1.30', '>=') and docker_py_version is version('2.6.0', '>=')
ignore_errors: yes
- name: rolling configs service update
docker_swarm_service:
name: "{{ service_name }}"
image: "{{ docker_test_image_alpine }}"
resolve_image: no
command: '/bin/sh -v -c "sleep 10m"'
configs:
- config_name: "{{ config_name_3 }}_v2"
filename: "/run/configs/{{ config_name_3 }}.txt"
register: configs_11
ignore_errors: yes
- name: cleanup - name: cleanup
docker_swarm_service: docker_swarm_service:
name: "{{ service_name }}" name: "{{ service_name }}"
@ -147,6 +190,9 @@
- configs_6 is not changed - configs_6 is not changed
- configs_7 is changed - configs_7 is changed
- configs_8 is not changed - configs_8 is not changed
- configs_9 is changed
- configs_10 is not failed
- configs_11 is changed
when: docker_api_version is version('1.30', '>=') and docker_py_version is version('2.6.0', '>=') when: docker_api_version is version('1.30', '>=') and docker_py_version is version('2.6.0', '>=')
- assert: - assert:
@ -407,6 +453,7 @@
loop: loop:
- "{{ config_name_1 }}" - "{{ config_name_1 }}"
- "{{ config_name_2 }}" - "{{ config_name_2 }}"
- "{{ config_name_3 }}"
loop_control: loop_control:
loop_var: config_name loop_var: config_name
ignore_errors: yes ignore_errors: yes