Skip to content

Commit c682fe9

Browse files
committed
Implement ApplicationSet generator script and update root configuration with new ApplicationSet definitions; remove obsolete MetalLB configurations and enhance existing app configurations with baseApp labels.
1 parent c860759 commit c682fe9

File tree

9 files changed

+485
-30
lines changed

9 files changed

+485
-30
lines changed

bin/test.py

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
#!/usr/bin/env python3
2+
import yaml
3+
import subprocess
4+
import json
5+
import sys
6+
import os
7+
import argparse
8+
import tempfile
9+
from pathlib import Path
10+
11+
def extract_appset_from_helm(chart_path, values_file=None, release_name="test-release"):
12+
"""Extract ApplicationSet definition from a Helm chart"""
13+
14+
cmd = ["helm", "template", release_name, chart_path]
15+
if values_file:
16+
cmd.extend(["-f", values_file])
17+
18+
print(f"Executing: {' '.join(cmd)}")
19+
result = subprocess.run(cmd, capture_output=True, text=True)
20+
21+
if result.returncode != 0:
22+
print(f"Error rendering Helm chart: {result.stderr}")
23+
sys.exit(1)
24+
25+
# Parse the output and find the ApplicationSet resources
26+
manifests = list(yaml.safe_load_all(result.stdout))
27+
appsets = [m for m in manifests if m and m.get("kind") == "ApplicationSet"]
28+
29+
if not appsets:
30+
print("No ApplicationSet resources found in the rendered Helm chart")
31+
sys.exit(1)
32+
33+
print(f"Found {len(appsets)} ApplicationSet resources")
34+
return appsets
35+
36+
def save_appset_to_file(appset, output_file):
37+
"""Save an ApplicationSet to a file"""
38+
with open(output_file, "w") as f:
39+
yaml.dump(appset, f)
40+
print(f"Saved ApplicationSet to {output_file}")
41+
42+
def extract_appset_from_values(values_yaml):
43+
"""Extract ApplicationSet from a Helm values.yaml structure"""
44+
data = yaml.safe_load(values_yaml)
45+
appsets = []
46+
47+
# Handle the specific format from the input
48+
if "applicationsets" in data:
49+
for appset_name, appset_data in data["applicationsets"].items():
50+
# Create a proper ApplicationSet structure
51+
appset = {
52+
"apiVersion": "argoproj.io/v1alpha1",
53+
"kind": "ApplicationSet",
54+
"metadata": {
55+
"name": appset_name,
56+
"namespace": appset_data.get("namespace", "argocd")
57+
},
58+
"spec": {
59+
"generators": appset_data.get("generators", []),
60+
"template": appset_data.get("template", {}),
61+
"templatePatch": appset_data.get("templatePatch", {}),
62+
"goTemplate": appset_data.get("goTemplate", False)
63+
}
64+
}
65+
66+
# Add optional fields if present
67+
if "goTemplateOptions" in appset_data:
68+
appset["spec"]["goTemplateOptions"] = appset_data["goTemplateOptions"]
69+
70+
if "syncPolicy" in appset_data:
71+
appset["spec"]["syncPolicy"] = appset_data["syncPolicy"]
72+
73+
appsets.append(appset)
74+
75+
if not appsets:
76+
print("No ApplicationSet resources found in the values format")
77+
return None
78+
79+
print(f"Found {len(appsets)} ApplicationSet resources in values format")
80+
return appsets
81+
82+
print("Could not extract ApplicationSet from values format")
83+
return None
84+
85+
def simulate_appset_generation(appset_file):
86+
"""Simulate the ApplicationSet controller's behavior to generate Applications"""
87+
# First try the kubectl-applicationset plugin
88+
plugin_check = subprocess.run(["kubectl", "applicationset", "generate", "--help"],
89+
capture_output=True, text=True)
90+
91+
if plugin_check.returncode == 0:
92+
print("Using kubectl-applicationset plugin to generate applications")
93+
cmd = ["kubectl", "applicationset", "generate", "-f", appset_file]
94+
result = subprocess.run(cmd, capture_output=True, text=True)
95+
96+
if result.returncode == 0:
97+
return list(yaml.safe_load_all(result.stdout))
98+
else:
99+
print(f"Error using kubectl-applicationset: {result.stderr}")
100+
101+
# If the plugin is not available, try using argocd CLI
102+
argocd_check = subprocess.run(["argocd", "version"], capture_output=True, text=True)
103+
104+
if argocd_check.returncode == 0:
105+
print("Using argocd CLI to generate applications")
106+
cmd = ["argocd", "appset", "generate", appset_file, "-o", "yaml"]
107+
result = subprocess.run(cmd, capture_output=True, text=True)
108+
109+
if result.returncode == 0:
110+
return list(yaml.safe_load_all(result.stdout))[0]
111+
else:
112+
print(f"Error using argocd CLI: {result.stderr}")
113+
114+
# Fallback to a basic simulation
115+
print("Falling back to basic generator simulation...")
116+
with open(appset_file, 'r') as f:
117+
appset = yaml.safe_load(f)
118+
119+
# This is a simplified simulation and may not cover all generator types and combinations
120+
# For a real test, you should install and use the kubectl-applicationset plugin
121+
apps = simulate_basic_generators(appset)
122+
123+
if not apps:
124+
print("WARNING: Could not simulate application generation. Install kubectl-applicationset plugin for better results.")
125+
126+
return apps
127+
128+
def simulate_basic_generators(appset):
129+
"""Very basic simulation of ApplicationSet generators"""
130+
# This is a simplified implementation and doesn't support all generators and combinations
131+
generators = appset.get("spec", {}).get("generators", [])
132+
template = appset.get("spec", {}).get("template", {})
133+
134+
# Example handling for list generator
135+
apps = []
136+
for generator in generators:
137+
if "list" in generator:
138+
elements = generator["list"].get("elements", [])
139+
for element in elements:
140+
app = create_application_from_template(template, element, appset)
141+
apps.append(app)
142+
143+
# Add support for other generator types here
144+
# ...
145+
146+
return apps
147+
148+
def create_application_from_template(template, params, appset):
149+
"""Create an Application from a template and parameters"""
150+
# This is a very simplified implementation
151+
# In reality, the ApplicationSet controller does more complex template rendering
152+
153+
# Start with a copy of the template
154+
app = {
155+
"apiVersion": "argoproj.io/v1alpha1",
156+
"kind": "Application",
157+
"metadata": {
158+
"name": "generated-application",
159+
"namespace": appset.get("metadata", {}).get("namespace", "argocd")
160+
},
161+
"spec": template.copy() if template else {}
162+
}
163+
164+
# Extremely simple parameter substitution
165+
# This doesn't handle the full Argo Go template functionality
166+
app_str = json.dumps(app)
167+
for key, value in params.items():
168+
placeholder = "{{" + key + "}}"
169+
app_str = app_str.replace(placeholder, str(value))
170+
171+
return json.loads(app_str)
172+
173+
def validate_applications(apps, validation_rules=None):
174+
"""Validate the generated applications against predefined rules"""
175+
if not apps:
176+
print("No applications to validate")
177+
return False
178+
179+
print(f"Found {len(apps)} generated applications")
180+
valid = True
181+
182+
# Simple validation: check if applications have required fields
183+
for i, app in enumerate(apps):
184+
print(f"Application {i+1}: {app.get('metadata', {}).get('name', 'unknown')}")
185+
186+
# Check basic structure
187+
if app.get("kind") != "Application":
188+
print(f" WARNING: Application {i+1} has incorrect kind: {app.get('kind')}")
189+
190+
if not app.get("spec"):
191+
print(f" ERROR: Application {i+1} missing spec section")
192+
valid = False
193+
continue
194+
195+
# Check for destination
196+
if not app["spec"].get("destination"):
197+
print(f" ERROR: Application {i+1} missing destination")
198+
valid = False
199+
200+
# Check for source
201+
if not app["spec"].get("sources"):
202+
print(f" ERROR: Application {i+1} missing source")
203+
valid = False
204+
205+
# Apply custom validation rules if provided
206+
if validation_rules:
207+
for rule_name, rule_func in validation_rules.items():
208+
print(f"Applying validation rule: {rule_name}")
209+
rule_passed = rule_func(apps)
210+
print(f" {'PASSED' if rule_passed else 'FAILED'}")
211+
valid = valid and rule_passed
212+
213+
return valid
214+
215+
def server_side_dry_run(appset_file):
216+
"""Perform a server-side dry-run of the ApplicationSet"""
217+
cmd = ["kubectl", "apply", "-f", appset_file, "--server-dry-run=true", "-o", "yaml"]
218+
result = subprocess.run(cmd, capture_output=True, text=True)
219+
220+
if result.returncode != 0:
221+
print(f"Error in server-side dry-run: {result.stderr}")
222+
return None
223+
224+
return yaml.safe_load(result.stdout)
225+
226+
def export_applications(apps, output_dir):
227+
"""Export generated applications to individual YAML files"""
228+
if not os.path.exists(output_dir):
229+
os.makedirs(output_dir)
230+
231+
for i, app in enumerate(apps):
232+
name = app.get("metadata", {}).get("name", f"app-{i+1}")
233+
output_file = os.path.join(output_dir, f"{name}.yaml")
234+
235+
with open(output_file, "w") as f:
236+
yaml.dump(app, f)
237+
238+
print(f"Exported application to {output_file}")
239+
240+
def main():
241+
parser = argparse.ArgumentParser(description="Test ArgoCD ApplicationSet generator combinations and templates")
242+
243+
# Input options
244+
input_group = parser.add_mutually_exclusive_group(required=True)
245+
input_group.add_argument("--helm-chart", help="Path to Helm chart containing ApplicationSet")
246+
input_group.add_argument("--values-file", help="Path to values.yaml file with ApplicationSet configuration")
247+
input_group.add_argument("--appset-file", help="Path to ApplicationSet YAML file")
248+
249+
# Optional arguments
250+
parser.add_argument("--release-name", default="test-release", help="Release name for Helm template (default: test-release)")
251+
parser.add_argument("--output-dir", default="./generated-apps", help="Directory to export generated applications (default: ./generated-apps)")
252+
parser.add_argument("--keep-appset", action="store_true", help="Keep the extracted ApplicationSet file")
253+
parser.add_argument("--appset-name", help="Specific ApplicationSet name to test (if not provided, all will be tested)")
254+
255+
args = parser.parse_args()
256+
257+
# Create output directory if it doesn't exist
258+
if args.output_dir and not os.path.exists(args.output_dir):
259+
os.makedirs(args.output_dir)
260+
261+
# Process ApplicationSets
262+
all_apps = []
263+
appset_files = []
264+
temp_files = [] # Keep track of temporary files to prevent them from being garbage collected
265+
266+
if args.helm_chart:
267+
values_file = args.values_file if args.values_file else None
268+
appsets = extract_appset_from_helm(args.helm_chart, values_file, args.release_name)
269+
if not appsets:
270+
sys.exit(1)
271+
272+
# Filter by name if specified
273+
if args.appset_name:
274+
appsets = [appset for appset in appsets if appset.get("metadata", {}).get("name") == args.appset_name]
275+
if not appsets:
276+
print(f"No ApplicationSet named '{args.appset_name}' found")
277+
sys.exit(1)
278+
279+
# Create temporary files for each ApplicationSet
280+
for appset in appsets:
281+
tmp = tempfile.NamedTemporaryFile(suffix=".yaml", delete=not args.keep_appset)
282+
temp_files.append(tmp) # Keep reference to prevent garbage collection
283+
appset_file = tmp.name
284+
save_appset_to_file(appset, appset_file)
285+
appset_files.append((appset.get("metadata", {}).get("name", "unknown"), appset_file))
286+
287+
elif args.values_file:
288+
with open(args.values_file, 'r') as f:
289+
appsets = extract_appset_from_values(f.read())
290+
if not appsets:
291+
sys.exit(1)
292+
293+
# Filter by name if specified
294+
if args.appset_name:
295+
appsets = [appset for appset in appsets if appset.get("metadata", {}).get("name") == args.appset_name]
296+
if not appsets:
297+
print(f"No ApplicationSet named '{args.appset_name}' found")
298+
sys.exit(1)
299+
300+
# Create temporary files for each ApplicationSet
301+
for appset in appsets:
302+
tmp = tempfile.NamedTemporaryFile(suffix=".yaml", delete=not args.keep_appset)
303+
temp_files.append(tmp) # Keep reference to prevent garbage collection
304+
appset_file = tmp.name
305+
save_appset_to_file(appset, appset_file)
306+
appset_files.append((appset.get("metadata", {}).get("name", "unknown"), appset_file))
307+
308+
elif args.appset_file:
309+
# For a single ApplicationSet file, just use it directly
310+
appset_files.append(("single", args.appset_file))
311+
312+
# If appset_name is specified, check if it matches
313+
if args.appset_name:
314+
with open(args.appset_file, 'r') as f:
315+
appset = yaml.safe_load(f)
316+
if appset.get("metadata", {}).get("name") != args.appset_name:
317+
print(f"ApplicationSet in file '{args.appset_file}' does not match requested name '{args.appset_name}'")
318+
sys.exit(1)
319+
320+
# Process each ApplicationSet
321+
for appset_name, appset_file in appset_files:
322+
print(f"\n=== Processing ApplicationSet: {appset_name} ===")
323+
324+
# Generate applications
325+
print(f"\n=== Generating applications for {appset_name} ===")
326+
apps = simulate_appset_generation(appset_file)
327+
328+
if not apps:
329+
print(f"WARNING: No applications generated for ApplicationSet {appset_name}")
330+
continue
331+
332+
all_apps.extend(apps)
333+
334+
# Validate applications
335+
print(f"\n=== Validating applications for {appset_name} ===")
336+
valid = validate_applications(apps)
337+
338+
# Export applications if requested
339+
if apps and args.output_dir:
340+
appset_output_dir = os.path.join(args.output_dir, appset_name)
341+
print(f"\n=== Exporting applications for {appset_name} ===")
342+
export_applications(apps, appset_output_dir)
343+
344+
if args.keep_appset:
345+
print(f"\nApplicationSet file for {appset_name} kept at: {appset_file}")
346+
347+
print(f"\n=== Summary for {appset_name} ===")
348+
print(f"Applications generated: {len(apps)}")
349+
print(f"Validation: {'PASSED' if valid else 'FAILED'}")
350+
351+
if not valid:
352+
print(f"Validation failed for ApplicationSet {appset_name}")
353+
# Continue with other ApplicationSets even if one fails
354+
355+
# Final summary
356+
print("\n=== Final Summary ===")
357+
print(f"Total ApplicationSets processed: {len(appset_files)}")
358+
print(f"Total applications generated: {len(all_apps)}")
359+
360+
# Exit with error if any ApplicationSet failed validation
361+
if not all_apps:
362+
print("ERROR: No applications were generated from any ApplicationSet")
363+
sys.exit(1)
364+
365+
if __name__ == "__main__":
366+
main()

clusters/cyrannus/picon/apps/metallb-system/addressPool.yaml

Lines changed: 0 additions & 8 deletions
This file was deleted.

clusters/cyrannus/picon/apps/metallb-system/kustomization.yaml

Lines changed: 0 additions & 6 deletions
This file was deleted.

clusters/cyrannus/picon/apps/metallb-system/l2Advertisement.yaml

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
namespace: pihole
22
project: pihole
33
argoProject: pihole
4+
baseApp: pihole

clusters/cyrannus/picon/apps/pihole/kustomization.yaml

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)