diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 632411bd0d..1f7be1f81e 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1169,6 +1169,8 @@ files: maintainers: inetfuture mattupstate $modules/web_infrastructure/taiga_issue.py: maintainers: lekum + $tests/a_module.py: + maintainers: felixfontein ######################### tests/: labels: tests @@ -1195,6 +1197,7 @@ macros: module_utils: plugins/module_utils modules: plugins/modules terminals: plugins/terminal + tests: plugins/test team_ansible_core: team_aix: MorrisA bcoca d-little flynn1973 gforster kairoaraujo marvin-sinister mator molekuul ramooncamacho wtcross team_bsd: JoergFiedler MacLemon bcoca dch jasperla mekanix opoplawski overhacked tuxillo diff --git a/changelogs/fragments/a_module-test.yml b/changelogs/fragments/a_module-test.yml new file mode 100644 index 0000000000..4bcfdc068a --- /dev/null +++ b/changelogs/fragments/a_module-test.yml @@ -0,0 +1,3 @@ +add plugin.test: + - name: a_module + description: Check whether the given string refers to an available module or action plugin diff --git a/docs/docsite/extra-docs.yml b/docs/docsite/extra-docs.yml index 22ae7b58f5..83f533ec08 100644 --- a/docs/docsite/extra-docs.yml +++ b/docs/docsite/extra-docs.yml @@ -3,3 +3,4 @@ sections: - title: Guides toctree: - filter_guide + - test_guide diff --git a/docs/docsite/rst/test_guide.rst b/docs/docsite/rst/test_guide.rst new file mode 100644 index 0000000000..2df0ed04cd --- /dev/null +++ b/docs/docsite/rst/test_guide.rst @@ -0,0 +1,28 @@ +.. _ansible_collections.community.general.docsite.test_guide: + +community.general Test (Plugin) Guide +===================================== + +The :ref:`community.general collection ` offers currently one test plugin. + +.. contents:: Topics + +Feature Tests +------------- + +The ``a_module`` test allows to check whether a given string refers to an existing module or action plugin. This can be useful in roles, which can use this to ensure that required modules are present ahead of time. + +.. code-block:: yaml+jinja + + - name: Make sure that community.aws.route53 is available + assert: + that: + - > + 'community.aws.route53' is community.general.a_module + + - name: Make sure that community.general.does_not_exist is not a module or action plugin + assert: + that: + - "'community.general.does_not_exist' is not community.general.a_module" + +.. versionadded:: 4.0.0 diff --git a/plugins/test/a_module.py b/plugins/test/a_module.py new file mode 100644 index 0000000000..ad57cda792 --- /dev/null +++ b/plugins/test/a_module.py @@ -0,0 +1,33 @@ +# (c) 2021, Felix Fontein +# 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 + + +from ansible.plugins.loader import action_loader, module_loader + + +def a_module(term): + """ + Example: + - 'community.general.ufw' is community.general.a_module + - 'community.general.does_not_exist' is not community.general.a_module + """ + for loader in (action_loader, module_loader): + data = loader.find_plugin(term) + # Ansible 2.9 returns a tuple + if isinstance(data, tuple): + data = data[0] + if data is not None: + return True + return False + + +class TestModule(object): + ''' Ansible jinja2 tests ''' + + def tests(self): + return { + 'a_module': a_module, + } diff --git a/tests/integration/targets/test_a_module/aliases b/tests/integration/targets/test_a_module/aliases new file mode 100644 index 0000000000..b59832142f --- /dev/null +++ b/tests/integration/targets/test_a_module/aliases @@ -0,0 +1 @@ +shippable/posix/group3 diff --git a/tests/integration/targets/test_a_module/collections/ansible_collections/testns/testcoll/galaxy.yml b/tests/integration/targets/test_a_module/collections/ansible_collections/testns/testcoll/galaxy.yml new file mode 100644 index 0000000000..f66a8af4c9 --- /dev/null +++ b/tests/integration/targets/test_a_module/collections/ansible_collections/testns/testcoll/galaxy.yml @@ -0,0 +1,7 @@ +namespace: testns +name: testcoll +version: 0.0.1 +authors: + - Ansible (https://github.com/ansible) +description: null +tags: [community] diff --git a/tests/integration/targets/test_a_module/collections/ansible_collections/testns/testcoll/plugins/modules/collection_module.py b/tests/integration/targets/test_a_module/collections/ansible_collections/testns/testcoll/plugins/modules/collection_module.py new file mode 100644 index 0000000000..8db51c39a7 --- /dev/null +++ b/tests/integration/targets/test_a_module/collections/ansible_collections/testns/testcoll/plugins/modules/collection_module.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2021, Felix Fontein +# 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: collection_module +short_description: Test collection module +description: + - This is a test module in a local collection. +author: "Felix Fontein (@felixfontein)" +options: {} +''' + +EXAMPLES = ''' # ''' + +RETURN = ''' # ''' + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + AnsibleModule(argument_spec={}).exit_json() + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/test_a_module/library/local_module.py b/tests/integration/targets/test_a_module/library/local_module.py new file mode 100644 index 0000000000..424c127ced --- /dev/null +++ b/tests/integration/targets/test_a_module/library/local_module.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2021, Felix Fontein +# 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: local_module +short_description: Test local module +description: + - This is a test module locally next to a playbook. +author: "Felix Fontein (@felixfontein)" +options: {} +''' + +EXAMPLES = ''' # ''' + +RETURN = ''' # ''' + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + AnsibleModule(argument_spec={}).exit_json() + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/test_a_module/runme.sh b/tests/integration/targets/test_a_module/runme.sh new file mode 100755 index 0000000000..8ce47c86d0 --- /dev/null +++ b/tests/integration/targets/test_a_module/runme.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -eux + +export ANSIBLE_TEST_PREFER_VENV=1 # see https://github.com/ansible/ansible/pull/73000#issuecomment-757012395; can be removed once Ansible 2.9 and ansible-base 2.10 support has been dropped +source virtualenv.sh + +# The collection loader ignores paths which have more than one ansible_collections in it. +# That's why we have to copy this directory to a temporary place and run the test there. + +# Create temporary folder +TEMPDIR=$(mktemp -d) +trap '{ rm -rf ${TEMPDIR}; }' EXIT + +cp -r . "${TEMPDIR}" +cd "${TEMPDIR}" + +ansible-playbook runme.yml "$@" diff --git a/tests/integration/targets/test_a_module/runme.yml b/tests/integration/targets/test_a_module/runme.yml new file mode 100644 index 0000000000..9f7618341a --- /dev/null +++ b/tests/integration/targets/test_a_module/runme.yml @@ -0,0 +1,25 @@ +- hosts: localhost + tasks: + - name: Test a_module + assert: + that: + # Modules/actions that do not exist + - "'foo_bar' is not community.general.a_module" + - "'foo.bar.baz' is not community.general.a_module" + # Short name and FQCN for builtin and other collections + - "'file' is community.general.a_module" + - "'set_fact' is community.general.a_module" + - "'ansible.builtin.file' is community.general.a_module" + - "'ansible.builtin.set_fact' is community.general.a_module" + - "'ansible.builtin.foo_bar' is not community.general.a_module" + - "'community.crypto.acme_certificate' is community.general.a_module" + - "'community.crypto.openssl_privatekey_pipe' is community.general.a_module" + - "'community.crypto.foo_bar' is not community.general.a_module" + # Modules from this collection (that exist or not) + - "'community.general.ufw' is community.general.a_module" + - "'community.general.foooo_really_does_not_exist' is not community.general.a_module" + # Local module + - "'local_module' is community.general.a_module" + # Local collection module (that exist or not) + - "'testns.testcoll.collection_module' is community.general.a_module" + - "'testns.testcoll.foobar' is not community.general.a_module"