From da62cfc24a2d018973cdc1267c097d2e8c10e270 Mon Sep 17 00:00:00 2001 From: Sakar Date: Mon, 1 Mar 2021 17:32:53 +0530 Subject: [PATCH] New module docker_plugin (#95) * New module docker-plugin with integration_tests * Fix sanity test * Changes made as per the reviewer suggested * Fix check-mode and test directory * Fix Sanity * fix integration * fix integration * fix integration * fix integration --- plugins/modules/docker_plugin.py | 343 ++++++++++++++++++ .../integration/targets/docker_plugin/aliases | 2 + .../targets/docker_plugin/meta/main.yml | 3 + .../targets/docker_plugin/tasks/main.yaml | 24 ++ .../targets/docker_plugin/tasks/run-test.yml | 3 + .../docker_plugin/tasks/tests/basic.yml | 80 ++++ 6 files changed, 455 insertions(+) create mode 100644 plugins/modules/docker_plugin.py create mode 100644 tests/integration/targets/docker_plugin/aliases create mode 100644 tests/integration/targets/docker_plugin/meta/main.yml create mode 100644 tests/integration/targets/docker_plugin/tasks/main.yaml create mode 100644 tests/integration/targets/docker_plugin/tasks/run-test.yml create mode 100644 tests/integration/targets/docker_plugin/tasks/tests/basic.yml diff --git a/plugins/modules/docker_plugin.py b/plugins/modules/docker_plugin.py new file mode 100644 index 00000000..0d058061 --- /dev/null +++ b/plugins/modules/docker_plugin.py @@ -0,0 +1,343 @@ +#!/usr/bin/python +# coding: utf-8 +# +# Copyright: (c) 2021 Red Hat | Ansible Sakar Mehra<@sakarmehra100@gmail.com | @sakar97> +# Copyright: (c) 2019, Vladimir Porshkevich (@porshkevich) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +module: docker_plugin +short_description: Manage Docker plugins +version_added: 1.3.0 +description: + - This module allows to install, delete, enable and disable Docker plugins. + - Performs largely the same function as the C(docker plugin) CLI subcommand. +options: + plugin_name: + description: + - Name of the plugin to operate on. + required: true + type: str + + state: + description: + - C(absent) remove the plugin. + - C(present) install the plugin, if it does not already exist. + - C(enable) enable the plugin. + - C(disable) disable the plugin. + default: present + choices: + - absent + - present + - enable + - disable + type: str + + plugin_options: + description: + - Dictionary of plugin settings. + type: dict + + force_remove: + description: + - Remove even if the plugin is enabled. + default: False + type: bool + + enable_timeout: + description: + - Timeout in seconds. + type: int + default: 0 + +extends_documentation_fragment: + - community.docker.docker + - community.docker.docker.docker_py_2_documentation + +author: + - Sakar Mehra (@sakar97) + - Vladimir Porshkevich (@porshkevich) + +requirements: + - "python >= 2.7" + - "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 2.6.0" + - "Docker API >= 1.25" +''' + +EXAMPLES = ''' +- name: Install a plugin + community.docker.docker_plugin: + plugin_name: plugin_one + state: present + +- name: Remove a plugin + community.docker.docker_plugin: + plugin_name: plugin_one + state: absent + +- name: Enable the plugin + community.docker.docker_plugin: + plugin_name: plugin_one + state: enable + +- name: Disable the plugin + community.docker.docker_plugin: + plugin_name: plugin_one + state: disable + +- name: Install a plugin with options + community.docker.docker_plugin: + name: weaveworks/net-plugin:latest_release + plugin_options: + IPALLOC_RANGE: "10.32.0.0/12" + WEAVE_PASSWORD: "PASSWORD" +''' + +RETURN = ''' +plugin: + description: + - Plugin inspection results for the affected plugin. + returned: success + type: dict + sample: {} +''' + +import traceback + +try: + from docker.errors import APIError, NotFound, DockerException + from docker import DockerClient +except ImportError: + # missing docker-py handled in ansible.module_utils.docker_common + pass + +from ansible_collections.community.docker.plugins.module_utils.common import ( + DockerBaseClass, + AnsibleDockerClient, + DifferenceTracker, + RequestException +) + +from ansible.module_utils.six import text_type + + +class TaskParameters(DockerBaseClass): + def __init__(self, client): + super(TaskParameters, self).__init__() + self.client = client + self.plugin_name = None + self.plugin_options = None + self.debug = None + self.force_remove = None + self.enable_timeout = None + + for key, value in client.module.params.items(): + setattr(self, key, value) + + +def prepare_options(options): + return ['%s=%s' % (k, v if v is not None else "") for k, v in options.items()] if options else [] + + +def parse_options(options_list): + return dict(x.split('=', 1) for x in options_list) if options_list else {} + + +class DockerPluginManager(object): + + def __init__(self, client): + self.client = client + + self.dclient = DockerClient(**self.client._connect_params) + self.dclient.api = client + + self.parameters = TaskParameters(client) + self.check_mode = self.client.check_mode + self.results = { + u'changed': False, + u'actions': [] + } + self.diff = self.client.module._diff + self.diff_tracker = DifferenceTracker() + self.diff_result = dict() + + self.existing_plugin = self.get_existing_plugin() + + state = self.parameters.state + if state == 'present': + self.present() + elif state == 'absent': + self.absent() + elif state == 'enable': + self.enable() + elif state == 'disable': + self.disable() + + if self.diff or self.check_mode or self.parameters.debug: + if self.diff: + self.diff_result['before'], self.diff_result['after'] = self.diff_tracker.get_before_after() + self.results['diff'] = self.diff_result + + def get_existing_plugin(self): + name = self.parameters.plugin_name + try: + plugin = self.dclient.plugins.get(name) + except NotFound: + return None + except APIError as e: + self.client.fail(text_type(e)) + + if plugin is None: + return None + else: + return plugin + + def has_different_config(self): + """ + Return the list of differences between the current parameters and the existing plugin. + + :return: list of options that differ + """ + differences = DifferenceTracker() + if self.parameters.plugin_options: + if not self.existing_plugin.settings: + differences.add('plugin_options', parameters=self.parameters.plugin_options, active=self.existing_plugin.settings['Env']) + else: + existing_options_list = self.existing_plugin.settings['Env'] + existing_options = parse_options(existing_options_list) + + for key, value in self.parameters.plugin_options.items(): + options_count = 0 + if ((not existing_options.get(key) and value) or + not value or + value != existing_options[key]): + differences.add('plugin_options.%s' % key, + parameter=value, + active=self.existing_plugin.settings['Env'][options_count]) + + return differences + + def install_plugin(self): + if not self.existing_plugin: + if not self.check_mode: + try: + self.existing_plugin = self.dclient.plugins.install(self.parameters.plugin_name, None) + except APIError as e: + self.client.fail(text_type(e)) + + self.results['actions'].append("Installed plugin %s" % self.parameters.plugin_name) + self.results['changed'] = True + + def remove_plugin(self): + force = self.parameters.force_remove + if self.existing_plugin: + if not self.check_mode: + try: + self.existing_plugin.remove(force) + except APIError as e: + self.client.fail(text_type(e)) + + self.results['actions'].append("Removed plugin %s" % self.parameters.plugin_name) + self.results['changed'] = True + + def update_plugin(self): + if self.existing_plugin: + differences = self.has_different_config() + if not differences.empty: + if not self.check_mode: + try: + self.existing_plugin.configure(prepare_options(self.parameters.plugin_options)) + except APIError as e: + self.client.fail(text_type(e)) + self.results['actions'].append("Updated plugin %s settings" % self.parameters.plugin_name) + self.results['changed'] = True + else: + self.fail("Cannot update the plugin: Plugin does not exist") + + def present(self): + differences = DifferenceTracker() + if self.existing_plugin: + differences = self.has_different_config() + + self.diff_tracker.add('exists', parameter=True, active=self.existing_plugin is not None) + + if self.existing_plugin: + self.update_plugin() + else: + self.install_plugin() + + if self.diff or self.check_mode or self.parameters.debug: + self.diff_tracker.merge(differences) + + if not self.check_mode and not self.parameters.debug: + self.results.pop('actions') + + def absent(self): + self.remove_plugin() + + def enable(self): + timeout = self.parameters.enable_timeout + if self.existing_plugin: + if not self.existing_plugin.enabled: + if not self.check_mode: + try: + self.existing_plugin.enable(timeout) + except APIError as e: + self.client.fail(text_type(e)) + self.results['actions'].append("Enabled plugin %s" % self.parameters.plugin_name) + self.results['changed'] = True + else: + self.install_plugin() + if not self.check_mode: + try: + self.existing_plugin.enable(timeout) + except APIError as e: + self.client.fail(text_type(e)) + self.results['actions'].append("Enabled plugin %s" % self.parameters.plugin_name) + self.results['changed'] = True + + def disable(self): + if self.existing_plugin: + if self.existing_plugin.enabled: + if not self.check_mode: + try: + self.existing_plugin.disable() + except APIError as e: + self.client.fail(text_type(e)) + self.results['actions'].append("Disable plugin %s" % self.parameters.plugin_name) + self.results['changed'] = True + else: + self.fail("Plugin not found: Plugin does not exist.") + + +def main(): + argument_spec = dict( + plugin_name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent', 'enable', 'disable']), + plugin_options=dict(type='dict', default={}), + debug=dict(type='bool', default=False), + force_remove=dict(type='bool', default=False), + enable_timeout=dict(type='int', default=0), + ) + client = AnsibleDockerClient( + argument_spec=argument_spec, + supports_check_mode=True, + min_docker_version='2.6.0', + min_docker_api_version='1.25', + ) + + try: + cm = DockerPluginManager(client) + client.module.exit_json(**cm.results) + except DockerException as e: + client.fail('An unexpected docker error occurred: {0}'.format(e), exception=traceback.format_exc()) + except RequestException as e: + client.fail('An unexpected requests error occurred when docker-py tried to talk to the docker daemon: {0}'.format(e), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/docker_plugin/aliases b/tests/integration/targets/docker_plugin/aliases new file mode 100644 index 00000000..02b78723 --- /dev/null +++ b/tests/integration/targets/docker_plugin/aliases @@ -0,0 +1,2 @@ +shippable/posix/group4 +destructive diff --git a/tests/integration/targets/docker_plugin/meta/main.yml b/tests/integration/targets/docker_plugin/meta/main.yml new file mode 100644 index 00000000..07da8c6d --- /dev/null +++ b/tests/integration/targets/docker_plugin/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_docker diff --git a/tests/integration/targets/docker_plugin/tasks/main.yaml b/tests/integration/targets/docker_plugin/tasks/main.yaml new file mode 100644 index 00000000..ae747404 --- /dev/null +++ b/tests/integration/targets/docker_plugin/tasks/main.yaml @@ -0,0 +1,24 @@ +- name: Create random name prefix + set_fact: + name_prefix: "cvmfs/overlay2-graphdriver" + plugin_names: [] + +- debug: + msg: "Using name prefix {{ name_prefix }}" + +- block: + - include_tasks: run-test.yml + with_fileglob: + - "tests/*.yml" + + always: + - name: "Make sure plugin is removed" + docker_plugin: + plugin_name: "{{ item }}" + state: absent + with_items: "{{ plugin_names }}" + + when: docker_py_version is version('1.10.0', '>=') and docker_api_version is version('1.20', '>=') + +- fail: msg="Too old docker / docker-py version to run docker_plugin tests!" + when: not(docker_py_version is version('1.10.0', '>=') and docker_api_version is version('1.20', '>=')) and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6) diff --git a/tests/integration/targets/docker_plugin/tasks/run-test.yml b/tests/integration/targets/docker_plugin/tasks/run-test.yml new file mode 100644 index 00000000..a2999370 --- /dev/null +++ b/tests/integration/targets/docker_plugin/tasks/run-test.yml @@ -0,0 +1,3 @@ +--- +- name: "Loading tasks from {{ item }}" + include_tasks: "{{ item }}" diff --git a/tests/integration/targets/docker_plugin/tasks/tests/basic.yml b/tests/integration/targets/docker_plugin/tasks/tests/basic.yml new file mode 100644 index 00000000..e94133d7 --- /dev/null +++ b/tests/integration/targets/docker_plugin/tasks/tests/basic.yml @@ -0,0 +1,80 @@ +--- +- name: Registering plugin name + set_fact: + plugin_name: "{{ name_prefix }}" + +- name: Registering container name + set_fact: + plugin_names: "{{ plugin_names + [plugin_name] }}" + +############ basic test ############ +#################################### + +- name: Create a plugin + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: present + register: create_1 + +- name: Create a plugin (Idempotent) + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: present + register: create_2 + +- name: Enable a plugin + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: enable + register: create_3 + +- name: Enable a plugin (Idempotent) + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: enable + register: create_4 + +- name: Disable a plugin + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: disable + register: absent_1 + +- name: Disable a plugin (Idempotent) + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: disable + register: absent_2 + +- name: Remove a plugin + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: absent + register: absent_3 + +- name: Remove a plugin (Idempotent) + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: absent + register: absent_4 + +- assert: + that: + - create_1 is changed + - create_2 is not changed + - create_3 is changed + - create_4 is not changed + - absent_1 is changed + - absent_2 is not changed + - absent_3 is changed + - absent_4 is not changed + +############ Plugin_Options ############ +######################################## +# Integration-Test with plugin_options is in TODO-List + +- name: Cleanup + docker_plugin: + plugin_name: "{{ plugin_name }}" + state: absent + force_remove: true