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,478 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present William Born <william.born.git@gmail.com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MIT
|
4
|
+
"""Debugging and troubleshooting tools for ArgoCD.
|
5
|
+
|
6
|
+
This module provides commands for debugging and troubleshooting ArgoCD applications
|
7
|
+
and Kubernetes clusters.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from __future__ import annotations
|
11
|
+
|
12
|
+
import subprocess
|
13
|
+
from typing import Any
|
14
|
+
|
15
|
+
import click
|
16
|
+
import yaml
|
17
|
+
|
18
|
+
from localargo.logging import logger
|
19
|
+
from localargo.utils.cli import (
|
20
|
+
build_kubectl_get_pods_cmd,
|
21
|
+
build_kubectl_logs_cmd,
|
22
|
+
check_cli_availability,
|
23
|
+
ensure_argocd_available,
|
24
|
+
ensure_kubectl_available,
|
25
|
+
log_subprocess_error,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
@click.group()
|
30
|
+
def debug() -> None:
|
31
|
+
"""Debugging and troubleshooting tools for ArgoCD."""
|
32
|
+
|
33
|
+
|
34
|
+
@debug.command()
|
35
|
+
@click.argument("app_name")
|
36
|
+
@click.option("--namespace", "-n", default="argocd", help="ArgoCD namespace")
|
37
|
+
@click.option("--tail", "-t", type=int, default=50, help="Number of log lines to show")
|
38
|
+
def logs(app_name: str, namespace: str, tail: int) -> None:
|
39
|
+
"""Show ArgoCD application logs."""
|
40
|
+
# Check if kubectl is available
|
41
|
+
kubectl_path = ensure_kubectl_available()
|
42
|
+
|
43
|
+
try:
|
44
|
+
logger.info("Fetching logs for application '%s'...", app_name)
|
45
|
+
|
46
|
+
# Get application pods
|
47
|
+
label_selector = f"app.kubernetes.io/instance={app_name}"
|
48
|
+
cmd = build_kubectl_get_pods_cmd(kubectl_path, namespace, label_selector)
|
49
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
50
|
+
|
51
|
+
pods = result.stdout.strip().split()
|
52
|
+
|
53
|
+
if not pods:
|
54
|
+
logger.info("ā No pods found for application '%s'", app_name)
|
55
|
+
return
|
56
|
+
|
57
|
+
# Show logs for each pod
|
58
|
+
for pod in pods:
|
59
|
+
logger.info("\nš Logs for pod: %s", pod)
|
60
|
+
logger.info("-" * 50)
|
61
|
+
|
62
|
+
try:
|
63
|
+
cmd = build_kubectl_logs_cmd(kubectl_path, namespace, pod, tail)
|
64
|
+
subprocess.run(cmd, check=True)
|
65
|
+
except subprocess.CalledProcessError as e:
|
66
|
+
logger.info("ā Error getting logs for pod %s: %s", pod, e)
|
67
|
+
|
68
|
+
except subprocess.CalledProcessError as e:
|
69
|
+
log_subprocess_error(e)
|
70
|
+
|
71
|
+
|
72
|
+
@debug.command()
|
73
|
+
@click.argument("app_name")
|
74
|
+
@click.option("--check-images", is_flag=True, help="Check if container images exist")
|
75
|
+
@click.option("--check-secrets", is_flag=True, help="Check if referenced secrets exist")
|
76
|
+
def validate(app_name: str, *, check_images: bool, check_secrets: bool) -> None:
|
77
|
+
"""Validate ArgoCD application configuration."""
|
78
|
+
try:
|
79
|
+
argocd_path = ensure_argocd_available()
|
80
|
+
kubectl_path = ensure_kubectl_available() if check_secrets else None
|
81
|
+
|
82
|
+
logger.info("Validating application '%s'...", app_name)
|
83
|
+
|
84
|
+
app_info = _get_application_info(argocd_path, app_name)
|
85
|
+
checks = _perform_basic_validation_checks(app_info)
|
86
|
+
|
87
|
+
if check_images:
|
88
|
+
image_issues = _check_container_images(app_name)
|
89
|
+
checks.extend(image_issues)
|
90
|
+
|
91
|
+
if check_secrets:
|
92
|
+
secret_issues = _check_secret_references(app_name, argocd_path, kubectl_path)
|
93
|
+
checks.extend(secret_issues)
|
94
|
+
|
95
|
+
_display_validation_results(checks)
|
96
|
+
|
97
|
+
except FileNotFoundError:
|
98
|
+
logger.error("ā argocd CLI not found")
|
99
|
+
except subprocess.CalledProcessError as e:
|
100
|
+
logger.error("ā Error validating application: %s", e)
|
101
|
+
|
102
|
+
|
103
|
+
def _get_application_info(argocd_path: str, app_name: str) -> str:
|
104
|
+
"""Get application details from ArgoCD."""
|
105
|
+
result = subprocess.run(
|
106
|
+
[argocd_path, "app", "get", app_name], capture_output=True, text=True, check=True
|
107
|
+
)
|
108
|
+
return result.stdout
|
109
|
+
|
110
|
+
|
111
|
+
def _perform_basic_validation_checks(app_info: str) -> list[tuple[str, str]]:
|
112
|
+
"""Perform basic validation checks on application info."""
|
113
|
+
checks = []
|
114
|
+
|
115
|
+
# Check sync status
|
116
|
+
if "OutOfSync" in app_info:
|
117
|
+
checks.append(("ā", "Application is out of sync"))
|
118
|
+
else:
|
119
|
+
checks.append(("ā
", "Application sync status OK"))
|
120
|
+
|
121
|
+
# Check health status
|
122
|
+
if "Degraded" in app_info or "Unknown" in app_info:
|
123
|
+
checks.append(("ā", "Application health is degraded"))
|
124
|
+
elif "Healthy" in app_info:
|
125
|
+
checks.append(("ā
", "Application health is OK"))
|
126
|
+
|
127
|
+
return checks
|
128
|
+
|
129
|
+
|
130
|
+
def _display_validation_results(checks: list[tuple[str, str]]) -> None:
|
131
|
+
"""Display validation results."""
|
132
|
+
logger.info("\nValidation Results:")
|
133
|
+
logger.info("=" * 30)
|
134
|
+
for status, message in checks:
|
135
|
+
logger.info("%s %s", status, message)
|
136
|
+
|
137
|
+
|
138
|
+
def _check_component_health( # pylint: disable=line-too-long
|
139
|
+
component: str, description: str, namespace: str, kubectl_path: str
|
140
|
+
) -> tuple[str, str]:
|
141
|
+
"""Check health of a single ArgoCD component."""
|
142
|
+
try:
|
143
|
+
result = subprocess.run(
|
144
|
+
[
|
145
|
+
kubectl_path,
|
146
|
+
"get",
|
147
|
+
"deployment",
|
148
|
+
component,
|
149
|
+
"-n",
|
150
|
+
namespace,
|
151
|
+
"-o",
|
152
|
+
"jsonpath={.status.readyReplicas}/{.status.replicas}",
|
153
|
+
],
|
154
|
+
capture_output=True,
|
155
|
+
text=True,
|
156
|
+
check=True,
|
157
|
+
)
|
158
|
+
|
159
|
+
status = result.stdout.strip()
|
160
|
+
if "/" in status:
|
161
|
+
ready, total = status.split("/")
|
162
|
+
if ready == total and ready != "0":
|
163
|
+
return ("ā
", f"{description}: {ready}/{total} ready")
|
164
|
+
return ("ā", f"{description}: {ready}/{total} ready")
|
165
|
+
except subprocess.CalledProcessError:
|
166
|
+
return ("ā", f"{description}: not found")
|
167
|
+
|
168
|
+
return ("ā", f"{description}: status unknown")
|
169
|
+
|
170
|
+
|
171
|
+
def health(namespace: str) -> None:
|
172
|
+
"""Check ArgoCD system health."""
|
173
|
+
kubectl_path = check_cli_availability("kubectl", "kubectl not found")
|
174
|
+
if not kubectl_path:
|
175
|
+
logger.error("kubectl not found in PATH. Please ensure kubectl is installed.")
|
176
|
+
return
|
177
|
+
|
178
|
+
logger.info("Checking ArgoCD system health...")
|
179
|
+
|
180
|
+
# Check ArgoCD components
|
181
|
+
components = [
|
182
|
+
("argocd-server", "ArgoCD Server"),
|
183
|
+
("argocd-repo-server", "Repository Server"),
|
184
|
+
("argocd-application-controller", "Application Controller"),
|
185
|
+
("argocd-dex-server", "Dex Server (optional)"),
|
186
|
+
("argocd-redis", "Redis Cache"),
|
187
|
+
]
|
188
|
+
|
189
|
+
health_checks = [
|
190
|
+
_check_component_health(comp, desc, namespace, kubectl_path)
|
191
|
+
for comp, desc in components
|
192
|
+
]
|
193
|
+
|
194
|
+
# Display results
|
195
|
+
logger.info("\nArgoCD Health Check:")
|
196
|
+
logger.info("=" * 30)
|
197
|
+
for status, message in health_checks:
|
198
|
+
logger.info("%s %s", status, message)
|
199
|
+
|
200
|
+
# Check API server connectivity
|
201
|
+
try:
|
202
|
+
argocd_path = ensure_argocd_available()
|
203
|
+
except FileNotFoundError:
|
204
|
+
logger.error("ā argocd CLI not found")
|
205
|
+
return
|
206
|
+
|
207
|
+
try:
|
208
|
+
subprocess.run([argocd_path, "version", "--client"], capture_output=True, check=True)
|
209
|
+
logger.info("ā
ArgoCD API connectivity OK")
|
210
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
211
|
+
logger.error("ā ArgoCD API connectivity issues")
|
212
|
+
|
213
|
+
|
214
|
+
@debug.command()
|
215
|
+
@click.argument("app_name")
|
216
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file for events")
|
217
|
+
def events(app_name: str, output: str | None) -> None:
|
218
|
+
"""Show Kubernetes events for an application."""
|
219
|
+
# Check if argocd and kubectl CLIs are available
|
220
|
+
argocd_path = ensure_argocd_available()
|
221
|
+
|
222
|
+
kubectl_path = check_cli_availability("kubectl", "kubectl not found")
|
223
|
+
if not kubectl_path:
|
224
|
+
msg = "kubectl not found"
|
225
|
+
raise FileNotFoundError(msg)
|
226
|
+
|
227
|
+
try:
|
228
|
+
# Get application namespace
|
229
|
+
result = subprocess.run(
|
230
|
+
[
|
231
|
+
argocd_path,
|
232
|
+
"app",
|
233
|
+
"get",
|
234
|
+
app_name,
|
235
|
+
"-o",
|
236
|
+
"jsonpath={.spec.destination.namespace}",
|
237
|
+
],
|
238
|
+
capture_output=True,
|
239
|
+
text=True,
|
240
|
+
check=True,
|
241
|
+
)
|
242
|
+
|
243
|
+
namespace = result.stdout.strip()
|
244
|
+
|
245
|
+
logger.info(
|
246
|
+
"Fetching events for application '%s' in namespace '%s'...", app_name, namespace
|
247
|
+
)
|
248
|
+
|
249
|
+
# Get events
|
250
|
+
cmd = [
|
251
|
+
kubectl_path,
|
252
|
+
"get",
|
253
|
+
"events",
|
254
|
+
"-n",
|
255
|
+
namespace,
|
256
|
+
"--sort-by=.metadata.creationTimestamp",
|
257
|
+
]
|
258
|
+
|
259
|
+
if output:
|
260
|
+
# Redirect output to file
|
261
|
+
with open(output, "w", encoding="utf-8") as f:
|
262
|
+
subprocess.run(cmd, stdout=f, check=True)
|
263
|
+
logger.info("ā
Events written to %s", output)
|
264
|
+
else:
|
265
|
+
# Show in terminal
|
266
|
+
subprocess.run(cmd, check=True)
|
267
|
+
|
268
|
+
except FileNotFoundError:
|
269
|
+
logger.error("ā kubectl or argocd CLI not found")
|
270
|
+
except subprocess.CalledProcessError as e:
|
271
|
+
logger.info(
|
272
|
+
"ā Error getting events: %s",
|
273
|
+
e,
|
274
|
+
)
|
275
|
+
|
276
|
+
|
277
|
+
def _check_container_images(app_name: str) -> list[tuple[str, str]]:
|
278
|
+
"""Check if container images referenced in the app exist."""
|
279
|
+
issues: list[tuple[str, str]] = []
|
280
|
+
|
281
|
+
# Check if argocd CLI is available
|
282
|
+
argocd_path = ensure_argocd_available()
|
283
|
+
|
284
|
+
try:
|
285
|
+
manifests = _load_manifests(argocd_path, app_name)
|
286
|
+
for manifest in manifests:
|
287
|
+
if _is_workload_kind(manifest.get("kind")):
|
288
|
+
containers = _get_containers_from_manifest(manifest)
|
289
|
+
_collect_image_issues(containers, manifest, issues)
|
290
|
+
except (subprocess.CalledProcessError, OSError, ValueError, yaml.YAMLError) as e:
|
291
|
+
# Keep this one as it's returning issues, not raising
|
292
|
+
issues.append(("ā", f"Error checking images: {e}"))
|
293
|
+
|
294
|
+
return issues
|
295
|
+
|
296
|
+
|
297
|
+
def _load_manifests(argocd_path: str, app_name: str) -> list[dict[str, Any]]:
|
298
|
+
"""Load manifests for an application via argocd CLI."""
|
299
|
+
result = subprocess.run(
|
300
|
+
[argocd_path, "app", "manifests", app_name],
|
301
|
+
capture_output=True,
|
302
|
+
text=True,
|
303
|
+
check=True,
|
304
|
+
)
|
305
|
+
return list(yaml.safe_load_all(result.stdout))
|
306
|
+
|
307
|
+
|
308
|
+
def _is_workload_kind(kind: str | None) -> bool:
|
309
|
+
"""Return True if kind is a workload that contains containers."""
|
310
|
+
return kind in {"Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob"}
|
311
|
+
|
312
|
+
|
313
|
+
def _get_containers_from_manifest(manifest: dict[str, Any]) -> list[dict[str, Any]]:
|
314
|
+
"""Extract containers list from manifest safely."""
|
315
|
+
template_spec = _get_template_spec(manifest)
|
316
|
+
if template_spec is None:
|
317
|
+
return []
|
318
|
+
containers = template_spec.get("containers")
|
319
|
+
if not isinstance(containers, list):
|
320
|
+
return []
|
321
|
+
return [c for c in containers if isinstance(c, dict)]
|
322
|
+
|
323
|
+
|
324
|
+
def _get_template_spec(manifest: dict[str, Any]) -> dict[str, Any] | None:
|
325
|
+
"""Safely navigate to spec.template.spec, returning a dict or None."""
|
326
|
+
spec = manifest.get("spec")
|
327
|
+
if not isinstance(spec, dict):
|
328
|
+
return None
|
329
|
+
template = spec.get("template")
|
330
|
+
if not isinstance(template, dict):
|
331
|
+
return None
|
332
|
+
template_spec = template.get("spec")
|
333
|
+
if not isinstance(template_spec, dict):
|
334
|
+
return None
|
335
|
+
return template_spec
|
336
|
+
|
337
|
+
|
338
|
+
def _collect_image_issues(
|
339
|
+
containers: list[dict[str, Any]],
|
340
|
+
manifest: dict[str, Any],
|
341
|
+
issues: list[tuple[str, str]],
|
342
|
+
) -> None:
|
343
|
+
"""Append image-related issues for given containers to the list."""
|
344
|
+
for container in containers:
|
345
|
+
image = container.get("image", "")
|
346
|
+
if not image:
|
347
|
+
name = manifest.get("metadata", {}).get("name", "unknown")
|
348
|
+
issues.append(("ā", f"Container missing image in {name}"))
|
349
|
+
elif ":" not in image:
|
350
|
+
issues.append(("ā ļø ", f"Container image without tag: {image}"))
|
351
|
+
|
352
|
+
|
353
|
+
def _check_secret_references(
|
354
|
+
app_name: str, argocd_path: str, kubectl_path: str | None
|
355
|
+
) -> list[tuple[str, str]]:
|
356
|
+
"""Check if secrets referenced in the app exist."""
|
357
|
+
issues: list[tuple[str, str]] = []
|
358
|
+
|
359
|
+
# CLI paths are already validated in the calling function
|
360
|
+
|
361
|
+
if kubectl_path is None:
|
362
|
+
issues.append(("ā", "kubectl path is required for secret checking"))
|
363
|
+
return issues
|
364
|
+
|
365
|
+
try:
|
366
|
+
app_namespace = _get_app_namespace(argocd_path, app_name)
|
367
|
+
secret_refs = _extract_secret_refs_from_manifests(argocd_path, app_name, app_namespace)
|
368
|
+
_verify_secrets_exist(kubectl_path, secret_refs, issues)
|
369
|
+
|
370
|
+
except (subprocess.CalledProcessError, OSError, ValueError, yaml.YAMLError) as exc:
|
371
|
+
issues.append(("ā", f"Error checking secrets: {exc}"))
|
372
|
+
|
373
|
+
return issues
|
374
|
+
|
375
|
+
|
376
|
+
def _get_app_namespace(argocd_path: str, app_name: str) -> str:
|
377
|
+
"""Get the namespace for the ArgoCD app."""
|
378
|
+
result = subprocess.run(
|
379
|
+
[argocd_path, "app", "get", app_name, "-o", "jsonpath={.spec.destination.namespace}"],
|
380
|
+
capture_output=True,
|
381
|
+
text=True,
|
382
|
+
check=True,
|
383
|
+
)
|
384
|
+
return result.stdout.strip()
|
385
|
+
|
386
|
+
|
387
|
+
def _extract_secret_refs_from_manifests( # pylint: disable=line-too-long
|
388
|
+
argocd_path: str, app_name: str, app_ns: str
|
389
|
+
) -> set[tuple[str, str]]:
|
390
|
+
"""Extract secret references from app manifests."""
|
391
|
+
manifests = _get_app_manifests(argocd_path, app_name)
|
392
|
+
secret_refs = set()
|
393
|
+
|
394
|
+
for manifest in manifests:
|
395
|
+
if manifest.get("kind") == "Secret":
|
396
|
+
continue
|
397
|
+
|
398
|
+
container_spec = _get_container_spec(manifest)
|
399
|
+
secret_refs.update(_extract_secret_refs_from_containers(container_spec, app_ns))
|
400
|
+
|
401
|
+
return secret_refs
|
402
|
+
|
403
|
+
|
404
|
+
def _get_app_manifests(argocd_path: str, app_name: str) -> list[dict[str, str]]:
|
405
|
+
"""Get manifests for an ArgoCD application."""
|
406
|
+
result = subprocess.run(
|
407
|
+
[argocd_path, "app", "manifests", app_name], capture_output=True, text=True, check=True
|
408
|
+
)
|
409
|
+
return list(yaml.safe_load_all(result.stdout))
|
410
|
+
|
411
|
+
|
412
|
+
def _get_container_spec(manifest: dict[str, Any]) -> dict[str, Any]:
|
413
|
+
"""Get the container specification from a manifest."""
|
414
|
+
spec = manifest.get("spec", {})
|
415
|
+
if not isinstance(spec, dict):
|
416
|
+
return {}
|
417
|
+
template_spec = spec.get("template", {})
|
418
|
+
if not isinstance(template_spec, dict):
|
419
|
+
return spec
|
420
|
+
container_spec = template_spec.get("spec", spec)
|
421
|
+
if not isinstance(container_spec, dict):
|
422
|
+
return spec
|
423
|
+
return container_spec
|
424
|
+
|
425
|
+
|
426
|
+
def _extract_secret_refs_from_containers(
|
427
|
+
container_spec: dict[str, Any], namespace: str
|
428
|
+
) -> set[tuple[str, str]]:
|
429
|
+
"""Extract secret references from container specifications."""
|
430
|
+
secret_refs = set()
|
431
|
+
|
432
|
+
for container in container_spec.get("containers", []):
|
433
|
+
secret_refs.update(_extract_secret_refs_from_env_from(container, namespace))
|
434
|
+
secret_refs.update(_extract_secret_refs_from_env_vars(container, namespace))
|
435
|
+
|
436
|
+
return secret_refs
|
437
|
+
|
438
|
+
|
439
|
+
def _extract_secret_refs_from_env_from(
|
440
|
+
container: dict[str, Any], namespace: str
|
441
|
+
) -> set[tuple[str, str]]:
|
442
|
+
"""Extract secret references from envFrom configurations."""
|
443
|
+
secret_refs = set()
|
444
|
+
|
445
|
+
for env_src in container.get("envFrom", []):
|
446
|
+
if "secretRef" in env_src:
|
447
|
+
secret_refs.add((env_src["secretRef"]["name"], namespace))
|
448
|
+
|
449
|
+
return secret_refs
|
450
|
+
|
451
|
+
|
452
|
+
def _extract_secret_refs_from_env_vars(
|
453
|
+
container: dict[str, Any], namespace: str
|
454
|
+
) -> set[tuple[str, str]]:
|
455
|
+
"""Extract secret references from environment variables."""
|
456
|
+
secret_refs = set()
|
457
|
+
|
458
|
+
for env_var in container.get("env", []):
|
459
|
+
if "valueFrom" in env_var and "secretKeyRef" in env_var["valueFrom"]:
|
460
|
+
secret_refs.add((env_var["valueFrom"]["secretKeyRef"]["name"], namespace))
|
461
|
+
|
462
|
+
return secret_refs
|
463
|
+
|
464
|
+
|
465
|
+
def _verify_secrets_exist( # pylint: disable=line-too-long
|
466
|
+
kubectl_path: str, secret_refs: set[tuple[str, str]], issues: list[tuple[str, str]]
|
467
|
+
) -> None:
|
468
|
+
"""Verify that referenced secrets exist in the cluster."""
|
469
|
+
for secret_name, namespace in secret_refs:
|
470
|
+
try:
|
471
|
+
subprocess.run(
|
472
|
+
[kubectl_path, "get", "secret", secret_name, "-n", namespace],
|
473
|
+
capture_output=True,
|
474
|
+
check=True,
|
475
|
+
)
|
476
|
+
issues.append(("ā
", f"Secret '{secret_name}' exists in '{namespace}'"))
|
477
|
+
except subprocess.CalledProcessError:
|
478
|
+
issues.append(("ā", f"Secret '{secret_name}' not found in '{namespace}'"))
|