Skip to content

Commit

Permalink
Scaffold action plugin through add subcommand (#348)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shatakshiiii authored Jan 15, 2025
1 parent c38f225 commit 7ca0789
Show file tree
Hide file tree
Showing 9 changed files with 486 additions and 26 deletions.
1 change: 1 addition & 0 deletions .config/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ addopts
antsibull
argcomplete
argnames
argspec
argvalues
capsys
chakarborty
Expand Down
6 changes: 2 additions & 4 deletions src/ansible_creator/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@

MIN_COLLECTION_NAME_LEN = 2

COMING_SOON = (
"add resource role",
"add plugin action",
)
COMING_SOON = ("add resource role",)


class Parser:
Expand Down Expand Up @@ -369,6 +366,7 @@ def _add_plugin_action(self, subparser: SubParser[ArgumentParser]) -> None:
formatter_class=CustomHelpFormatter,
)
self._add_args_common(parser)
self._add_overwrite(parser)
self._add_args_plugin_common(parser)

def _add_plugin_filter(self, subparser: SubParser[ArgumentParser]) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{# action_plugin_template.j2 #}
{%- set action_name = plugin_name | default("hello_world") -%}
{%- set author = author | default("Your Name") -%}
{%- set description = description | default("A custom action plugin for Ansible.") -%}
{%- set license = license | default("GPL-3.0-or-later") -%}
# {{ action_name }}.py - {{ description }}
# Author: {{ author }}
# License: {{ license }}
# pylint: disable=E0401

from __future__ import absolute_import, annotations, division, print_function

__metaclass__ = type # pylint: disable=C0103

from typing import TYPE_CHECKING
from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( # type: ignore
AnsibleArgSpecValidator,
)
from ansible_collections.ansible.utils.plugins.modules.fact_diff import DOCUMENTATION # type: ignore
from ansible.plugins.action import ActionBase # type: ignore


if TYPE_CHECKING:
from typing import Optional, Dict, Any


class ActionModule(ActionBase): # type: ignore[misc]
"""
Custom Ansible action plugin: {{ action_name }}
A custom action plugin for Ansible.
"""

def _check_argspec(self, result: dict[str, Any]) -> None:
aav = AnsibleArgSpecValidator(
data=self._task.args,
schema=DOCUMENTATION,
schema_format="doc",
name=self._task.action,
)
valid, errors, self._task.args = aav.validate()
if not valid:
result["failed"] = True
result["msg"] = errors

def run(
self,
tmp: Optional[str] = None,
task_vars: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Executes the action plugin.

Args:
tmp: Temporary path provided by Ansible for the module execution. Defaults to None.
task_vars: Dictionary of task variables available to the plugin. Defaults to None.

Returns:
dict: Result of the action plugin execution.
"""
# Get the task arguments
if task_vars is None:
task_vars = {}
result = {}
warnings: list[str] = []

# Example processing logic - Replace this with actual action code
result = super(ActionModule, self).run(tmp, task_vars)
self._check_argspec(result)

# Copy the task arguments
module_args = self._task.args.copy()

prefix = module_args.get("prefix", "DefaultPrefix")
message = module_args.get("msg", "No message provided")
module_args["msg"] = f"{prefix}: {message}"

result.update(
self._execute_module(
module_name="debug",
module_args=module_args,
task_vars=task_vars,
tmp=tmp,
),
)

if warnings:
if "warnings" in result:
result["warnings"].extend(warnings)
else:
result["warnings"] = warnings
return result
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{%- set module_name = plugin_name | default("hello_world") -%}
{%- set author = author | default("Your Name (@username)") -%}
# {{ module_name }}.py
# GNU General Public License v3.0+

DOCUMENTATION = """
module: {{ module_name }}
author: {{ author }}
version_added: "1.0.0"
short_description: A custom action plugin for Ansible.
description:
- This is a custom action plugin to provide action functionality.
options:
prefix:
description:
- A string that is added as a prefix to the message passed to the module.
type: str
msg:
description: The message to display in the output.
type: str
with_prefix:
description:
- A boolean flag indicating whether to include the prefix in the message.
type: bool
notes:
- This is a scaffold template. Customize the plugin to fit your needs.
"""

EXAMPLES = """
- name: Example Action Plugin
hosts: localhost
tasks:
- name: Example {{ module_name }} plugin
with_prefix:
prefix: "Hello, World"
msg: "Ansible!"
"""
90 changes: 80 additions & 10 deletions src/ansible_creator/subcommands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from pathlib import Path
from typing import TYPE_CHECKING

import yaml

from ansible_creator.constants import GLOBAL_TEMPLATE_VARS
from ansible_creator.exceptions import CreatorError
from ansible_creator.templar import Templar
Expand Down Expand Up @@ -92,13 +94,33 @@ def unique_name_in_devfile(self) -> str:
final_uuid = str(uuid.uuid4())[:8]
return f"{final_name}-{final_uuid}"

def update_galaxy_dependency(self) -> None:
"""Update galaxy.yml file with the required dependency."""
galaxy_file = self._add_path / "galaxy.yml"

# Load the galaxy.yml file
with galaxy_file.open("r", encoding="utf-8") as file:
data = yaml.safe_load(file)

# Ensure the dependencies key exists
if "dependencies" not in data:
data["dependencies"] = {"ansible.utils": "*"}

# Empty dependencies key or dependencies key without ansible.utils
elif not data["dependencies"] or "ansible.utils" not in data["dependencies"]:
data["dependencies"]["ansible.utils"] = "*"

# Save the updated YAML back to the file
with galaxy_file.open("w", encoding="utf-8") as file:
yaml.dump(data, file, sort_keys=False)

def _resource_scaffold(self) -> None:
"""Scaffold the specified resource file based on the resource type.
Raises:
CreatorError: If unsupported resource type is given.
"""
self.output.debug(f"Started copying {self._project} resource to destination")
self.output.debug(f"Started adding {self._resource_type} to destination")

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

# Call the appropriate scaffolding function based on the plugin type
if self._plugin_type in ("lookup", "filter"):
if self._plugin_type == "action":
self.update_galaxy_dependency()
template_data = self._get_plugin_template_data()
self._perform_action_plugin_scaffold(template_data, plugin_path)

elif self._plugin_type == "filter":
template_data = self._get_plugin_template_data()
self._perform_filter_plugin_scaffold(template_data, plugin_path)

elif self._plugin_type == "lookup":
template_data = self._get_plugin_template_data()
self._perform_lookup_plugin_scaffold(template_data, plugin_path)

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

self._perform_plugin_scaffold(template_data, plugin_path)
def _perform_action_plugin_scaffold(
self,
template_data: TemplateData,
plugin_path: Path,
) -> None:
resources = (
f"collection_project.plugins.{self._plugin_type}",
"collection_project.plugins.modules",
)
module_path = self._add_path / "plugins" / "modules"
module_path.mkdir(parents=True, exist_ok=True)
final_plugin_path = [plugin_path, module_path]
self._perform_plugin_scaffold(resources, template_data, final_plugin_path)

def _perform_filter_plugin_scaffold(
self,
template_data: TemplateData,
plugin_path: Path,
) -> None:
resources = (f"collection_project.plugins.{self._plugin_type}",)
self._perform_plugin_scaffold(resources, template_data, plugin_path)

def _perform_plugin_scaffold(self, template_data: TemplateData, plugin_path: Path) -> None:
def _perform_lookup_plugin_scaffold(
self,
template_data: TemplateData,
plugin_path: Path,
) -> None:
resources = (f"collection_project.plugins.{self._plugin_type}",)
self._perform_plugin_scaffold(resources, template_data, plugin_path)

def _perform_plugin_scaffold(
self,
resources: tuple[str, ...],
template_data: TemplateData,
plugin_path: Path | list[Path],
) -> None:
"""Perform the actual scaffolding process using the provided template data.
Args:
resources: Tuple of resources.
template_data: TemplateData
plugin_path: Path where the plugin will be scaffolded.
Expand All @@ -195,7 +261,7 @@ def _perform_plugin_scaffold(self, template_data: TemplateData, plugin_path: Pat
destination directory contains files that will be overwritten.
"""
walker = Walker(
resources=(f"collection_project.plugins.{self._plugin_type}",),
resources=resources,
resource_id=self._plugin_id,
dest=plugin_path,
output=self.output,
Expand All @@ -213,6 +279,10 @@ def _perform_plugin_scaffold(self, template_data: TemplateData, plugin_path: Pat
)
raise CreatorError(msg)

# This check is for action plugins (having module file as an additional path)
if isinstance(plugin_path, list):
plugin_path = plugin_path[0]

if not paths.has_conflicts() or self._force or self._overwrite:
copier.copy_containers(paths)
self.output.note(f"{self._plugin_type.capitalize()} plugin added to {plugin_path}")
Expand Down Expand Up @@ -270,10 +340,10 @@ def _get_devcontainer_template_data(self) -> TemplateData:
)

def _get_plugin_template_data(self) -> TemplateData:
"""Get the template data for lookup plugin.
"""Get the template data for plugin.
Returns:
TemplateData: Data required for templating the lookup plugin.
TemplateData: Data required for templating the plugin.
"""
return TemplateData(
plugin_type=self._plugin_type,
Expand All @@ -282,10 +352,10 @@ def _get_plugin_template_data(self) -> TemplateData:
)

def _get_ee_template_data(self) -> TemplateData:
"""Get the template data for lookup plugin.
"""Get the template data for plugin.
Returns:
TemplateData: Data required for templating the lookup plugin.
TemplateData: Data required for templating the plugin.
"""
return TemplateData(
resource_type=self._resource_type,
Expand Down
Loading

0 comments on commit 7ca0789

Please sign in to comment.