Skip to content

Commit 5130556

Browse files
authored
[fix][feat] Check baseresource implementations in plugins (#2104)
1 parent 3c944e8 commit 5130556

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

.github/workflows/basecheck.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Check Base Resources
2+
on:
3+
push:
4+
tags:
5+
- "*.*.*"
6+
branches:
7+
- main
8+
pull_request:
9+
paths:
10+
- 'fixlib/**'
11+
- 'plugins/**'
12+
13+
jobs:
14+
basecheck:
15+
name: "basecheck"
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Setup Python
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: '3.11'
25+
architecture: 'x64'
26+
27+
- name: Install Dependencies
28+
run: |
29+
python -m pip install --upgrade pip
30+
./setup_venv.sh
31+
32+
- name: Run tests
33+
run: |
34+
source venv/bin/activate
35+
./tools/basecheck.py html > basecheck.html
36+
37+
- name: Archive base resource coverage results
38+
uses: actions/upload-artifact@v2
39+
with:
40+
name: basecheck
41+
path: ./basecheck.html

tools/basechecker.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env python3
2+
import io
3+
import csv
4+
import sys
5+
import pkgutil
6+
import importlib
7+
import inspect
8+
from typing import List, Type, Any, Dict, Optional
9+
10+
11+
def list_submodules(package_name: str) -> List[str]:
12+
package = importlib.import_module(package_name)
13+
try:
14+
package_path = package.__path__
15+
submodules = [f"{package_name}.{name}" for _, name, _ in pkgutil.iter_modules(package_path)]
16+
except AttributeError:
17+
submodules = []
18+
return submodules
19+
20+
21+
def list_fix_resource_modules(plugin_name: str) -> List[str]:
22+
plugin_package = f"fix_plugin_{plugin_name}"
23+
package_name = f"{plugin_package}.resource"
24+
submodules = []
25+
try:
26+
submodules = list_submodules(package_name)
27+
except ModuleNotFoundError:
28+
try:
29+
package_name = f"{plugin_package}.resources"
30+
submodules = list_submodules(package_name)
31+
except ModuleNotFoundError:
32+
raise
33+
except Exception:
34+
print(f"Failed to load {plugin_package}")
35+
submodules.append(package_name)
36+
return submodules
37+
38+
39+
def list_classes_in_module(module_name: str) -> List[Type[Any]]:
40+
module = importlib.import_module(module_name)
41+
classes = [cls for _, cls in inspect.getmembers(module, inspect.isclass) if cls.__module__ == module_name]
42+
return classes
43+
44+
45+
def get_fix_baseresource_classes() -> List[Type[Any]]:
46+
return [
47+
cls
48+
for cls in list_classes_in_module("fixlib.baseresources")
49+
if cls.__name__.startswith("Base") and cls.__name__ not in ["BaseResource", "BaseCloud"]
50+
]
51+
52+
53+
def filter_fix_resources(all_classes: List[Type[Any]], base_classes: List[Type[Any]]) -> List[Type[Any]]:
54+
filtered_classes = [cls for cls in all_classes if any(issubclass(cls, base) for base in base_classes)]
55+
return filtered_classes
56+
57+
58+
def analyze_plugin_implementations(
59+
base_classes: List[Type[Any]], plugins: List[str]
60+
) -> Dict[Type[Any], Dict[str, Optional[Type[Any]]]]:
61+
results = {cls: {plugin: None for plugin in plugins} for cls in base_classes}
62+
for plugin in plugins:
63+
try:
64+
plugin_classes = []
65+
for module in list_fix_resource_modules(plugin):
66+
plugin_classes.extend(list_classes_in_module(module))
67+
for base_cls in base_classes:
68+
for cls in plugin_classes:
69+
if issubclass(cls, base_cls):
70+
results[base_cls][plugin] = cls
71+
break
72+
except Exception as e:
73+
print(f"Error analyzing {plugin}: {e}")
74+
return results
75+
76+
77+
def print_stats(implementation_stats: Dict[Type[Any], Dict[str, Optional[Type[Any]]]]) -> None:
78+
for base_cls, plugins in implementation_stats.items():
79+
print(f"Base Resource: {base_cls.__name__}")
80+
for plugin, impl_cls in plugins.items():
81+
cls_name = impl_cls.__name__ if impl_cls else "None"
82+
print(f" Plugin {plugin}: {cls_name}")
83+
84+
85+
def stats_to_csv(implementation_stats: Dict[Type[Any], Dict[str, Optional[Type[Any]]]]) -> str:
86+
output = io.StringIO()
87+
fieldnames = ["Base Resource"] + list(next(iter(implementation_stats.values())).keys())
88+
writer = csv.DictWriter(output, fieldnames=fieldnames)
89+
writer.writeheader()
90+
for base_cls, plugins in implementation_stats.items():
91+
row = {"Base Resource": base_cls.__name__}
92+
for plugin, impl_cls in plugins.items():
93+
row[plugin] = impl_cls.__name__ if impl_cls else ""
94+
writer.writerow(row)
95+
csv_data = output.getvalue()
96+
output.close()
97+
return csv_data
98+
99+
100+
def stats_to_html(implementation_stats: Dict[Type[Any], Dict[str, Optional[Type[Any]]]]) -> str:
101+
html_output = (
102+
"<html>\n<head>\n"
103+
"<title>Fix Inventory BaseResource Plugin Implementations</title>\n"
104+
"<style>\n"
105+
"body { font-family: Arial, sans-serif; font-size: 14px; }\n"
106+
"table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }\n"
107+
"th, td { border: 1px solid #cccccc; padding: 8px; text-align: left; }\n"
108+
"th { background-color: #f2f2f2; }\n"
109+
"</style>\n"
110+
"</head>\n<body>\n"
111+
"<table>\n"
112+
)
113+
plugins = next(iter(implementation_stats.values())).keys()
114+
html_output += "<tr><th>Base Resource</th>" + "".join(f"<th>{plugin}</th>" for plugin in plugins) + "</tr>\n"
115+
116+
for base_class, plugin_dict in implementation_stats.items():
117+
row = f"<tr><td>{base_class.__name__}</td>"
118+
for _, impl_class in plugin_dict.items():
119+
class_name = impl_class.__name__ if impl_class else ""
120+
row += f"<td>{class_name}</td>"
121+
row += "</tr>\n"
122+
html_output += row
123+
124+
html_output += "</table>\n</body>\n</html>"
125+
126+
return html_output
127+
128+
129+
if __name__ == "__main__":
130+
base_classes = get_fix_baseresource_classes()
131+
plugins = ["aws", "gcp", "azure", "digitalocean"]
132+
implementation_stats = analyze_plugin_implementations(base_classes, plugins)
133+
134+
mode = sys.argv[1] if len(sys.argv) > 1 else "text"
135+
match mode:
136+
case "csv":
137+
print(stats_to_csv(implementation_stats))
138+
case "html":
139+
print(stats_to_html(implementation_stats))
140+
case _:
141+
print_stats(implementation_stats)

0 commit comments

Comments
 (0)