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.
Files changed (42) hide show
  1. localargo/__about__.py +6 -0
  2. localargo/__init__.py +6 -0
  3. localargo/__main__.py +11 -0
  4. localargo/cli/__init__.py +49 -0
  5. localargo/cli/commands/__init__.py +5 -0
  6. localargo/cli/commands/app.py +150 -0
  7. localargo/cli/commands/cluster.py +312 -0
  8. localargo/cli/commands/debug.py +478 -0
  9. localargo/cli/commands/port_forward.py +311 -0
  10. localargo/cli/commands/secrets.py +300 -0
  11. localargo/cli/commands/sync.py +291 -0
  12. localargo/cli/commands/template.py +288 -0
  13. localargo/cli/commands/up.py +341 -0
  14. localargo/config/__init__.py +15 -0
  15. localargo/config/manifest.py +520 -0
  16. localargo/config/store.py +66 -0
  17. localargo/core/__init__.py +6 -0
  18. localargo/core/apps.py +330 -0
  19. localargo/core/argocd.py +509 -0
  20. localargo/core/catalog.py +284 -0
  21. localargo/core/cluster.py +149 -0
  22. localargo/core/k8s.py +140 -0
  23. localargo/eyecandy/__init__.py +15 -0
  24. localargo/eyecandy/progress_steps.py +283 -0
  25. localargo/eyecandy/table_renderer.py +154 -0
  26. localargo/eyecandy/tables.py +57 -0
  27. localargo/logging.py +99 -0
  28. localargo/manager.py +232 -0
  29. localargo/providers/__init__.py +6 -0
  30. localargo/providers/base.py +146 -0
  31. localargo/providers/k3s.py +206 -0
  32. localargo/providers/kind.py +326 -0
  33. localargo/providers/registry.py +52 -0
  34. localargo/utils/__init__.py +4 -0
  35. localargo/utils/cli.py +231 -0
  36. localargo/utils/proc.py +148 -0
  37. localargo/utils/retry.py +58 -0
  38. localargo-0.1.0.dist-info/METADATA +149 -0
  39. localargo-0.1.0.dist-info/RECORD +42 -0
  40. localargo-0.1.0.dist-info/WHEEL +4 -0
  41. localargo-0.1.0.dist-info/entry_points.txt +2 -0
  42. localargo-0.1.0.dist-info/licenses/LICENSE +21 -0
