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

Scaffold a generic module plugin in an existing ansible collection using add subcommand. #365

Merged
merged 21 commits into from
Feb 26, 2025
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
25 changes: 25 additions & 0 deletions docs/installing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <plugin-type> <plugin-name> <collection-path>
```

#### 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`
16 changes: 16 additions & 0 deletions src/ansible_creator/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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}
13 changes: 13 additions & 0 deletions src/ansible_creator/subcommands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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, ...],
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
77 changes: 77 additions & 0 deletions tests/units/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down