Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for rotating docker configs #295

Merged
merged 1 commit into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/272-rolling-configs.yml
Original file line number Diff line number Diff line change
@@ -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).
99 changes: 84 additions & 15 deletions plugins/modules/docker_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@
- If C(true), an existing config will be replaced, even if it has not been changed.
type: bool
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:
description:
- The name of the config.
Expand Down Expand Up @@ -156,6 +171,13 @@
returned: success and I(state) is C(present)
type: str
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
Expand Down Expand Up @@ -205,26 +227,54 @@ def __init__(self, client, results):
self.client.fail('Error while reading {src}: {error}'.format(src=data_src, error=to_native(exc)))
self.labels = parameters.get('labels')
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.configs = []

def __call__(self):
self.get_config()
if self.state == 'present':
self.data_key = hashlib.sha224(self.data).hexdigest()
self.present()
self.remove_old_versions()
elif self.state == '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):
''' Find an existing config. '''
try:
configs = self.client.configs(filters={'name': self.name})
except APIError as exc:
self.client.fail("Error accessing config %s: %s" % (self.name, to_native(exc)))

for config in configs:
if config['Spec']['Name'] == self.name:
return config
return None
if self.rolling_versions:
self.configs = [
config
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):
''' Create a new config '''
Expand All @@ -233,12 +283,17 @@ def create_config(self):
labels = {
'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:
labels.update(self.labels)

try:
if not self.check_mode:
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:
self.client.fail("Error creating config: %s" % to_native(exc))

Expand All @@ -247,36 +302,48 @@ def create_config(self):

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):
''' Handles state == 'present', creating or updating the config '''
config = self.get_config()
if config:
if self.configs:
config = self.configs[-1]
self.results['config_id'] = config['ID']
self.results['config_name'] = config['Spec']['Name']
data_changed = False
attrs = config.get('Spec', {})
if attrs.get('Labels', {}).get('ansible_key'):
if attrs['Labels']['ansible_key'] != self.data_key:
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')
if self.rolling_versions:
self.version = self.get_version(config)
if data_changed or labels_changed or self.force:
# if something changed or force, delete and re-create the config
self.absent()
if not self.rolling_versions:
self.absent()
config_id = self.create_config()
self.results['changed'] = True
self.results['config_id'] = config_id
self.results['config_name'] = self.name
else:
self.results['changed'] = True
self.results['config_id'] = self.create_config()
self.results['config_name'] = self.name

def absent(self):
''' Handles state == 'absent', removing the config '''
config = self.get_config()
if 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" % (self.name, to_native(exc)))
if self.configs:
for config in self.configs:
self.remove_config(config)
self.results['changed'] = True


Expand All @@ -288,7 +355,9 @@ def main():
data_is_b64=dict(type='bool', default=False),
data_src=dict(type='path'),
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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,82 @@
that:
- 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:
- name: Remove a Swarm cluster
docker_swarm:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
service_name: "{{ name_prefix ~ '-configs' }}"
config_name_1: "{{ name_prefix ~ '-configs-1' }}"
config_name_2: "{{ name_prefix ~ '-configs-2' }}"
config_name_3: "{{ name_prefix ~ '-configs-3' }}"

- name: Registering container name
set_fact:
Expand All @@ -24,6 +25,14 @@
register: "config_result_2"
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 #########################################################
####################################################################
Expand Down Expand Up @@ -131,6 +140,40 @@
register: configs_8
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
docker_swarm_service:
name: "{{ service_name }}"
Expand All @@ -147,6 +190,9 @@
- configs_6 is not changed
- configs_7 is 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', '>=')

- assert:
Expand Down Expand Up @@ -407,6 +453,7 @@
loop:
- "{{ config_name_1 }}"
- "{{ config_name_2 }}"
- "{{ config_name_3 }}"
loop_control:
loop_var: config_name
ignore_errors: yes
Expand Down