Skip to content

Commit 7ca0789

Browse files
authored
Scaffold action plugin through add subcommand (#348)
* Scaffold action plugin through add subcommand * Make seperate functions to collect and store resources based on plugin_type * Changes for adding module path * Fix logic for module scaffolding as part of adding action plugin * Chages in the module template to fix the error: DOCUMENTATION.module- not a valid value for dictionary value quit exit * fix the arg spec validation related errors in plugin template * add a function to update galaxy dependency for action plugin * logic cleanup * dependency key update and add checks for it * move the update_galaxy_dependency func back to add.py and initial tests for action plugin * test update_galaxy_dependency function * correct the debug message * author name change in the module documentation
1 parent c38f225 commit 7ca0789

File tree

9 files changed

+486
-26
lines changed

9 files changed

+486
-26
lines changed

.config/dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ addopts
22
antsibull
33
argcomplete
44
argnames
5+
argspec
56
argvalues
67
capsys
78
chakarborty

src/ansible_creator/arg_parser.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@
3434

3535
MIN_COLLECTION_NAME_LEN = 2
3636

37-
COMING_SOON = (
38-
"add resource role",
39-
"add plugin action",
40-
)
37+
COMING_SOON = ("add resource role",)
4138

4239

4340
class Parser:
@@ -369,6 +366,7 @@ def _add_plugin_action(self, subparser: SubParser[ArgumentParser]) -> None:
369366
formatter_class=CustomHelpFormatter,
370367
)
371368
self._add_args_common(parser)
369+
self._add_overwrite(parser)
372370
self._add_args_plugin_common(parser)
373371

