Skip to content

Commit f8b098d

Browse files
Scaffold a generic module plugin in an existing ansible collection using 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>
1 parent edd5012 commit f8b098d

File tree

8 files changed

+256
-0
lines changed

8 files changed

+256
-0
lines changed

docs/installing.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,28 @@ $ ansible-creator add resource devfile /home/user/..path/to/your/existing_projec
330330
```
331331

332332
This command will scaffold the devfile.yaml file at `/home/user/..path/to/your/existing_project`
333+
334+
### Add support to scaffold plugins in an existing ansible collection
335+
336+
The `add plugin` command enables you to add a plugin to an existing collection project. Use the following command template:
337+
338+
```console
339+
$ ansible-creator add plugin <plugin-type> <plugin-name> <collection-path>
340+
```
341+
342+
#### Positional Arguments
343+
344+
| Parameter | Description |
345+
| --------- | -------------------------------------------------------------- |
346+
| action | Add an action plugin to an existing Ansible Collection. |
347+
| filter | Add a filter plugin to an existing Ansible Collection. |
348+
| lookup | Add a lookup plugin to an existing Ansible Collection. |
349+
| module | Add a generic module plugin to an existing Ansible Collection. |
350+
351+
#### Example
352+
353+
```console
354+
$ ansible-creator add plugin module test_plugin /home/user/..path/to/your/existing_project
355+
```
356+
357+
This command will scaffold a generic module plugin at `/home/user/..path/to/your/existing_project`

src/ansible_creator/arg_parser.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ def _add_plugin(self, subparser: SubParser[ArgumentParser]) -> None:
353353
self._add_plugin_action(subparser=subparser)
354354
self._add_plugin_filter(subparser=subparser)
355355
self._add_plugin_lookup(subparser=subparser)
356+
self._add_plugin_module(subparser=subparser)
356357

357358
def _add_plugin_action(self, subparser: SubParser[ArgumentParser]) -> None:
358359
"""Add an action plugin to an existing Ansible collection project.
@@ -399,6 +400,21 @@ def _add_plugin_lookup(self, subparser: SubParser[ArgumentParser]) -> None:
399400
self._add_overwrite(parser)
400401
self._add_args_plugin_common(parser)
401402

403+
def _add_plugin_module(self, subparser: SubParser[ArgumentParser]) -> None:
404+
"""Add a module plugin to an existing Ansible collection project.
405+
406+
Args:
407+
subparser: The subparser to add module plugin to
408+
"""
409+
parser = subparser.add_parser(
410+
"module",
411+
help="Add a module plugin to an existing Ansible collection.",
412+
formatter_class=CustomHelpFormatter,
413+
)
414+
self._add_args_common(parser)
415+
self._add_overwrite(parser)
416+
self._add_args_plugin_common(parser)
417+
402418
def _add_overwrite(self, parser: ArgumentParser) -> None:
403419
"""Add overwrite and no-overwrite arguments to the parser.
404420

src/ansible_creator/resources/collection_project/plugins/sample_module/__init__.py.j2

Whitespace-only changes.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{# module_plugin_template.j2 #}
2+
{%- set module_name = plugin_name | default("hello_world") -%}
3+
{%- set author = author | default("Your Name (@username)") -%}
4+
{%- set description = description | default("A custom module plugin for Ansible.") -%}
5+
{%- set license = license | default("GPL-3.0-or-later") -%}
6+
# {{ module_name }}.py - {{ description }}
7+
# Author: {{ author }}
8+
# License: {{ license }}
9+
10+
from __future__ import absolute_import, annotations, division, print_function
11+
12+
13+
__metaclass__ = type # pylint: disable=C0103
14+
15+
from typing import TYPE_CHECKING
16+
17+
18+
if TYPE_CHECKING:
19+
from typing import Callable
20+
21+
22+
DOCUMENTATION = """
23+
name: {{ module_name }}
24+
author: {{ author }}
25+
version_added: "1.0.0"
26+
short_description: {{ description }}
27+
description:
28+
- This is a demo module plugin designed to return Hello message.
29+
options:
30+
name:
31+
description: Value specified here is appended to the Hello message.
32+
type: str
33+
"""
34+
35+
EXAMPLES = """
36+
# {{ module_name }} module example
37+
{% raw %}
38+
- name: Display a hello message
39+
ansible.builtin.debug:
40+
msg: "{{ 'ansible-creator' {%- endraw %} | {{ module_name }} }}"
41+
"""
42+
43+
44+
def _hello_world(name: str) -> str:
45+
"""Returns Hello message.
46+
47+
Args:
48+
name: The name to greet.
49+
50+
Returns:
51+
str: The greeting message.
52+
"""
53+
return "Hello, " + name
54+
55+
56+
class SampleModule:
57+
"""module plugin."""
58+
59+
def modules(self) -> dict[str, Callable[[str], str]]:
60+
"""Map module plugin names to their functions.
61+
62+
Returns:
63+
dict: The module plugin functions.
64+
"""
65+
return {"{{ module_name }}": _hello_world}

src/ansible_creator/subcommands/add.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ def _plugin_scaffold(self, plugin_path: Path) -> None:
209209
template_data = self._get_plugin_template_data()
210210
self._perform_lookup_plugin_scaffold(template_data, plugin_path)
211211

212+
elif self._plugin_type == "module":
213+
template_data = self._get_plugin_template_data()
214+
plugin_path = self._add_path / "plugins" / "sample_module"
215+
plugin_path.mkdir(parents=True, exist_ok=True)
216+
self._perform_module_plugin_scaffold(template_data, plugin_path)
212217
else:
213218
msg = f"Unsupported plugin type: {self._plugin_type}"
214219
raise CreatorError(msg)
@@ -243,6 +248,14 @@ def _perform_lookup_plugin_scaffold(
243248
resources = (f"collection_project.plugins.{self._plugin_type}",)
244249
self._perform_plugin_scaffold(resources, template_data, plugin_path)
245250

251+
def _perform_module_plugin_scaffold(
252+
self,
253+
template_data: TemplateData,
254+
plugin_path: Path,
255+
) -> None:
256+
resources = ("collection_project.plugins.sample_module",)
257+
self._perform_plugin_scaffold(resources, template_data, plugin_path)
258+
246259
def _perform_plugin_scaffold(
247260
self,
248261
resources: tuple[str, ...],

tests/fixtures/collection/testorg/testcol/plugins/sample_module/__init__.py

Whitespace-only changes.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# hello_world.py - A custom module plugin for Ansible.
2+
# Author: Your Name (@username)
3+
# License: GPL-3.0-or-later
4+
5+
from __future__ import absolute_import, annotations, division, print_function
6+
7+
8+
__metaclass__ = type # pylint: disable=C0103
9+
10+
from typing import TYPE_CHECKING
11+
12+
13+
if TYPE_CHECKING:
14+
from typing import Callable
15+
16+
17+
DOCUMENTATION = """
18+
name: hello_world
19+
author: Your Name (@username)
20+
version_added: "1.0.0"
21+
short_description: A custom module plugin for Ansible.
22+
description:
23+
- This is a demo module plugin designed to return Hello message.
24+
options:
25+
name:
26+
description: Value specified here is appended to the Hello message.
27+
type: str
28+
"""
29+
30+
EXAMPLES = """
31+
# hello_world module example
32+
33+
- name: Display a hello message
34+
ansible.builtin.debug:
35+
msg: "{{ 'ansible-creator' | hello_world }}"
36+
"""
37+
38+
39+
def _hello_world(name: str) -> str:
40+
"""Returns Hello message.
41+
42+
Args:
43+
name: The name to greet.
44+
45+
Returns:
46+
str: The greeting message.
47+
"""
48+
return "Hello, " + name
49+
50+
51+
class SampleModule:
52+
"""module plugin."""
53+
54+
def modules(self) -> dict[str, Callable[[str], str]]:
55+
"""Map module plugin names to their functions.
56+
57+
Returns:
58+
dict: The module plugin functions.
59+
"""
60+
return {"hello_world": _hello_world}

tests/units/test_add.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,83 @@ def mock_update_galaxy_dependency() -> None:
675675
assert cmp_result1, cmp_result2
676676

677677

678+
def test_run_success_add_plugin_module(
679+
capsys: pytest.CaptureFixture[str],
680+
tmp_path: Path,
681+
cli_args: ConfigDict,
682+
monkeypatch: pytest.MonkeyPatch,
683+
) -> None:
684+
"""Test Add.run().
685+
686+
Successfully add plugin to path
687+
688+
Args:
689+
capsys: Pytest fixture to capture stdout and stderr.
690+
tmp_path: Temporary directory path.
691+
cli_args: Dictionary, partial Add class object.
692+
monkeypatch: Pytest monkeypatch fixture.
693+
"""
694+
cli_args["plugin_type"] = "module"
695+
add = Add(
696+
Config(**cli_args),
697+
)
698+
699+
# Mock the "_check_collection_path" method
700+
def mock_check_collection_path() -> None:
701+
"""Mock function to skip checking collection path."""
702+
703+
monkeypatch.setattr(
704+
Add,
705+
"_check_collection_path",
706+
staticmethod(mock_check_collection_path),
707+
)
708+
add.run()
709+
result = capsys.readouterr().out
710+
assert re.search("Note: Module plugin added to", result) is not None
711+
712+
expected_file = tmp_path / "plugins" / "sample_module" / "hello_world.py"
713+
effective_file = (
714+
FIXTURES_DIR
715+
/ "collection"
716+
/ "testorg"
717+
/ "testcol"
718+
/ "plugins"
719+
/ "sample_module"
720+
/ "hello_world.py"
721+
)
722+
cmp_result = cmp(expected_file, effective_file, shallow=False)
723+
assert cmp_result
724+
725+
conflict_file = tmp_path / "plugins" / "sample_module" / "hello_world.py"
726+
conflict_file.write_text("Author: Your Name")
727+
728+
# expect a CreatorError when the response to overwrite is no.
729+
monkeypatch.setattr("builtins.input", lambda _: "n")
730+
fail_msg = (
731+
"The destination directory contains files that will be overwritten."
732+
" Please re-run ansible-creator with --overwrite to continue."
733+
)
734+
with pytest.raises(
735+
CreatorError,
736+
match=fail_msg,
737+
):
738+
add.run()
739+
740+
# expect a warning followed by module plugin addition msg
741+
# when response to overwrite is yes.
742+
monkeypatch.setattr("builtins.input", lambda _: "y")
743+
add.run()
744+
result = capsys.readouterr().out
745+
assert (
746+
re.search(
747+
"already exists",
748+
result,
749+
)
750+
is not None
751+
), result
752+
assert re.search("Note: Module plugin added to", result) is not None
753+
754+
678755
def test_run_error_plugin_no_overwrite(
679756
capsys: pytest.CaptureFixture[str],
680757
tmp_path: Path,

0 commit comments

Comments
 (0)