localargo 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- localargo/__about__.py +6 -0
- localargo/__init__.py +6 -0
- localargo/__main__.py +11 -0
- localargo/cli/__init__.py +49 -0
- localargo/cli/commands/__init__.py +5 -0
- localargo/cli/commands/app.py +150 -0
- localargo/cli/commands/cluster.py +312 -0
- localargo/cli/commands/debug.py +478 -0
- localargo/cli/commands/port_forward.py +311 -0
- localargo/cli/commands/secrets.py +300 -0
- localargo/cli/commands/sync.py +291 -0
- localargo/cli/commands/template.py +288 -0
- localargo/cli/commands/up.py +341 -0
- localargo/config/__init__.py +15 -0
- localargo/config/manifest.py +520 -0
- localargo/config/store.py +66 -0
- localargo/core/__init__.py +6 -0
- localargo/core/apps.py +330 -0
- localargo/core/argocd.py +509 -0
- localargo/core/catalog.py +284 -0
- localargo/core/cluster.py +149 -0
- localargo/core/k8s.py +140 -0
- localargo/eyecandy/__init__.py +15 -0
- localargo/eyecandy/progress_steps.py +283 -0
- localargo/eyecandy/table_renderer.py +154 -0
- localargo/eyecandy/tables.py +57 -0
- localargo/logging.py +99 -0
- localargo/manager.py +232 -0
- localargo/providers/__init__.py +6 -0
- localargo/providers/base.py +146 -0
- localargo/providers/k3s.py +206 -0
- localargo/providers/kind.py +326 -0
- localargo/providers/registry.py +52 -0
- localargo/utils/__init__.py +4 -0
- localargo/utils/cli.py +231 -0
- localargo/utils/proc.py +148 -0
- localargo/utils/retry.py +58 -0
- localargo-0.1.0.dist-info/METADATA +149 -0
- localargo-0.1.0.dist-info/RECORD +42 -0
- localargo-0.1.0.dist-info/WHEEL +4 -0
- localargo-0.1.0.dist-info/entry_points.txt +2 -0
- localargo-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,291 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present William Born <william.born.git@gmail.com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MIT
|
4
|
+
"""Sync ArgoCD applications and local directories.
|
5
|
+
|
6
|
+
This module provides commands for syncing ArgoCD applications with local directories
|
7
|
+
and watching for changes to automatically sync.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from __future__ import annotations
|
11
|
+
|
12
|
+
import subprocess
|
13
|
+
import time
|
14
|
+
from pathlib import Path
|
15
|
+
from typing import TYPE_CHECKING
|
16
|
+
|
17
|
+
try:
|
18
|
+
from watchdog.events import FileSystemEventHandler
|
19
|
+
from watchdog.observers import Observer
|
20
|
+
except ImportError:
|
21
|
+
FileSystemEventHandler = None
|
22
|
+
Observer = None
|
23
|
+
|
24
|
+
import click
|
25
|
+
|
26
|
+
from localargo.core.argocd import ArgoClient
|
27
|
+
from localargo.logging import logger
|
28
|
+
from localargo.utils.cli import run_subprocess
|
29
|
+
|
30
|
+
if TYPE_CHECKING:
|
31
|
+
from watchdog.events import FileSystemEvent
|
32
|
+
|
33
|
+
# Constants
|
34
|
+
SYNC_DEBOUNCE_SECONDS = 2
|
35
|
+
|
36
|
+
|
37
|
+
@click.command()
|
38
|
+
@click.option("--watch", "-w", is_flag=True, help="Watch for changes and auto-sync")
|
39
|
+
@click.option("--path", "-p", help="Local path to watch (for directory sync)")
|
40
|
+
@click.option("--app", "-a", help="Specific ArgoCD application to sync")
|
41
|
+
@click.option("--sync-all", is_flag=True, help="Sync all applications")
|
42
|
+
@click.option("--force", "-f", is_flag=True, help="Force sync even if no changes")
|
43
|
+
def sync_cmd(
|
44
|
+
*, watch: bool, path: str | None, app: str | None, sync_all: bool, force: bool
|
45
|
+
) -> None:
|
46
|
+
"""Sync ArgoCD applications or local directories."""
|
47
|
+
if not _validate_sync_arguments(path, app, watch=watch, sync_all=sync_all):
|
48
|
+
return
|
49
|
+
|
50
|
+
sync_mode = _determine_sync_mode(path, app, watch=watch, sync_all=sync_all)
|
51
|
+
_execute_sync_mode(sync_mode, path, app, force=force)
|
52
|
+
|
53
|
+
|
54
|
+
def _validate_sync_arguments(
|
55
|
+
path: str | None, app: str | None, *, watch: bool, sync_all: bool
|
56
|
+
) -> bool:
|
57
|
+
"""Validate sync command arguments."""
|
58
|
+
if watch and not path and not app:
|
59
|
+
logger.error("❌ --watch requires --path or --app")
|
60
|
+
return False
|
61
|
+
|
62
|
+
if sync_all and app:
|
63
|
+
logger.error("❌ Cannot specify both --sync-all and --app")
|
64
|
+
return False
|
65
|
+
|
66
|
+
return True
|
67
|
+
|
68
|
+
|
69
|
+
def _determine_sync_mode(
|
70
|
+
path: str | None, app: str | None, *, watch: bool, sync_all: bool
|
71
|
+
) -> str:
|
72
|
+
"""Determine which sync mode to use based on arguments."""
|
73
|
+
if watch:
|
74
|
+
return "watch"
|
75
|
+
if sync_all:
|
76
|
+
return "sync_all"
|
77
|
+
if app:
|
78
|
+
return "sync_app"
|
79
|
+
if path:
|
80
|
+
return "sync_path"
|
81
|
+
return "error"
|
82
|
+
|
83
|
+
|
84
|
+
def _execute_sync_mode(
|
85
|
+
sync_mode: str,
|
86
|
+
path: str | None,
|
87
|
+
app: str | None,
|
88
|
+
*,
|
89
|
+
force: bool,
|
90
|
+
) -> None:
|
91
|
+
"""Execute the appropriate sync mode."""
|
92
|
+
if sync_mode == "watch":
|
93
|
+
_sync_watch(path, app)
|
94
|
+
elif sync_mode == "sync_all":
|
95
|
+
_sync_all_applications(force=force)
|
96
|
+
elif sync_mode == "sync_app":
|
97
|
+
_sync_application(app, force=force) # type: ignore[arg-type]
|
98
|
+
elif sync_mode == "sync_path":
|
99
|
+
_sync_directory(path) # type: ignore[arg-type]
|
100
|
+
else:
|
101
|
+
logger.error("❌ Specify what to sync: --app, --sync-all, or --path")
|
102
|
+
|
103
|
+
|
104
|
+
def _get_application_list() -> list[str]:
|
105
|
+
"""Get list of all ArgoCD applications."""
|
106
|
+
# Ensure we're authenticated before invoking argocd
|
107
|
+
ArgoClient() # constructor auto-logins if needed
|
108
|
+
try:
|
109
|
+
result = run_subprocess(["argocd", "app", "list", "-o", "name"])
|
110
|
+
except subprocess.CalledProcessError as e:
|
111
|
+
logger.info(
|
112
|
+
"❌ Error listing applications: %s. Try 'localargo cluster password' then re-run.",
|
113
|
+
e,
|
114
|
+
)
|
115
|
+
raise
|
116
|
+
names = [app.strip() for app in result.stdout.strip().split("\n") if app.strip()]
|
117
|
+
if not names:
|
118
|
+
logger.info("i No applications found. Use 'localargo app deploy <app>' to create one.")
|
119
|
+
return names
|
120
|
+
|
121
|
+
|
122
|
+
def _sync_multiple_applications(apps: list[str], *, force: bool) -> None:
|
123
|
+
"""Sync multiple ArgoCD applications."""
|
124
|
+
for app in apps:
|
125
|
+
_sync_single_application_with_error_handling(app, force=force)
|
126
|
+
|
127
|
+
|
128
|
+
def _sync_single_application_with_error_handling(app_name: str, *, force: bool) -> None:
|
129
|
+
"""Sync a single application with error handling."""
|
130
|
+
try:
|
131
|
+
_sync_single_application(app_name, force=force)
|
132
|
+
logger.info("✅ '%s' synced", app_name)
|
133
|
+
except subprocess.CalledProcessError as e:
|
134
|
+
logger.info("❌ Error syncing '%s': %s", app_name, e)
|
135
|
+
|
136
|
+
|
137
|
+
def _sync_single_application(app_name: str, *, force: bool) -> None:
|
138
|
+
"""Sync a single ArgoCD application."""
|
139
|
+
logger.info("Syncing '%s'...", app_name)
|
140
|
+
# Ensure authentication prior to syncing
|
141
|
+
ArgoClient()
|
142
|
+
cmd = ["argocd", "app", "sync", app_name]
|
143
|
+
if force:
|
144
|
+
cmd.append("--force")
|
145
|
+
try:
|
146
|
+
subprocess.run(cmd, check=True)
|
147
|
+
except subprocess.CalledProcessError as e:
|
148
|
+
logger.info(
|
149
|
+
"❌ Error syncing '%s': %s. Try re-authenticating (localargo cluster password).",
|
150
|
+
app_name,
|
151
|
+
e,
|
152
|
+
)
|
153
|
+
raise
|
154
|
+
|
155
|
+
|
156
|
+
def _sync_application(app_name: str, *, force: bool = False) -> None:
|
157
|
+
"""Sync a specific ArgoCD application."""
|
158
|
+
try:
|
159
|
+
logger.info("Syncing application '%s'...", app_name)
|
160
|
+
ArgoClient()
|
161
|
+
cmd = ["argocd", "app", "sync", app_name]
|
162
|
+
if force:
|
163
|
+
cmd.append("--force")
|
164
|
+
|
165
|
+
subprocess.run(cmd, check=True)
|
166
|
+
logger.info("✅ Application '%s' synced successfully", app_name)
|
167
|
+
|
168
|
+
except FileNotFoundError:
|
169
|
+
logger.error("❌ argocd CLI not found")
|
170
|
+
except subprocess.CalledProcessError as e:
|
171
|
+
logger.info("❌ Error syncing application: %s", e)
|
172
|
+
|
173
|
+
|
174
|
+
def _sync_all_applications(*, force: bool = False) -> None:
|
175
|
+
"""Sync all ArgoCD applications."""
|
176
|
+
try:
|
177
|
+
logger.info("Syncing all applications...")
|
178
|
+
ArgoClient()
|
179
|
+
apps = _get_application_list()
|
180
|
+
if not apps:
|
181
|
+
logger.info("No applications found")
|
182
|
+
return
|
183
|
+
|
184
|
+
logger.info("Found %d applications: %s", len(apps), ", ".join(apps))
|
185
|
+
|
186
|
+
_sync_multiple_applications(apps, force=force)
|
187
|
+
logger.info("✅ All applications sync completed")
|
188
|
+
|
189
|
+
except FileNotFoundError:
|
190
|
+
logger.error("❌ argocd CLI not found")
|
191
|
+
except subprocess.CalledProcessError as e:
|
192
|
+
logger.info(
|
193
|
+
"❌ Error listing applications: %s. Consider re-authenticating via "
|
194
|
+
"'localargo cluster password'",
|
195
|
+
e,
|
196
|
+
)
|
197
|
+
|
198
|
+
|
199
|
+
def _sync_directory(path: str) -> None:
|
200
|
+
"""Sync a local directory (placeholder for future GitOps integration)."""
|
201
|
+
path_obj = Path(path)
|
202
|
+
|
203
|
+
if not path_obj.exists():
|
204
|
+
logger.info("❌ Path does not exist: %s", path)
|
205
|
+
return
|
206
|
+
|
207
|
+
if not path_obj.is_dir():
|
208
|
+
logger.info("❌ Path is not a directory: %s", path)
|
209
|
+
return
|
210
|
+
|
211
|
+
logger.info("Directory sync for '%s' not yet implemented", path)
|
212
|
+
logger.info("This would integrate with GitOps workflows in the future")
|
213
|
+
|
214
|
+
|
215
|
+
def _sync_watch(path: str | None = None, app: str | None = None) -> None:
|
216
|
+
"""Watch for changes and auto-sync."""
|
217
|
+
if path:
|
218
|
+
_watch_directory(path)
|
219
|
+
elif app:
|
220
|
+
_watch_application(app)
|
221
|
+
|
222
|
+
|
223
|
+
def _watch_directory(path: str) -> None:
|
224
|
+
"""Watch a directory for changes and sync."""
|
225
|
+
if FileSystemEventHandler is None or Observer is None:
|
226
|
+
logger.error(
|
227
|
+
"❌ watchdog package required for watching. Install with: pip install watchdog"
|
228
|
+
)
|
229
|
+
return
|
230
|
+
|
231
|
+
path_obj = Path(path)
|
232
|
+
if not path_obj.exists():
|
233
|
+
logger.info("❌ Path does not exist: %s", path)
|
234
|
+
return
|
235
|
+
|
236
|
+
class ChangeHandler(FileSystemEventHandler):
|
237
|
+
"""Handler for file system events during directory watching.
|
238
|
+
|
239
|
+
Initializes the change handler with last sync timestamp.
|
240
|
+
"""
|
241
|
+
|
242
|
+
def __init__(self) -> None:
|
243
|
+
self.last_sync = 0.0
|
244
|
+
|
245
|
+
@property
|
246
|
+
def is_ready(self) -> bool:
|
247
|
+
"""Check if handler is ready for sync operations."""
|
248
|
+
return time.time() - self.last_sync > SYNC_DEBOUNCE_SECONDS
|
249
|
+
|
250
|
+
def on_any_event(self, event: FileSystemEvent) -> None:
|
251
|
+
"""Handle file system events."""
|
252
|
+
# Debounce syncs
|
253
|
+
current_time = time.time()
|
254
|
+
if current_time - self.last_sync > SYNC_DEBOUNCE_SECONDS:
|
255
|
+
logger.info("📁 Change detected: %s", event.src_path)
|
256
|
+
# Here you would trigger a sync
|
257
|
+
logger.info("🔄 Auto-sync not yet implemented")
|
258
|
+
self.last_sync = current_time
|
259
|
+
|
260
|
+
logger.info("👀 Watching directory: %s", path)
|
261
|
+
logger.info("Press Ctrl+C to stop watching")
|
262
|
+
|
263
|
+
observer = Observer()
|
264
|
+
observer.schedule(ChangeHandler(), path, recursive=True)
|
265
|
+
observer.start()
|
266
|
+
|
267
|
+
try:
|
268
|
+
observer.join()
|
269
|
+
except KeyboardInterrupt:
|
270
|
+
observer.stop()
|
271
|
+
logger.info("\n✅ Stopped watching")
|
272
|
+
|
273
|
+
|
274
|
+
def _watch_application(app_name: str) -> None:
|
275
|
+
"""Watch an ArgoCD application for changes."""
|
276
|
+
try:
|
277
|
+
logger.info("👀 Watching application: %s", app_name)
|
278
|
+
logger.info("Press Ctrl+C to stop watching")
|
279
|
+
|
280
|
+
# Use argocd app wait to watch for changes
|
281
|
+
cmd = ["argocd", "app", "wait", app_name, "--watch-only"]
|
282
|
+
|
283
|
+
try:
|
284
|
+
subprocess.run(cmd, check=True)
|
285
|
+
except KeyboardInterrupt:
|
286
|
+
logger.info("\n✅ Stopped watching application '%s'", app_name)
|
287
|
+
|
288
|
+
except FileNotFoundError:
|
289
|
+
logger.error("❌ argocd CLI not found")
|
290
|
+
except subprocess.CalledProcessError as e:
|
291
|
+
logger.info("❌ Error watching application: %s", e)
|
@@ -0,0 +1,288 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present William Born <william.born.git@gmail.com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MIT
|
4
|
+
"""Template management for ArgoCD applications.
|
5
|
+
|
6
|
+
This module provides commands for creating ArgoCD applications from templates.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
import subprocess
|
12
|
+
import tempfile
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from pathlib import Path
|
15
|
+
from typing import Any
|
16
|
+
|
17
|
+
import click
|
18
|
+
import yaml
|
19
|
+
|
20
|
+
from localargo.logging import logger
|
21
|
+
from localargo.utils.cli import check_cli_availability
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass
|
25
|
+
class TemplateConfig: # pylint: disable=too-many-instance-attributes
|
26
|
+
"""Configuration for application template generation."""
|
27
|
+
|
28
|
+
name: str
|
29
|
+
app_type: str
|
30
|
+
repo: str
|
31
|
+
path: str
|
32
|
+
namespace: str
|
33
|
+
image: str | None
|
34
|
+
port: int
|
35
|
+
env_vars: tuple[str, ...]
|
36
|
+
|
37
|
+
|
38
|
+
@click.group()
|
39
|
+
def template() -> None:
|
40
|
+
"""Create ArgoCD applications from templates."""
|
41
|
+
|
42
|
+
|
43
|
+
@template.command()
|
44
|
+
@click.argument("name")
|
45
|
+
@click.option(
|
46
|
+
"--app-type",
|
47
|
+
"-t",
|
48
|
+
type=click.Choice(["web-app", "api", "worker", "database"]),
|
49
|
+
default="web-app",
|
50
|
+
help="Application type",
|
51
|
+
)
|
52
|
+
@click.option("--repo", "-r", help="Git repository URL")
|
53
|
+
@click.option("--path", "-p", default=".", help="Path within repository")
|
54
|
+
@click.option("--namespace", "-n", default="default", help="Target namespace")
|
55
|
+
@click.option("--image", "-i", help="Container image")
|
56
|
+
@click.option("--port", type=int, default=80, help="Service port")
|
57
|
+
@click.option("--env", multiple=True, help="Environment variables (KEY=VALUE)")
|
58
|
+
@click.option("--create-app", is_flag=True, help="Create the ArgoCD application immediately")
|
59
|
+
def create( # pylint: disable=too-many-arguments
|
60
|
+
name: str,
|
61
|
+
app_type: str,
|
62
|
+
*,
|
63
|
+
repo: str | None,
|
64
|
+
path: str,
|
65
|
+
namespace: str,
|
66
|
+
image: str | None,
|
67
|
+
port: int,
|
68
|
+
env: tuple[str, ...],
|
69
|
+
create_app: bool,
|
70
|
+
) -> None:
|
71
|
+
"""Create an application from a template."""
|
72
|
+
if not repo:
|
73
|
+
logger.error("❌ Repository URL required (--repo)")
|
74
|
+
return
|
75
|
+
|
76
|
+
config = _build_template_config(
|
77
|
+
name=name,
|
78
|
+
app_type=app_type,
|
79
|
+
repo=repo,
|
80
|
+
path=path,
|
81
|
+
namespace=namespace,
|
82
|
+
image=image,
|
83
|
+
port=port,
|
84
|
+
env_vars=env,
|
85
|
+
)
|
86
|
+
app_config = _generate_app_template(config)
|
87
|
+
|
88
|
+
_display_generated_config(app_config)
|
89
|
+
|
90
|
+
if create_app:
|
91
|
+
_create_argocd_app(name, app_config)
|
92
|
+
else:
|
93
|
+
logger.info("\nUse --create-app to create the application immediately")
|
94
|
+
|
95
|
+
|
96
|
+
@template.command()
|
97
|
+
def list_templates() -> None:
|
98
|
+
"""List available application templates."""
|
99
|
+
templates = {
|
100
|
+
"web-app": "Web application with service and ingress",
|
101
|
+
"api": "REST API application",
|
102
|
+
"worker": "Background worker/job application",
|
103
|
+
"database": "Database deployment (PostgreSQL, MySQL, etc.)",
|
104
|
+
}
|
105
|
+
|
106
|
+
logger.info("Available templates:")
|
107
|
+
for name, desc in templates.items():
|
108
|
+
logger.info(" %-12s - %s", name, desc)
|
109
|
+
|
110
|
+
|
111
|
+
@template.command()
|
112
|
+
@click.argument("template_type")
|
113
|
+
def show(template_type: str) -> None:
|
114
|
+
"""Show template details."""
|
115
|
+
if template_type not in ["web-app", "api", "worker", "database"]:
|
116
|
+
logger.error("❌ Unknown template type: %s", template_type)
|
117
|
+
return
|
118
|
+
|
119
|
+
# Generate example config
|
120
|
+
example_config = _generate_app_template(
|
121
|
+
TemplateConfig(
|
122
|
+
name=f"example-{template_type}",
|
123
|
+
app_type=template_type,
|
124
|
+
repo="https://github.com/example/example-repo",
|
125
|
+
path=".",
|
126
|
+
namespace="default",
|
127
|
+
image=f"example/{template_type}:latest",
|
128
|
+
port=80,
|
129
|
+
env_vars=(),
|
130
|
+
)
|
131
|
+
)
|
132
|
+
|
133
|
+
logger.info("Template: %s", template_type)
|
134
|
+
logger.info("=" * 30)
|
135
|
+
logger.info(yaml.dump(example_config, default_flow_style=False))
|
136
|
+
|
137
|
+
|
138
|
+
def _build_template_config( # pylint: disable=too-many-arguments
|
139
|
+
name: str,
|
140
|
+
app_type: str,
|
141
|
+
*,
|
142
|
+
repo: str,
|
143
|
+
path: str,
|
144
|
+
namespace: str,
|
145
|
+
image: str | None,
|
146
|
+
port: int,
|
147
|
+
env_vars: tuple[str, ...],
|
148
|
+
) -> TemplateConfig:
|
149
|
+
"""Build a TemplateConfig object from parameters."""
|
150
|
+
return TemplateConfig(
|
151
|
+
name=name,
|
152
|
+
app_type=app_type,
|
153
|
+
repo=repo,
|
154
|
+
path=path,
|
155
|
+
namespace=namespace,
|
156
|
+
image=image,
|
157
|
+
port=port,
|
158
|
+
env_vars=env_vars,
|
159
|
+
)
|
160
|
+
|
161
|
+
|
162
|
+
def _display_generated_config(app_config: dict[str, Any]) -> None:
|
163
|
+
"""Display the generated application configuration."""
|
164
|
+
logger.info("Generated ArgoCD Application:")
|
165
|
+
logger.info("=" * 50)
|
166
|
+
logger.info(yaml.dump(app_config, default_flow_style=False))
|
167
|
+
|
168
|
+
|
169
|
+
def _create_argocd_app(name: str, app_config: dict[str, Any]) -> None:
|
170
|
+
"""Create an ArgoCD application from the configuration."""
|
171
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
172
|
+
yaml.dump(app_config, f)
|
173
|
+
temp_file = f.name
|
174
|
+
|
175
|
+
try:
|
176
|
+
argocd_path = check_cli_availability("argocd")
|
177
|
+
if argocd_path is None:
|
178
|
+
msg = "argocd not found in PATH. Please ensure argocd CLI is installed."
|
179
|
+
raise RuntimeError(msg)
|
180
|
+
subprocess.run([argocd_path, "app", "create", name, "--file", temp_file], check=True)
|
181
|
+
logger.info("✅ ArgoCD application '%s' created", name)
|
182
|
+
except FileNotFoundError:
|
183
|
+
logger.error("❌ argocd CLI not found")
|
184
|
+
except subprocess.CalledProcessError as e:
|
185
|
+
logger.info("❌ Error creating application: %s", e)
|
186
|
+
finally:
|
187
|
+
Path(temp_file).unlink(missing_ok=True)
|
188
|
+
|
189
|
+
|
190
|
+
def _generate_app_template(config: TemplateConfig) -> dict[str, Any]:
|
191
|
+
"""Generate ArgoCD application configuration from template."""
|
192
|
+
app = _create_base_application(config)
|
193
|
+
_customize_application_for_type(app, config)
|
194
|
+
return app
|
195
|
+
|
196
|
+
|
197
|
+
def _create_base_application(config: TemplateConfig) -> dict[str, Any]:
|
198
|
+
"""Create the base ArgoCD application structure."""
|
199
|
+
return {
|
200
|
+
"apiVersion": "argoproj.io/v1alpha1",
|
201
|
+
"kind": "Application",
|
202
|
+
"metadata": {"name": config.name, "namespace": "argocd"},
|
203
|
+
"spec": {
|
204
|
+
"project": "default",
|
205
|
+
"source": {"repoURL": config.repo, "path": config.path, "targetRevision": "HEAD"},
|
206
|
+
"destination": {
|
207
|
+
"server": "https://kubernetes.default.svc",
|
208
|
+
"namespace": config.namespace,
|
209
|
+
},
|
210
|
+
"syncPolicy": {"automated": {"prune": True, "selfHeal": True}},
|
211
|
+
},
|
212
|
+
}
|
213
|
+
|
214
|
+
|
215
|
+
def _customize_application_for_type(app: dict[str, Any], config: TemplateConfig) -> None:
|
216
|
+
"""Customize the application based on its type."""
|
217
|
+
if config.app_type == "web-app":
|
218
|
+
_configure_web_app(app, config)
|
219
|
+
elif config.app_type == "api":
|
220
|
+
_configure_api_app(app, config)
|
221
|
+
elif config.app_type == "worker":
|
222
|
+
_configure_worker_app(app, config)
|
223
|
+
elif config.app_type == "database":
|
224
|
+
_configure_database_app(app, config)
|
225
|
+
|
226
|
+
|
227
|
+
def _configure_web_app(app: dict[str, Any], config: TemplateConfig) -> None:
|
228
|
+
"""Configure a web application."""
|
229
|
+
app["spec"]["source"]["helm"] = {
|
230
|
+
"parameters": [
|
231
|
+
{"name": "image.repository", "value": config.image or f"{config.name}"},
|
232
|
+
{"name": "image.tag", "value": "latest"},
|
233
|
+
{"name": "service.port", "value": str(config.port)},
|
234
|
+
]
|
235
|
+
}
|
236
|
+
|
237
|
+
if config.env_vars:
|
238
|
+
env_params = _build_env_parameters(config.env_vars)
|
239
|
+
if "helm" in app["spec"]["source"] and "parameters" in app["spec"]["source"]["helm"]:
|
240
|
+
app["spec"]["source"]["helm"]["parameters"].extend(env_params)
|
241
|
+
|
242
|
+
|
243
|
+
def _configure_api_app(app: dict[str, Any], config: TemplateConfig) -> None:
|
244
|
+
"""Configure an API application."""
|
245
|
+
app["spec"]["source"]["helm"] = {
|
246
|
+
"parameters": [
|
247
|
+
{"name": "image.repository", "value": config.image or f"{config.name}-api"},
|
248
|
+
{"name": "image.tag", "value": "latest"},
|
249
|
+
{"name": "service.port", "value": str(config.port)},
|
250
|
+
{"name": "ingress.enabled", "value": "true"},
|
251
|
+
]
|
252
|
+
}
|
253
|
+
|
254
|
+
|
255
|
+
def _configure_worker_app(app: dict[str, Any], config: TemplateConfig) -> None:
|
256
|
+
"""Configure a worker application."""
|
257
|
+
app["spec"]["source"]["helm"] = {
|
258
|
+
"parameters": [
|
259
|
+
{"name": "image.repository", "value": config.image or f"{config.name}-worker"},
|
260
|
+
{"name": "image.tag", "value": "latest"},
|
261
|
+
{"name": "replicaCount", "value": "2"},
|
262
|
+
]
|
263
|
+
}
|
264
|
+
# Remove service-related config for workers
|
265
|
+
if "syncPolicy" in app["spec"]:
|
266
|
+
app["spec"]["syncPolicy"] = {"automated": {}} # Simpler sync policy
|
267
|
+
|
268
|
+
|
269
|
+
def _configure_database_app(app: dict[str, Any], config: TemplateConfig) -> None:
|
270
|
+
"""Configure a database application."""
|
271
|
+
app["spec"]["source"]["helm"] = {
|
272
|
+
"parameters": [
|
273
|
+
{"name": "image.repository", "value": config.image or "postgres"},
|
274
|
+
{"name": "image.tag", "value": "13"},
|
275
|
+
{"name": "persistence.enabled", "value": "true"},
|
276
|
+
{"name": "persistence.size", "value": "10Gi"},
|
277
|
+
]
|
278
|
+
}
|
279
|
+
|
280
|
+
|
281
|
+
def _build_env_parameters(env_vars: tuple[str, ...]) -> list[dict[str, str]]:
|
282
|
+
"""Build environment variable parameters for helm."""
|
283
|
+
env_params = []
|
284
|
+
for env in env_vars:
|
285
|
+
if "=" in env:
|
286
|
+
key, value = env.split("=", 1)
|
287
|
+
env_params.append({"name": f"env.{key}", "value": value})
|
288
|
+
return env_params
|