Skip to content

Commit 20978fd

Browse files
committed
make plugin functionality discoverable
And then show the user how to automate their discovery
1 parent 58eb019 commit 20978fd

File tree

3 files changed

+129
-31
lines changed

3 files changed

+129
-31
lines changed

resources/plugins/simln/simln.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def wait_for_predicate(predicate, timeout=5 * 60, interval=5):
127127
)
128128

129129

130-
def wait_for_all_tanks_status(target="running", timeout=20 * 60, interval=5):
130+
def wait_for_all_tanks_status(target: str = "running", timeout: int = 20 * 60, interval: int = 5):
131131
"""Poll the warnet server for container status
132132
Block until all tanks are running
133133
"""
@@ -180,8 +180,8 @@ def generate_nodes_file(activity, output_file: Path = Path("nodes.json")):
180180
node = {
181181
"id": name,
182182
"address": f"https://{name}:10009",
183-
"macaroon": "/config/admin.macaroon",
184-
"cert": "/config/tls.cert",
183+
"macaroon": "/working/admin.macaroon",
184+
"cert": "/working/tls.cert",
185185
}
186186
nodes.append(node)
187187

src/warnet/hooks.py

+109-14
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import copy
22
import importlib.util
33
import inspect
4+
import json
45
import os
56
import sys
67
import tempfile
78
from importlib.metadata import PackageNotFoundError, version
89
from pathlib import Path
9-
from typing import Any, Callable, Optional
10+
from types import ModuleType
11+
from typing import Any, Callable, Optional, Union, get_args, get_origin, get_type_hints
1012

1113
import click
1214
import inquirer
@@ -27,7 +29,7 @@ class PluginError(Exception):
2729

2830

2931
hook_registry: set[Callable[..., Any]] = set()
30-
imported_modules = {}
32+
imported_modules: dict[str, ModuleType] = {}
3133

3234

3335
@click.group(name="plugin")
@@ -97,10 +99,11 @@ def toggle(plugin: str):
9799

98100

99101
@plugin.command()
100-
@click.argument("plugin", type=str)
101-
@click.argument("function", type=str)
102-
@click.argument("args", nargs=-1, type=str) # Accepts zero or more arguments
103-
def run(plugin: str, function: str, args: tuple[str, ...]):
102+
@click.argument("plugin", type=str, default="")
103+
@click.argument("function", type=str, default="")
104+
@click.option("--args", default="", type=str, help="Apply positional arguments to the function")
105+
@click.option("--json-dict", default="", type=str, help="Use json dict to populate parameters")
106+
def run(plugin: str, function: str, args: tuple[str, ...], json_dict: str):
104107
"""Run a command available in a plugin"""
105108
plugin_dir = _get_plugin_directory()
106109
plugins = get_plugins_with_status(plugin_dir)
@@ -110,16 +113,106 @@ def run(plugin: str, function: str, args: tuple[str, ...]):
110113
click.secho("Please toggle it on to run commands.")
111114
sys.exit(0)
112115

