diff --git a/docs/installing.md b/docs/installing.md index d866661..02baa14 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -330,3 +330,28 @@ $ ansible-creator add resource devfile /home/user/..path/to/your/existing_projec ``` This command will scaffold the devfile.yaml file at `/home/user/..path/to/your/existing_project` + +### Add support to scaffold plugins in an existing ansible collection + +The `add plugin` command enables you to add a plugin to an existing collection project. Use the following command template: + +```console +$ ansible-creator add plugin +``` + +#### Positional Arguments + +| Parameter | Description | +| --------- | -------------------------------------------------------------- | +| action | Add an action plugin to an existing Ansible Collection. | +| filter | Add a filter plugin to an existing Ansible Collection. | +| lookup | Add a lookup plugin to an existing Ansible Collection. | +| module | Add a generic module plugin to an existing Ansible Collection. | + +#### Example + +```console +$ ansible-creator add plugin module test_plugin /home/user/..path/to/your/existing_project +``` + +This command will scaffold a generic module plugin at `/home/user/..path/to/your/existing_project` diff --git a/src/ansible_creator/arg_parser.py b/src/ansible_creator/arg_parser.py index 2781712..3e3fb9f 100644 --- a/src/ansible_creator/arg_parser.py +++ b/src/ansible_creator/arg_parser.py @@ -353,6 +353,7 @@ def _add_plugin(self, subparser: SubParser[ArgumentParser]) -> None: self._add_plugin_action(subparser=subparser) self._add_plugin_filter(subparser=subparser) self._add_plugin_lookup(subparser=subparser) + self._add_plugin_module(subparser=subparser) def _add_plugin_action(self, subparser: SubParser[ArgumentParser]) -> None: """Add an action plugin to an existing Ansible collection project. @@ -399,6 +400,21 @@ def _add_plugin_lookup(self, subparser: SubParser[ArgumentParser]) -> None: self._add_overwrite(parser) self._add_args_plugin_common(parser) + def _add_plugin_module(self, subparser: SubParser[ArgumentParser]) -> None: + """Add a module plugin to an existing Ansible collection project. + + Args: + subparser: The subparser to add module plugin to + """ + parser = subparser.add_parser( + "module", + help="Add a module plugin to an existing Ansible collection.", + formatter_class=CustomHelpFormatter, + ) + self._add_args_common(parser) + self._add_overwrite(parser) + self._add_args_plugin_common(parser) + def _add_overwrite(self, parser: ArgumentParser) -> None: """Add overwrite and no-overwrite arguments to the parser. diff --git a/src/ansible_creator/resources/collection_project/plugins/sample_module/__init__.py.j2 b/src/ansible_creator/resources/collection_project/plugins/sample_module/__init__.py.j2 new file mode 100644 index 0000000..e69de29 diff --git a/src/ansible_creator/resources/collection_project/plugins/sample_module/hello_world.py.j2 b/src/ansible_creator/resources/collection_project/plugins/sample_module/hello_world.py.j2 new file mode 100644 index 0000000..dc3afa0 --- /dev/null +++ b/src/ansible_creator/resources/collection_project/plugins/sample_module/hello_world.py.j2 @@ -0,0 +1,65 @@ +{# module_plugin_template.j2 #} +{%- set module_name = plugin_name | default("hello_world") -%} +{%- set author = author | default("Your Name (@username)") -%} +{%- set description = description | default("A custom module plugin for Ansible.") -%} +{%- set license = license | default("GPL-3.0-or-later") -%} +# {{ module_name }}.py - {{ description }} +# Author: {{ author }} +# License: {{ license }} + +from __future__ import absolute_import, annotations, division, print_function + + +__metaclass__ = type # pylint: disable=C0103 + +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from typing import Callable + + +DOCUMENTATION = """ + name: {{ module_name }} + author: {{ author }} + version_added: "1.0.0" + short_description: {{ description }} + description: + - This is a demo module plugin designed to return Hello message. + options: + name: + description: Value specified here is appended to the Hello message. + type: str +""" + +EXAMPLES = """ +# {{ module_name }} module example +{% raw %} +- name: Display a hello message + ansible.builtin.debug: + msg: "{{ 'ansible-creator' {%- endraw %} | {{ module_name }} }}" +""" + + +def _hello_world(name: str) -> str: + """Returns Hello message. + + Args: + name: The name to greet. + + Returns: + str: The greeting message. + """ + return "Hello, " + name + + +class SampleModule: + """module plugin.""" + + def modules(self) -> dict[str, Callable[[str], str]]: + """Map module plugin names to their functions. + + Returns: + dict: The module plugin functions. + """ + return {"{{ module_name }}": _hello_world} diff --git a/src/ansible_creator/subcommands/add.py b/src/ansible_creator/subcommands/add.py index 7fdf505..57b5c52 100644 --- a/src/ansible_creator/subcommands/add.py +++ b/src/ansible_creator/subcommands/add.py @@ -209,6 +209,11 @@ def _plugin_scaffold(self, plugin_path: Path) -> None: template_data = self._get_plugin_template_data() self._perform_lookup_plugin_scaffold(template_data, plugin_path) + elif self._plugin_type == "module": + template_data = self._get_plugin_template_data() + plugin_path = self._add_path / "plugins" / "sample_module" + plugin_path.mkdir(parents=True, exist_ok=True) + self._perform_module_plugin_scaffold(template_data, plugin_path) else: msg = f"Unsupported plugin type: {self._plugin_type}" raise CreatorError(msg) @@ -243,6 +248,14 @@ def _perform_lookup_plugin_scaffold( resources = (f"collection_project.plugins.{self._plugin_type}",) self._perform_plugin_scaffold(resources, template_data, plugin_path) + def _perform_module_plugin_scaffold( + self, + template_data: TemplateData, + plugin_path: Path, + ) -> None: + resources = ("collection_project.plugins.sample_module",) + self._perform_plugin_scaffold(resources, template_data, plugin_path) + def _perform_plugin_scaffold( self, resources: tuple[str, ...], diff --git a/tests/fixtures/collection/testorg/testcol/plugins/sample_module/__init__.py b/tests/fixtures/collection/testorg/testcol/plugins/sample_module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/collection/testorg/testcol/plugins/sample_module/hello_world.py b/tests/fixtures/collection/testorg/testcol/plugins/sample_module/hello_world.py new file mode 100644 index 0000000..949aa2d --- /dev/null +++ b/tests/fixtures/collection/testorg/testcol/plugins/sample_module/hello_world.py @@ -0,0 +1,60 @@ +# hello_world.py - A custom module plugin for Ansible. +# Author: Your Name (@username) +# License: GPL-3.0-or-later + +from __future__ import absolute_import, annotations, division, print_function + + +__metaclass__ = type # pylint: disable=C0103 + +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from typing import Callable + + +DOCUMENTATION = """ + name: hello_world + author: Your Name (@username) + version_added: "1.0.0" + short_description: A custom module plugin for Ansible. + description: + - This is a demo module plugin designed to return Hello message. + options: + name: + description: Value specified here is appended to the Hello message. + type: str +""" + +EXAMPLES = """ +# hello_world module example + +- name: Display a hello message + ansible.builtin.debug: + msg: "{{ 'ansible-creator' | hello_world }}" +""" + + +def _hello_world(name: str) -> str: + """Returns Hello message. + + Args: + name: The name to greet. + + Returns: + str: The greeting message. + """ + return "Hello, " + name + + +class SampleModule: + """module plugin.""" + + def modules(self) -> dict[str, Callable[[str], str]]: + """Map module plugin names to their functions. + + Returns: + dict: The module plugin functions. + """ + return {"hello_world": _hello_world} diff --git a/tests/units/test_add.py b/tests/units/test_add.py index eb24411..468283a 100644 --- a/tests/units/test_add.py +++ b/tests/units/test_add.py @@ -675,6 +675,83 @@ def mock_update_galaxy_dependency() -> None: assert cmp_result1, cmp_result2 +def test_run_success_add_plugin_module( + capsys: pytest.CaptureFixture[str], + tmp_path: Path, + cli_args: ConfigDict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Add.run(). + + Successfully add plugin to path + + Args: + capsys: Pytest fixture to capture stdout and stderr. + tmp_path: Temporary directory path. + cli_args: Dictionary, partial Add class object. + monkeypatch: Pytest monkeypatch fixture. + """ + cli_args["plugin_type"] = "module" + add = Add( + Config(**cli_args), + ) + + # Mock the "_check_collection_path" method + def mock_check_collection_path() -> None: + """Mock function to skip checking collection path.""" + + monkeypatch.setattr( + Add, + "_check_collection_path", + staticmethod(mock_check_collection_path), + ) + add.run() + result = capsys.readouterr().out + assert re.search("Note: Module plugin added to", result) is not None + + expected_file = tmp_path / "plugins" / "sample_module" / "hello_world.py" + effective_file = ( + FIXTURES_DIR + / "collection" + / "testorg" + / "testcol" + / "plugins" + / "sample_module" + / "hello_world.py" + ) + cmp_result = cmp(expected_file, effective_file, shallow=False) + assert cmp_result + + conflict_file = tmp_path / "plugins" / "sample_module" / "hello_world.py" + conflict_file.write_text("Author: Your Name") + + # expect a CreatorError when the response to overwrite is no. + monkeypatch.setattr("builtins.input", lambda _: "n") + fail_msg = ( + "The destination directory contains files that will be overwritten." + " Please re-run ansible-creator with --overwrite to continue." + ) + with pytest.raises( + CreatorError, + match=fail_msg, + ): + add.run() + + # expect a warning followed by module plugin addition msg + # when response to overwrite is yes. + monkeypatch.setattr("builtins.input", lambda _: "y") + add.run() + result = capsys.readouterr().out + assert ( + re.search( + "already exists", + result, + ) + is not None + ), result + assert re.search("Note: Module plugin added to", result) is not None + + def test_run_error_plugin_no_overwrite( capsys: pytest.CaptureFixture[str], tmp_path: Path,