Skip to content

Commit

Permalink
Scaffold a generic module plugin in an existing ansible collection us…
Browse files Browse the repository at this point in the history
…ing add subcommand. (#365)

* Scaffold generic module through add subcommand

* Removed add_resource_module function

* chore: auto fixes from pre-commit.com hooks

* Changed Module dir to Sample_Module AND Updated test_add.py

* chore: auto fixes from pre-commit.com hooks

* updated test_add.py

* Added sample_module dir under test/fixtures

* updated hello_world.py under tests/fixtures/plugins/sample_module

* updated test_add.py

* Updated test_add.py and arg_parser.py

* Updated docs

* changed launch.json

* chore: auto fixes from pre-commit.com hooks

* Reverted launch.json changes

* Updated docs

* Fixed docs

* Fixed docs, args_parser.py and add.py

* chore: auto fixes from pre-commit.com hooks

* Updated add.py

* Update add.py

* Reverted changes in add.py

---------

Co-authored-by: Shashank Venkat <shvenkat>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
shvenkat-rh and pre-commit-ci[bot] authored Feb 26, 2025
1 parent edd5012 commit f8b098d
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 0 deletions.
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
Empty file.
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
Empty file.
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

0 comments on commit f8b098d

Please sign in to comment.