localargo/core/apps.py ADDED
@@ -0,0 +1,330 @@
1
+ """App orchestration combining catalog, ArgoCD, and Kubernetes helpers."""
2
+
3
+ # pylint: disable=too-many-arguments
4
+
5
+ from __future__ import annotations
6
+
7
+ import time
8
+
9
+ import click
10
+
11
+ from localargo.core.argocd import ArgoClient
12
+ from localargo.core.catalog import AppSpec, load_catalog
13
+ from localargo.core.k8s import apply_manifests, list_pods_for_app, stream_logs
14
+ from localargo.eyecandy.progress_steps import StepLogger
15
+ from localargo.eyecandy.tables import AppTables
16
+ from localargo.logging import logger
17
+ from localargo.utils.proc import ProcessError
18
+
19
+
20
+ def _targets(specs: list[AppSpec], app_name: str | None, *, all_: bool) -> list[AppSpec]:
21
+ """Resolve which apps to target given a name or --all flag."""
22
+ if all_:
23
+ return specs
24
+ if not app_name:
25
+ msg = "App name is required unless --all is provided"
26
+ raise ValueError(msg)
27
+ for s in specs:
28
+ if s.name == app_name:
29
+ return [s]
30
+ msg = f"App not found: {app_name}"
31
+ raise ValueError(msg)
32
+
33
+
34
+ def deploy( # pylint: disable=too-many-locals
35
+ app_name: str | None,
36
+ *,
37
+ all_: bool,
38
+ wait: bool,
39
+ profile: str | None,
40
+ kubeconfig: str | None = None,
41
+ # explicit overrides to create a single ArgoCD app or apply manifests
42
+ manifest_files: list[str] | None = None,
43
+ override_repo: str | None = None,
44
+ override_name: str | None = None,
45
+ override_path: str | None = None,
46
+ override_namespace: str | None = None,
47
+ override_project: str | None = None,
48
+ override_type: str | None = None,
49
+ override_helm_values: list[str] | None = None,
50
+ ) -> None:
51
+ """Create/update and sync target apps from the catalog.
52
+
53
+ If an app spec defines manifest_files, apply them via kubectl; otherwise
54
+ use ArgoCD to create/update and sync the application.
55
+ """
56
+ # Handle direct modes first
57
+ if manifest_files:
58
+ _deploy_manifest_files(manifest_files, kubeconfig)
59
+ return
60
+ if override_repo:
61
+ _deploy_override_app(
62
+ app_name,
63
+ wait=wait,
64
+ override_repo=override_repo,
65
+ override_name=override_name,
66
+ override_path=override_path,
67
+ override_namespace=override_namespace,
68
+ override_project=override_project,
69
+ override_type=override_type,
70
+ override_helm_values=override_helm_values,
71
+ )
72
+ return
73
+
74
+ specs = load_catalog(profile=profile)
75
+ targets = _targets(specs, app_name, all_=all_)
76
+ manifest_targets, argocd_targets = _split_targets_by_mode(targets)
77
+ steps = _build_steps(manifest_targets, argocd_targets)
78
+
79
+ client = ArgoClient()
80
+ with StepLogger(steps) as log:
81
+ _apply_manifest_targets(manifest_targets, kubeconfig, log)
82
+ _deploy_argocd_targets(argocd_targets, client, wait=wait, log=log)
83
+
84
+
85
+ def _split_targets_by_mode(targets: list[AppSpec]) -> tuple[list[AppSpec], list[AppSpec]]:
86
+ manifest_targets = [s for s in targets if getattr(s, "manifest_files", [])]
87
+ argocd_targets = [s for s in targets if not getattr(s, "manifest_files", [])]
88
+ return manifest_targets, argocd_targets
89
+
90
+
91
+ def _build_steps(manifest_targets: list[AppSpec], argocd_targets: list[AppSpec]) -> list[str]:
92
+ steps: list[str] = [f"Apply manifests for {s.name}" for s in manifest_targets]
93
+ steps += [
94
+ step for s in argocd_targets for step in (f"Create/Update {s.name}", f"Sync {s.name}")
95
+ ]
96
+ return steps
97
+
98
+
99
+ def _apply_manifest_targets(
100
+ manifest_targets: list[AppSpec], kubeconfig: str | None, log: StepLogger
101
+ ) -> None:
102
+ for s in manifest_targets:
103
+ try:
104
+ apply_manifests(s.manifest_files, kubeconfig=kubeconfig)
105
+ log.step(f"Apply manifests for {s.name}")
106
+ except Exception as e:
107
+ log.step(f"Apply manifests for {s.name}", status="error", error_msg=str(e))
108
+ raise
109
+
110
+
111
+ def _deploy_argocd_targets(
112
+ argocd_targets: list[AppSpec], client: ArgoClient, *, wait: bool, log: StepLogger
113
+ ) -> None:
114
+ for s in argocd_targets:
115
+ try:
116
+ client.create_or_update_app(s)
117
+ log.step(f"Create/Update {s.name}")
118
+ except Exception as e:
119
+ log.step(f"Create/Update {s.name}", status="error", error_msg=str(e))
120
+ raise
121
+ for s in argocd_targets:
122
+ try:
123
+ client.sync_app(s.name, wait=wait, timeout=s.health_timeout)
124
+ log.step(f"Sync {s.name}")
125
+ except Exception as e:
126
+ log.step(f"Sync {s.name}", status="error", error_msg=str(e))
127
+ raise
128
+
129
+
130
+ def _deploy_manifest_files(files: list[str], kubeconfig: str | None) -> None:
131
+ steps = ["Apply manifests"]
132
+ with StepLogger(steps) as log:
133
+ try:
134
+ apply_manifests(files, kubeconfig=kubeconfig)
135
+ log.step("Apply manifests")
136
+ except Exception as e: # pragma: no cover - surface error
137
+ log.step("Apply manifests", status="error", error_msg=str(e))
138
+ raise
139
+
140
+
141
+ def _deploy_override_app(
142
+ app_name: str | None,
143
+ *,
144
+ wait: bool,
145
+ override_repo: str,
146
+ override_name: str | None,
147
+ override_path: str | None,
148
+ override_namespace: str | None,
149
+ override_project: str | None,
150
+ override_type: str | None,
151
+ override_helm_values: list[str] | None,
152
+ ) -> None:
153
+ spec = _build_override_spec(
154
+ app_name,
155
+ override_repo=override_repo,
156
+ override_name=override_name,
157
+ override_path=override_path,
158
+ override_namespace=override_namespace,
159
+ override_project=override_project,
160
+ override_type=override_type,
161
+ override_helm_values=override_helm_values,
162
+ )
163
+ _apply_argocd_deploy(spec, wait=wait)
164
+
165
+
166
+ def _build_override_spec(
167
+ app_name: str | None,
168
+ *,
169
+ override_repo: str,
170
+ override_name: str | None,
171
+ override_path: str | None,
172
+ override_namespace: str | None,
173
+ override_project: str | None,
174
+ override_type: str | None,
175
+ override_helm_values: list[str] | None,
176
+ ) -> AppSpec:
177
+ name = _coalesce(override_name, app_name, "app")
178
+ path = _coalesce(override_path, ".")
179
+ app_type = _coalesce(override_type, "kustomize")
180
+ namespace = _coalesce(override_namespace, "default")
181
+ project = _coalesce(override_project, "default")
182
+ helm = list(override_helm_values or [])
183
+ return AppSpec(
184
+ name=name,
185
+ repo=override_repo,
186
+ path=path,
187
+ type=app_type, # type: ignore[arg-type]
188
+ namespace=namespace,
189
+ project=project,
190
+ helm_values=helm,
191
+ )
192
+
193
+
194
+ def _apply_argocd_deploy(spec: AppSpec, *, wait: bool) -> None:
195
+ client = ArgoClient()
196
+ steps = [f"Create/Update {spec.name}", f"Sync {spec.name}"]
197
+ with StepLogger(steps) as log:
198
+ try:
199
+ client.create_or_update_app(spec)
200
+ log.step(f"Create/Update {spec.name}")
201
+ client.sync_app(spec.name, wait=wait, timeout=spec.health_timeout)
202
+ log.step(f"Sync {spec.name}")
203
+ except Exception as e: # pragma: no cover - bubble up
204
+ log.step(f"Sync {spec.name}", status="error", error_msg=str(e))
205
+ raise
206
+
207
+
208
+ def _coalesce(*values: str | None) -> str:
209
+ for v in values:
210
+ if v is not None and str(v) != "":
211
+ return str(v)
212
+ return ""
213
+
214
+
215
+ def sync(app_name: str | None, *, all_: bool, wait: bool, profile: str | None) -> None:
216
+ """Sync target apps from the catalog (optionally wait)."""
217
+ specs = load_catalog(profile=profile)
218
+ targets = _targets(specs, app_name, all_=all_)
219
+ client = ArgoClient()
220
+ steps = [f"Sync {s.name}" for s in targets]
221
+ with StepLogger(steps) as log:
222
+ for s in targets:
223
+ try:
224
+ client.sync_app(s.name, wait=wait, timeout=s.health_timeout)
225
+ log.step(f"Sync {s.name}")
226
+ except Exception as e:
227
+ log.step(f"Sync {s.name}", status="error", error_msg=str(e))
228
+ raise
229
+
230
+
231
+ def list_apps(profile: str | None) -> None:
232
+ """Render a table of all ArgoCD apps."""
233
+ del profile
234
+ client = ArgoClient()
235
+ try:
236
+ states = client.get_apps()
237
+ except ProcessError as e:
238
+ logger.info(
239
+ "❌ Failed to query apps: %s. Try 'localargo cluster password' to refresh auth.",
240
+ e,
241
+ )
242
+ return
243
+ if not states:
244
+ logger.info("i No applications found. Create apps via 'localargo app deploy <app>'")
245
+ return
246
+ rows = [
247
+ {
248
+ "Name": st.name,
249
+ "Namespace": st.namespace,
250
+ "Health": st.health,
251
+ "Sync": st.sync,
252
+ "Revision": (st.revision or "")[:10],
253
+ }
254
+ for st in states
255
+ ]
256
+ AppTables().render_app_states(rows)
257
+
258
+
259
+ def status(app_name: str | None, *, watch: bool, profile: str | None) -> None:
260
+ """Show app status for one or all apps; supports --watch."""
261
+ del profile
262
+ client = ArgoClient()
263
+ tables = AppTables()
264
+
265
+ def render() -> None:
266
+ if app_name:
267
+ st = client.get_app(app_name)
268
+ rows = [
269
+ {
270
+ "Name": st.name,
271
+ "Namespace": st.namespace,
272
+ "Health": st.health,
273
+ "Sync": st.sync,
274
+ "Revision": (st.revision or "")[:10],
275
+ }
276
+ ]
277
+ else:
278
+ sts = client.get_apps()
279
+ rows = [
280
+ {
281
+ "Name": st.name,
282
+ "Namespace": st.namespace,
283
+ "Health": st.health,
284
+ "Sync": st.sync,
285
+ "Revision": (st.revision or "")[:10],
286
+ }
287
+ for st in sts
288
+ ]
289
+ tables.render_app_states(rows)
290
+
291
+ if not watch:
292
+ render()
293
+ return
294
+
295
+ try:
296
+ while True:
297
+ render()
298
+ time.sleep(2)
299
+ except KeyboardInterrupt:
300
+ return
301
+
302
+
303
+ def delete(app_name: str, *, profile: str | None) -> None:
304
+ """Delete an app from ArgoCD."""
305
+ del profile
306
+ client = ArgoClient()
307
+ client.delete_app(app_name)
308
+
309
+
310
+ def logs(
311
+ app_name: str,
312
+ *,
313
+ all_pods: bool,
314
+ container: str | None,
315
+ since: str | None,
316
+ follow: bool,
317
+ profile: str | None,
318
+ ) -> None: # pylint: disable=too-many-arguments
319
+ """Tail pod logs for an app. Supports multi-pod and follow modes.
320
+
321
+ pylint: disable=too-many-arguments
322
+ """
323
+ specs = load_catalog(profile=profile)
324
+ ns = next((s.namespace for s in specs if s.name == app_name), "default")
325
+ pods = list_pods_for_app(app_name, ns)
326
+ target_pods: list[str] = pods if all_pods else pods[:1]
327
+ for pod in target_pods:
328
+ prefix = f"[{pod}/{container or '-'}] "
329
+ for line in stream_logs(pod, ns, container=container, since=since, follow=follow):
330
+ click.echo(f"{prefix}{line}")