374372
def _add_plugin_filter(self, subparser: SubParser[ArgumentParser]) -> None:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
{# action_plugin_template.j2 #}
2+
{%- set action_name = plugin_name | default("hello_world") -%}
3+
{%- set author = author | default("Your Name") -%}
4+
{%- set description = description | default("A custom action plugin for Ansible.") -%}
5+
{%- set license = license | default("GPL-3.0-or-later") -%}
6+
# {{ action_name }}.py - {{ description }}
7+
# Author: {{ author }}
8+
# License: {{ license }}
9+
# pylint: disable=E0401
10+
11+
from __future__ import absolute_import, annotations, division, print_function
12+
13+
__metaclass__ = type # pylint: disable=C0103
14+
15+
from typing import TYPE_CHECKING
16+
from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( # type: ignore
17+
AnsibleArgSpecValidator,
18+
)
19+
from ansible_collections.ansible.utils.plugins.modules.fact_diff import DOCUMENTATION # type: ignore
20+
from ansible.plugins.action import ActionBase # type: ignore
21+
22+
23+
if TYPE_CHECKING:
24+
from typing import Optional, Dict, Any
25+
26+
27+
class ActionModule(ActionBase): # type: ignore[misc]
28+
"""
29+
Custom Ansible action plugin: {{ action_name }}
30+
A custom action plugin for Ansible.
31+
"""
32+
33+
def _check_argspec(self, result: dict[str, Any]) -> None:
34+
aav = AnsibleArgSpecValidator(
35+
data=self._task.args,
36+
schema=DOCUMENTATION,
37+
schema_format="doc",
38+
name=self._task.action,
39+
)
40+
valid, errors, self._task.args = aav.validate()
41+
if not valid:
42+
result["failed"] = True
43+
result["msg"] = errors
44+
45+
def run(
46+
self,
47+
tmp: Optional[str] = None,
48+
task_vars: Optional[Dict[str, Any]] = None,
49+
) -> Dict[str, Any]:
50+
"""
51+
Executes the action plugin.
52+
53+
Args:
54+
tmp: Temporary path provided by Ansible for the module execution. Defaults to None.
55+
task_vars: Dictionary of task variables available to the plugin. Defaults to None.
56+
57+
Returns:
58+
dict: Result of the action plugin execution.
59+
"""
60+
# Get the task arguments
61+
if task_vars is None:
62+
task_vars = {}
63+
result = {}
64+
warnings: list[str] = []
65+
66+
# Example processing logic - Replace this with actual action code
67+
result = super(ActionModule, self).run(tmp, task_vars)
68+
self._check_argspec(result)
69+
70+
# Copy the task arguments
71+
module_args = self._task.args.copy()
72+
73+
prefix = module_args.get("prefix", "DefaultPrefix")
74+
message = module_args.get("msg", "No message provided")
75+
module_args["msg"] = f"{prefix}: {message}"
76+
77+
result.update(
78+
self._execute_module(
79+
module_name="debug",
80+
module_args=module_args,
81+
task_vars=task_vars,
82+
tmp=tmp,
83+
),
84+
)
85+
86+
if warnings:
87+
if "warnings" in result:
88+
result["warnings"].extend(warnings)
89+
else:
90+
result["warnings"] = warnings
91+
return result
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{%- set module_name = plugin_name | default("hello_world") -%}
2+
{%- set author = author | default("Your Name (@username)") -%}
3+
# {{ module_name }}.py
4+
# GNU General Public License v3.0+
5+
6+
DOCUMENTATION = """
7+
module: {{ module_name }}
8+
author: {{ author }}
9+
version_added: "1.0.0"
10+
short_description: A custom action plugin for Ansible.
11+
description:
12+
- This is a custom action plugin to provide action functionality.
13+
options:
14+
prefix:
15+
description:
16+
- A string that is added as a prefix to the message passed to the module.
17+
type: str
18+
msg:
19+
description: The message to display in the output.
20+
type: str
21+
with_prefix:
22+
description:
23+
- A boolean flag indicating whether to include the prefix in the message.
24+
type: bool
25+
notes:
26+
- This is a scaffold template. Customize the plugin to fit your needs.
27+
"""
28+
29+
EXAMPLES = """
30+
- name: Example Action Plugin
31+
hosts: localhost
32+
tasks:
33+
- name: Example {{ module_name }} plugin
34+
with_prefix:
35+
prefix: "Hello, World"
36+
msg: "Ansible!"
37+
"""

src/ansible_creator/subcommands/add.py

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from pathlib import Path
88
from typing import TYPE_CHECKING
99

10+
import yaml
11+
1012
from ansible_creator.constants import GLOBAL_TEMPLATE_VARS
1113
from ansible_creator.exceptions import CreatorError
1214
from ansible_creator.templar import Templar
@@ -92,13 +94,33 @@ def unique_name_in_devfile(self) -> str:
9294
final_uuid = str(uuid.uuid4())[:8]
9395
return f"{final_name}-{final_uuid}"
9496

97+
def update_galaxy_dependency(self) -> None:
98+
"""Update galaxy.yml file with the required dependency."""
99+
galaxy_file = self._add_path / "galaxy.yml"
100+
101+
# Load the galaxy.yml file
102+
with galaxy_file.open("r", encoding="utf-8") as file:
103+
data = yaml.safe_load(file)
104+
105+
# Ensure the dependencies key exists
106+
if "dependencies" not in data:
107+
data["dependencies"] = {"ansible.utils": "*"}
108+
109+
# Empty dependencies key or dependencies key without ansible.utils
110+
elif not data["dependencies"] or "ansible.utils" not in data["dependencies"]:
111+
data["dependencies"]["ansible.utils"] = "*"
112+
113+
# Save the updated YAML back to the file
114+
with galaxy_file.open("w", encoding="utf-8") as file:
115+
yaml.dump(data, file, sort_keys=False)
116+
95117
def _resource_scaffold(self) -> None:
96118
"""Scaffold the specified resource file based on the resource type.
97119
98120
Raises:
99121
CreatorError: If unsupported resource type is given.
100122
"""
101-
self.output.debug(f"Started copying {self._project} resource to destination")
123+
self.output.debug(f"Started adding {self._resource_type} to destination")
102124

103125
# Call the appropriate scaffolding function based on the resource type
104126
if self._resource_type == "devfile":
@@ -171,22 +193,66 @@ def _plugin_scaffold(self, plugin_path: Path) -> None:
171193
Raises:
172194
CreatorError: If unsupported plugin type is given.
173195
"""
174-
self.output.debug(f"Started copying {self._project} plugin to destination")
196+
self.output.debug(f"Started adding {self._plugin_type} plugin to destination")
175197

176198
# Call the appropriate scaffolding function based on the plugin type
177-
if self._plugin_type in ("lookup", "filter"):
199+
if self._plugin_type == "action":
200+
self.update_galaxy_dependency()
178201
template_data = self._get_plugin_template_data()
202+
self._perform_action_plugin_scaffold(template_data, plugin_path)
203+
204+
elif self._plugin_type == "filter":
205+
template_data = self._get_plugin_template_data()
206+
self._perform_filter_plugin_scaffold(template_data, plugin_path)
207+
208+
elif self._plugin_type == "lookup":
209+
template_data = self._get_plugin_template_data()
210+
self._perform_lookup_plugin_scaffold(template_data, plugin_path)
179211

180212
else:
181213
msg = f"Unsupported plugin type: {self._plugin_type}"
182214
raise CreatorError(msg)
183215

184-
self._perform_plugin_scaffold(template_data, plugin_path)
216+
def _perform_action_plugin_scaffold(
217+
self,
218+
template_data: TemplateData,
219+
plugin_path: Path,
220+
) -> None:
221+
resources = (
222+
f"collection_project.plugins.{self._plugin_type}",
223+
"collection_project.plugins.modules",
224+
)
225+
module_path = self._add_path / "plugins" / "modules"
226+
module_path.mkdir(parents=True, exist_ok=True)
227+
final_plugin_path = [plugin_path, module_path]
228+
self._perform_plugin_scaffold(resources, template_data, final_plugin_path)
229+
230+
def _perform_filter_plugin_scaffold(
231+
self,
232+
template_data: TemplateData,
233+
plugin_path: Path,
234+
) -> None:
235+
resources = (f"collection_project.plugins.{self._plugin_type}",)
236+
self._perform_plugin_scaffold(resources, template_data, plugin_path)
185237

186-
def _perform_plugin_scaffold(self, template_data: TemplateData, plugin_path: Path) -> None:
238+
def _perform_lookup_plugin_scaffold(
239+
self,
240+
template_data: TemplateData,
241+
plugin_path: Path,
242+
) -> None:
243+
resources = (f"collection_project.plugins.{self._plugin_type}",)
244+
self._perform_plugin_scaffold(resources, template_data, plugin_path)
245+
246+
def _perform_plugin_scaffold(
247+
self,
248+
resources: tuple[str, ...],
249+
template_data: TemplateData,
250+
plugin_path: Path | list[Path],
251+
) -> None:
187252
"""Perform the actual scaffolding process using the provided template data.
188253
189254
Args:
255+
resources: Tuple of resources.
190256
template_data: TemplateData
191257
plugin_path: Path where the plugin will be scaffolded.
192258
@@ -195,7 +261,7 @@ def _perform_plugin_scaffold(self, template_data: TemplateData, plugin_path: Pat
195261
destination directory contains files that will be overwritten.
196262
"""
197263
walker = Walker(
198-
resources=(f"collection_project.plugins.{self._plugin_type}",),
264+
resources=resources,
199265
resource_id=self._plugin_id,
200266
dest=plugin_path,
201267
output=self.output,
@@ -213,6 +279,10 @@ def _perform_plugin_scaffold(self, template_data: TemplateData, plugin_path: Pat
213279
)
214280
raise CreatorError(msg)
215281

282+
# This check is for action plugins (having module file as an additional path)
283+
if isinstance(plugin_path, list):
284+
plugin_path = plugin_path[0]
285+
216286
if not paths.has_conflicts() or self._force or self._overwrite:
217287
copier.copy_containers(paths)
218288
self.output.note(f"{self._plugin_type.capitalize()} plugin added to {plugin_path}")
@@ -270,10 +340,10 @@ def _get_devcontainer_template_data(self) -> TemplateData:
270340
)
271341

272342
def _get_plugin_template_data(self) -> TemplateData:
273-
"""Get the template data for lookup plugin.
343+
"""Get the template data for plugin.
274344
275345
Returns:
276-
TemplateData: Data required for templating the lookup plugin.
346+
TemplateData: Data required for templating the plugin.
277347
"""
278348
return TemplateData(
279349
plugin_type=self._plugin_type,
@@ -282,10 +352,10 @@ def _get_plugin_template_data(self) -> TemplateData:
282352
)
283353

284354
def _get_ee_template_data(self) -> TemplateData:
285-
"""Get the template data for lookup plugin.
355+
"""Get the template data for plugin.
286356
287357
Returns:
288-
TemplateData: Data required for templating the lookup plugin.
358+
TemplateData: Data required for templating the plugin.
289359
"""
290360
return TemplateData(
291361
resource_type=self._resource_type,

0 commit comments

Comments
 (0)