113-
module = imported_modules.get(f"plugins.{plugin}")
114-
if hasattr(module, function):
115-
func = getattr(module, function)
116+
if plugin == "":
117+
plugin_names = [
118+
plugin_name.stem for plugin_name, status in get_plugins_with_status() if status
119+
]
120+
121+
q = [inquirer.List(name="plugin", message="Please choose a plugin", choices=plugin_names)]
122+
plugin = inquirer.prompt(q, theme=GreenPassion()).get("plugin")
123+
124+
if function == "":
125+
module = imported_modules.get(f"plugins.{plugin}")
126+
funcs = [name for name, _func in inspect.getmembers(module, inspect.isfunction)]
127+
q = [inquirer.List(name="func", message="Please choose a function", choices=funcs)]
128+
function = inquirer.prompt(q, theme=GreenPassion()).get("func")
129+
130+
func = get_func(function_name=function, plugin_name=plugin)
131+
hints = get_type_hints(func)
132+
if not func:
133+
sys.exit(0)
134+
135+
if args:
136+
func(*args)
137+
sys.exit(0)
138+
139+
if not json_dict:
140+
params = {}
141+
sig = inspect.signature(func)
142+
for name, param in sig.parameters.items():
143+
hint = hints.get(name)
144+
hint_name = get_type_name(hint)
145+
if param.default != inspect.Parameter.empty:
146+
q = [
147+
inquirer.Text(
148+
"input",
149+
message=f"Enter a value for '{name}' ({hint_name})",
150+
default=param.default,
151+
)
152+
]
153+
else:
154+
q = [
155+
inquirer.Text(
156+
"input",
157+
message=f"Enter a value for '{name}' ({hint_name})",
158+
)
159+
]
160+
user_input = inquirer.prompt(q).get("input")
161+
params[name] = cast_to_hint(user_input, hint)
162+
click.secho(
163+
f"\nwarnet plugin run {plugin} {function} --json-dict '{json.dumps(params)}'\n",
164+
fg="green",
165+
)
166+
else:
167+
params = json.loads(json_dict)
168+
169+
func(**params)
170+
171+
172+
def cast_to_hint(value: str, hint: Any) -> Any:
173+
"""
174+
Cast a string value to the provided type hint.
175+
"""
176+
origin = get_origin(hint)
177+
args = get_args(hint)
178+
179+
# Handle basic types (int, str, float, etc.)
180+
if origin is None:
181+
return hint(value)
182+
183+
# Handle Union (e.g., Union[int, str])
184+
if origin is Union:
185+
for arg in args:
186+
try:
187+
return cast_to_hint(value, arg)
188+
except (ValueError, TypeError):
189+
continue
190+
raise ValueError(f"Cannot cast {value} to {hint}")
191+
192+
# Handle Lists (e.g., List[int])
193+
if origin is list:
194+
return [cast_to_hint(v.strip(), args[0]) for v in value.split(",")]
195+
196+
raise ValueError(f"Unsupported hint: {hint}")
197+
198+
199+
def get_type_name(type_hint) -> str:
200+
if hasattr(type_hint, "__name__"):
201+
return type_hint.__name__
202+
return str(type_hint)
203+
204+
205+
def get_func(function_name: str, plugin_name: str) -> Optional[Callable[..., Any]]:
206+
module = imported_modules.get(f"plugins.{plugin_name}")
207+
if hasattr(module, function_name):
208+
func = getattr(module, function_name)
116209
if callable(func):
117-
result = func(*args)
118-
print(result)
210+
return func
119211
else:
120-
click.secho(f"{function} in {module} is not callable.")
212+
click.secho(f"{function_name} in {module} is not callable.")
121213
else:
122-
click.secho(f"Could not find {function} in {module}")
214+
click.secho(f"Could not find {function_name} in {module}")
215+
return None
123216

124217

125218
def api(func: Callable[..., Any]) -> Callable[..., Any]:
@@ -322,7 +415,9 @@ def check_if_plugin_enabled(path: Path) -> bool:
322415
return bool(enabled)
323416

324417

325-
def get_plugins_with_status(plugin_dir: Path) -> list[tuple[Path, bool]]:
418+
def get_plugins_with_status(plugin_dir: Optional[Path] = None) -> list[tuple[Path, bool]]:
419+
if not plugin_dir:
420+
plugin_dir = _get_plugin_directory()
326421
candidates = [
327422
Path(os.path.join(plugin_dir, name))
328423
for name in os.listdir(plugin_dir)

test/simln_test.py

+17-14
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pexpect
88
from test_base import TestBase
99

10-
from warnet.k8s import get_pods_with_label
10+
from warnet.k8s import download, get_pods_with_label
1111
from warnet.process import run_command
1212

1313

@@ -46,19 +46,22 @@ def check_simln_logs(self):
4646
def copy_results(self) -> bool:
4747
self.log.info("Copying results")
4848
sleep(20)
49-
get_pods_with_label("mission=plugin")[0]
50-
51-
destination_path = "results"
52-
53-
for root, _dirs, files in os.walk(destination_path):
54-
for file_name in files:
55-
file_path = os.path.join(root, file_name)
56-
57-
with open(file_path) as file:
58-
content = file.read()
59-
if "Success" in content:
60-
return True
61-
return False
49+
pod = get_pods_with_label("mission=plugin")[0]
50+
51+
download(
52+
pod.metadata.name,
53+
pod.metadata.namespace,
54+
)
55+
56+
# for root, _dirs, files in os.walk(destination_path):
57+
# for file_name in files:
58+
# file_path = os.path.join(root, file_name)
59+
#
60+
# with open(file_path) as file:
61+
# content = file.read()
62+
# if "Success" in content:
63+
# return True
64+
# return False
6265

6366

6467
if __name__ == "__main__":

0 commit comments

Comments
 (0)