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
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}")
|