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/__about__.py
ADDED
localargo/__init__.py
ADDED
localargo/__main__.py
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present William Born <william.born.git@gmail.com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MIT
|
4
|
+
"""Main entry point for localargo CLI."""
|
5
|
+
|
6
|
+
import sys
|
7
|
+
|
8
|
+
if __name__ == "__main__":
|
9
|
+
from localargo.cli import localargo
|
10
|
+
|
11
|
+
sys.exit(localargo(verbose=False))
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present William Born <william.born.git@gmail.com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MIT
|
4
|
+
"""Command-line interface for LocalArgo."""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import rich_click as click
|
9
|
+
|
10
|
+
from localargo.__about__ import __version__
|
11
|
+
from localargo.cli.commands import app, cluster, debug, port_forward, secrets, sync, template
|
12
|
+
from localargo.cli.commands import up as up_cmds
|
13
|
+
from localargo.logging import init_cli_logging, logger
|
14
|
+
from localargo.utils.cli import ensure_core_tools_available
|
15
|
+
|
16
|
+
|
17
|
+
@click.group(
|
18
|
+
context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True
|
19
|
+
)
|
20
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging")
|
21
|
+
@click.version_option(version=__version__, prog_name="localargo")
|
22
|
+
def localargo(*, verbose: bool) -> None:
|
23
|
+
"""Localargo - Convenient ArgoCD local development tool."""
|
24
|
+
# Initialize logging
|
25
|
+
init_cli_logging(verbose=verbose)
|
26
|
+
|
27
|
+
# Check core tools presence on boot
|
28
|
+
try:
|
29
|
+
ensure_core_tools_available()
|
30
|
+
except FileNotFoundError as e:
|
31
|
+
logger.info("Required CLI missing: %s", e)
|
32
|
+
|
33
|
+
ctx = click.get_current_context()
|
34
|
+
if ctx is None or ctx.invoked_subcommand is None:
|
35
|
+
logger.info("Localargo - Convenient ArgoCD local development tool")
|
36
|
+
logger.info("Run 'localargo --help' for available commands.")
|
37
|
+
|
38
|
+
|
39
|
+
# Register subcommands
|
40
|
+
localargo.add_command(cluster.cluster)
|
41
|
+
localargo.add_command(app.app)
|
42
|
+
localargo.add_command(port_forward.port_forward)
|
43
|
+
localargo.add_command(secrets.secrets)
|
44
|
+
localargo.add_command(sync.sync_cmd)
|
45
|
+
localargo.add_command(template.template)
|
46
|
+
localargo.add_command(debug.debug)
|
47
|
+
localargo.add_command(up_cmds.validate_cmd, name="validate")
|
48
|
+
localargo.add_command(up_cmds.up_cmd, name="up")
|
49
|
+
localargo.add_command(up_cmds.down_cmd, name="down")
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present William Born <william.born.git@gmail.com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MIT
|
4
|
+
"""Application management for ArgoCD.
|
5
|
+
|
6
|
+
Thin CLI wrapper delegating to core app orchestration.
|
7
|
+
"""
|
8
|
+
|
9
|
+
# pylint: disable=too-many-arguments
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
import click
|
14
|
+
|
15
|
+
from localargo.core import apps as core_apps
|
16
|
+
|
17
|
+
|
18
|
+
@click.group()
|
19
|
+
def app() -> None:
|
20
|
+
"""Manage ArgoCD applications."""
|
21
|
+
|
22
|
+
|
23
|
+
@app.command("list")
|
24
|
+
@click.option("--profile", default=None, help="Profile overlay to apply")
|
25
|
+
def list_cmd(profile: str | None) -> None:
|
26
|
+
"""List applications from ArgoCD."""
|
27
|
+
core_apps.list_apps(profile=profile)
|
28
|
+
|
29
|
+
|
30
|
+
@app.command()
|
31
|
+
@click.argument("name", required=False)
|
32
|
+
@click.option("--watch", is_flag=True, help="Watch app status")
|
33
|
+
@click.option("--profile", default=None, help="Profile overlay to apply")
|
34
|
+
def status(name: str | None, *, watch: bool, profile: str | None) -> None:
|
35
|
+
"""Show application status (optionally watch)."""
|
36
|
+
core_apps.status(name, watch=watch, profile=profile)
|
37
|
+
|
38
|
+
|
39
|
+
@app.command()
|
40
|
+
@click.argument("name", required=False)
|
41
|
+
@click.option("--all", "all_", is_flag=True, help="Target all apps from catalog")
|
42
|
+
@click.option("--wait/--no-wait", default=True, help="Wait for Healthy after sync")
|
43
|
+
@click.option("--profile", default=None, help="Profile overlay to apply")
|
44
|
+
@click.option(
|
45
|
+
"--kubeconfig",
|
46
|
+
default=None,
|
47
|
+
help="Path to kubeconfig for kubectl-based deployments",
|
48
|
+
)
|
49
|
+
@click.option(
|
50
|
+
"-f",
|
51
|
+
"--file",
|
52
|
+
"manifest_files",
|
53
|
+
multiple=True,
|
54
|
+
help="Manifest file or directory to apply (kubectl -f). Repeatable.",
|
55
|
+
)
|
56
|
+
@click.option("--repo", default=None, help="Git repo URL for ArgoCD app")
|
57
|
+
@click.option("--app-name", "app_name_override", default=None, help="ArgoCD application name")
|
58
|
+
@click.option("--repo-path", default=".", help="Path within repo (default '.')")
|
59
|
+
@click.option("--namespace", default="default", help="Destination namespace")
|
60
|
+
@click.option("--project", default="default", help="ArgoCD project")
|
61
|
+
@click.option(
|
62
|
+
"--type",
|
63
|
+
"app_type",
|
64
|
+
type=click.Choice(["kustomize", "helm"], case_sensitive=False),
|
65
|
+
default="kustomize",
|
66
|
+
help="Application type",
|
67
|
+
)
|
68
|
+
@click.option(
|
69
|
+
"--helm-values",
|
70
|
+
"helm_values",
|
71
|
+
multiple=True,
|
72
|
+
help="Additional Helm values files (only for --type helm). Repeatable.",
|
73
|
+
)
|
74
|
+
def deploy(
|
75
|
+
name: str | None,
|
76
|
+
*,
|
77
|
+
all_: bool,
|
78
|
+
wait: bool,
|
79
|
+
profile: str | None,
|
80
|
+
kubeconfig: str | None,
|
81
|
+
manifest_files: tuple[str, ...],
|
82
|
+
repo: str | None,
|
83
|
+
app_name_override: str | None,
|
84
|
+
repo_path: str,
|
85
|
+
namespace: str,
|
86
|
+
project: str,
|
87
|
+
app_type: str,
|
88
|
+
helm_values: tuple[str, ...],
|
89
|
+
) -> None:
|
90
|
+
"""Create/update and sync one or all apps using catalog."""
|
91
|
+
core_apps.deploy(
|
92
|
+
name,
|
93
|
+
all_=all_,
|
94
|
+
wait=wait,
|
95
|
+
profile=profile,
|
96
|
+
kubeconfig=kubeconfig,
|
97
|
+
manifest_files=list(manifest_files),
|
98
|
+
override_repo=repo,
|
99
|
+
override_name=app_name_override,
|
100
|
+
override_path=repo_path,
|
101
|
+
override_namespace=namespace,
|
102
|
+
override_project=project,
|
103
|
+
override_type=app_type,
|
104
|
+
override_helm_values=list(helm_values),
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
@app.command()
|
109
|
+
@click.argument("name", required=False)
|
110
|
+
@click.option("--all", "all_", is_flag=True, help="Target all apps from catalog")
|
111
|
+
@click.option("--wait/--no-wait", default=True, help="Wait for Healthy after sync")
|
112
|
+
@click.option("--profile", default=None, help="Profile overlay to apply")
|
113
|
+
def sync(name: str | None, *, all_: bool, wait: bool, profile: str | None) -> None:
|
114
|
+
"""Sync one or all apps using catalog."""
|
115
|
+
core_apps.sync(name, all_=all_, wait=wait, profile=profile)
|
116
|
+
|
117
|
+
|
118
|
+
@app.command()
|
119
|
+
@click.argument("name")
|
120
|
+
@click.option("--all-pods", is_flag=True, help="Tail all pods for the app")
|
121
|
+
@click.option("--container", default=None, help="Container name to select")
|
122
|
+
@click.option("--since", default=None, help="Only return logs newer than a relative time")
|
123
|
+
@click.option("--follow/--no-follow", default=True, help="Follow log output")
|
124
|
+
@click.option("--profile", default=None, help="Profile overlay to apply")
|
125
|
+
def logs(
|
126
|
+
name: str,
|
127
|
+
*,
|
128
|
+
all_pods: bool,
|
129
|
+
container: str | None,
|
130
|
+
since: str | None,
|
131
|
+
follow: bool,
|
132
|
+
profile: str | None,
|
133
|
+
) -> None: # pylint: disable=too-many-arguments
|
134
|
+
"""Tail logs for the selected app."""
|
135
|
+
core_apps.logs(
|
136
|
+
name,
|
137
|
+
all_pods=all_pods,
|
138
|
+
container=container,
|
139
|
+
since=since,
|
140
|
+
follow=follow,
|
141
|
+
profile=profile,
|
142
|
+
)
|
143
|
+
|
144
|
+
|
145
|
+
@app.command()
|
146
|
+
@click.argument("name")
|
147
|
+
@click.option("--profile", default=None, help="Profile overlay to apply")
|
148
|
+
def delete(name: str, *, profile: str | None) -> None:
|
149
|
+
"""Delete an application from ArgoCD."""
|
150
|
+
core_apps.delete(name, profile=profile)
|
@@ -0,0 +1,312 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present William Born <william.born.git@gmail.com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MIT
|
4
|
+
"""Cluster management commands for ArgoCD development.
|
5
|
+
|
6
|
+
This module provides commands for managing Kubernetes clusters used for ArgoCD development.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
import base64
|
12
|
+
import subprocess
|
13
|
+
|
14
|
+
import rich_click as click
|
15
|
+
|
16
|
+
from localargo.core.cluster import cluster_manager
|
17
|
+
from localargo.eyecandy.table_renderer import TableRenderer
|
18
|
+
from localargo.logging import logger
|
19
|
+
from localargo.utils.cli import (
|
20
|
+
build_kubectl_get_cmd,
|
21
|
+
ensure_kubectl_available,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
@click.group()
|
26
|
+
def cluster() -> None:
|
27
|
+
"""Manage Kubernetes clusters for ArgoCD development."""
|
28
|
+
|
29
|
+
|
30
|
+
@cluster.command()
|
31
|
+
@click.option("--context", "-c", help="Specific context to use")
|
32
|
+
@click.option("--namespace", "-n", default="argocd", help="ArgoCD namespace")
|
33
|
+
def status(context: str | None, namespace: str) -> None:
|
34
|
+
"""Show current cluster and ArgoCD status."""
|
35
|
+
kubectl_path = ensure_kubectl_available()
|
36
|
+
|
37
|
+
try:
|
38
|
+
cluster_status = _get_cluster_status(context)
|
39
|
+
argocd_installed = _check_argocd_installation(kubectl_path, namespace)
|
40
|
+
|
41
|
+
_display_cluster_status(cluster_status, namespace, argocd_installed=argocd_installed)
|
42
|
+
|
43
|
+
if argocd_installed:
|
44
|
+
_display_argocd_pods_status(kubectl_path, namespace)
|
45
|
+
else:
|
46
|
+
_show_argocd_not_found_message(namespace)
|
47
|
+
|
48
|
+
except subprocess.CalledProcessError as e:
|
49
|
+
logger.error("Error checking cluster status: %s", e)
|
50
|
+
except FileNotFoundError:
|
51
|
+
logger.error("kubectl not found. Please install kubectl.")
|
52
|
+
|
53
|
+
|
54
|
+
def _get_cluster_status(context: str | None) -> dict[str, str | bool]:
|
55
|
+
"""Get cluster status information."""
|
56
|
+
if context:
|
57
|
+
logger.info("Using context: %s", context)
|
58
|
+
return {"context": context, "ready": True} # Assume ready if specified
|
59
|
+
|
60
|
+
cluster_status = cluster_manager.get_cluster_status()
|
61
|
+
logger.info("Current context: %s", cluster_status.get("context", "unknown"))
|
62
|
+
return cluster_status
|
63
|
+
|
64
|
+
|
65
|
+
def _check_argocd_installation(kubectl_path: str, namespace: str) -> bool:
|
66
|
+
"""Check if ArgoCD is installed in the specified namespace."""
|
67
|
+
result = subprocess.run(
|
68
|
+
[
|
69
|
+
kubectl_path,
|
70
|
+
"get",
|
71
|
+
"deployment",
|
72
|
+
"argocd-server",
|
73
|
+
"-n",
|
74
|
+
namespace,
|
75
|
+
"--ignore-not-found",
|
76
|
+
],
|
77
|
+
capture_output=True,
|
78
|
+
text=True,
|
79
|
+
check=False,
|
80
|
+
)
|
81
|
+
return bool(result.returncode == 0 and result.stdout.strip())
|
82
|
+
|
83
|
+
|
84
|
+
def _display_cluster_status(
|
85
|
+
cluster_status: dict[str, str | bool], namespace: str, *, argocd_installed: bool
|
86
|
+
) -> None:
|
87
|
+
"""Display the cluster status information."""
|
88
|
+
status_data = {
|
89
|
+
"Cluster Context": cluster_status.get("context", "unknown"),
|
90
|
+
"Cluster Ready": "Yes" if cluster_status.get("ready", False) else "No",
|
91
|
+
"ArgoCD Status": "Installed" if argocd_installed else "Not Found",
|
92
|
+
"Namespace": namespace,
|
93
|
+
}
|
94
|
+
|
95
|
+
table_renderer = TableRenderer()
|
96
|
+
table_renderer.render_key_values("Cluster Status", status_data)
|
97
|
+
|
98
|
+
|
99
|
+
def _display_argocd_pods_status(kubectl_path: str, namespace: str) -> None:
|
100
|
+
"""Display ArgoCD pods status if available."""
|
101
|
+
logger.info("✅ ArgoCD found in namespace: %s", namespace)
|
102
|
+
|
103
|
+
pod_result = subprocess.run(
|
104
|
+
build_kubectl_get_cmd(
|
105
|
+
kubectl_path,
|
106
|
+
"pods",
|
107
|
+
namespace,
|
108
|
+
label_selector="app.kubernetes.io/name=argocd-server",
|
109
|
+
output_format=(
|
110
|
+
"custom-columns=NAME:.metadata.name,"
|
111
|
+
"STATUS:.status.phase,"
|
112
|
+
"READY:.status.containerStatuses[0].ready"
|
113
|
+
),
|
114
|
+
),
|
115
|
+
check=False,
|
116
|
+
capture_output=True,
|
117
|
+
text=True,
|
118
|
+
)
|
119
|
+
|
120
|
+
if pod_result.returncode == 0 and pod_result.stdout.strip():
|
121
|
+
pod_lines = pod_result.stdout.strip().split("\n")[1:] # Skip header
|
122
|
+
if pod_lines:
|
123
|
+
table_renderer = TableRenderer()
|
124
|
+
table_renderer.render_simple_list(
|
125
|
+
[line for line in pod_lines if line.strip()], "ArgoCD Pods"
|
126
|
+
)
|
127
|
+
|
128
|
+
|
129
|
+
def _show_argocd_not_found_message(namespace: str) -> None:
|
130
|
+
"""Show message when ArgoCD is not found."""
|
131
|
+
logger.warning("❌ ArgoCD not found in namespace: %s", namespace)
|
132
|
+
logger.info("Run 'localargo cluster init' to set up ArgoCD locally")
|
133
|
+
|
134
|
+
|
135
|
+
@cluster.command()
|
136
|
+
@click.option(
|
137
|
+
"--provider",
|
138
|
+
type=click.Choice(["kind", "k3s"]),
|
139
|
+
default="kind",
|
140
|
+
help="Local cluster provider",
|
141
|
+
)
|
142
|
+
@click.option("--name", default="localargo", help="Cluster name")
|
143
|
+
@click.argument("cluster_name", required=False)
|
144
|
+
def init(provider: str, name: str, cluster_name: str | None) -> None:
|
145
|
+
"""Initialize a local Kubernetes cluster with ArgoCD."""
|
146
|
+
effective_name = cluster_name or name
|
147
|
+
if _do_create_cluster(provider, effective_name):
|
148
|
+
_log_kind_hints_if_applicable(provider)
|
149
|
+
|
150
|
+
|
151
|
+
def _do_create_cluster(provider: str, name: str) -> bool:
|
152
|
+
"""Create cluster with error handling and logging. Returns success flag."""
|
153
|
+
logger.info("Initializing %s cluster '%s' with ArgoCD...", provider, name)
|
154
|
+
|
155
|
+
try:
|
156
|
+
success = cluster_manager.create_cluster(provider, name)
|
157
|
+
except subprocess.CalledProcessError as e:
|
158
|
+
logger.error("❌ Creating cluster failed with return code %s", e.returncode)
|
159
|
+
if e.stderr:
|
160
|
+
logger.error("Error details: %s", e.stderr.strip())
|
161
|
+
return False
|
162
|
+
except (OSError, ValueError) as e:
|
163
|
+
logger.error("Error creating cluster: %s", e)
|
164
|
+
return False
|
165
|
+
|
166
|
+
if success:
|
167
|
+
logger.info("✅ %s cluster '%s' created successfully", provider.upper(), name)
|
168
|
+
return True
|
169
|
+
|
170
|
+
logger.error("Failed to create %s cluster '%s'", provider, name)
|
171
|
+
return False
|
172
|
+
|
173
|
+
|
174
|
+
def _log_kind_hints_if_applicable(provider: str) -> None:
|
175
|
+
"""Log helpful hints when using kind provider."""
|
176
|
+
if provider != "kind":
|
177
|
+
return
|
178
|
+
logger.info(
|
179
|
+
"🌐 ArgoCD UI will be available at: "
|
180
|
+
"https://argocd.localtest.me (after installation)"
|
181
|
+
)
|
182
|
+
logger.info("🔧 Development ports available: 30000-30002")
|
183
|
+
logger.info("🚀 Cluster configured with direct port access to ingress controller")
|
184
|
+
|
185
|
+
|
186
|
+
@cluster.command()
|
187
|
+
@click.argument("context_name")
|
188
|
+
def switch(context_name: str) -> None:
|
189
|
+
"""Switch to a different Kubernetes context."""
|
190
|
+
if cluster_manager.switch_context(context_name):
|
191
|
+
logger.info("✅ Switched to context: %s", context_name)
|
192
|
+
else:
|
193
|
+
logger.error("Context '%s' not found", context_name)
|
194
|
+
|
195
|
+
|
196
|
+
@cluster.command()
|
197
|
+
def list_contexts() -> None:
|
198
|
+
"""List available Kubernetes contexts."""
|
199
|
+
contexts = cluster_manager.get_contexts()
|
200
|
+
if contexts:
|
201
|
+
logger.info("Available contexts:")
|
202
|
+
for ctx in contexts:
|
203
|
+
logger.info(" %s", ctx)
|
204
|
+
else:
|
205
|
+
logger.error("No contexts found or kubectl not available")
|
206
|
+
|
207
|
+
|
208
|
+
@cluster.command(name="list")
|
209
|
+
def list_clusters() -> None:
|
210
|
+
"""List available clusters across providers."""
|
211
|
+
clusters = cluster_manager.list_clusters()
|
212
|
+
|
213
|
+
rows: list[dict[str, str]] = []
|
214
|
+
for c in clusters:
|
215
|
+
provider = str(c.get("provider", ""))
|
216
|
+
name = str(c.get("name", ""))
|
217
|
+
cluster_status = (
|
218
|
+
"ready"
|
219
|
+
if c.get("ready", False)
|
220
|
+
else ("exists" if c.get("exists", False) else "missing")
|
221
|
+
)
|
222
|
+
context = str(c.get("context", ""))
|
223
|
+
rows.append(
|
224
|
+
{
|
225
|
+
"name": name,
|
226
|
+
"provider": provider,
|
227
|
+
"status": cluster_status,
|
228
|
+
"context": context,
|
229
|
+
}
|
230
|
+
)
|
231
|
+
|
232
|
+
renderer = TableRenderer()
|
233
|
+
if rows:
|
234
|
+
renderer.render_status_table(rows)
|
235
|
+
else:
|
236
|
+
renderer.render_simple_list([], "Clusters")
|
237
|
+
|
238
|
+
|
239
|
+
@cluster.command()
|
240
|
+
@click.argument("name")
|
241
|
+
@click.option(
|
242
|
+
"--provider",
|
243
|
+
type=click.Choice(["kind", "k3s"]),
|
244
|
+
default="kind",
|
245
|
+
help="Cluster provider",
|
246
|
+
)
|
247
|
+
def delete(name: str, provider: str) -> None:
|
248
|
+
"""Delete a specific cluster."""
|
249
|
+
logger.info("Deleting %s cluster '%s'...", provider, name)
|
250
|
+
|
251
|
+
success = cluster_manager.delete_cluster(provider, name)
|
252
|
+
if success:
|
253
|
+
logger.info("✅ Cluster '%s' deleted successfully", name)
|
254
|
+
else:
|
255
|
+
error_msg = f"Failed to delete cluster '{name}'"
|
256
|
+
logger.error("❌ %s", error_msg)
|
257
|
+
raise click.ClickException(error_msg)
|
258
|
+
|
259
|
+
|
260
|
+
@cluster.command()
|
261
|
+
@click.argument("name")
|
262
|
+
@click.option(
|
263
|
+
"--provider",
|
264
|
+
type=click.Choice(["kind", "k3s"]),
|
265
|
+
default="kind",
|
266
|
+
help="Cluster provider",
|
267
|
+
)
|
268
|
+
def password(name: str, provider: str) -> None:
|
269
|
+
"""Get ArgoCD initial admin password for a cluster."""
|
270
|
+
# Check if kubectl is available
|
271
|
+
kubectl_path = ensure_kubectl_available()
|
272
|
+
|
273
|
+
logger.info("Getting ArgoCD password for %s cluster '%s'...", provider, name)
|
274
|
+
|
275
|
+
try:
|
276
|
+
# Switch to the cluster context if needed
|
277
|
+
cluster_manager.switch_context(f"{provider}-{name}")
|
278
|
+
|
279
|
+
# Get the ArgoCD initial admin secret
|
280
|
+
result = subprocess.run(
|
281
|
+
[
|
282
|
+
kubectl_path,
|
283
|
+
"get",
|
284
|
+
"secret",
|
285
|
+
"argocd-initial-admin-secret",
|
286
|
+
"-n",
|
287
|
+
"argocd",
|
288
|
+
"-o",
|
289
|
+
"jsonpath={.data.password}",
|
290
|
+
],
|
291
|
+
capture_output=True,
|
292
|
+
text=True,
|
293
|
+
check=True,
|
294
|
+
)
|
295
|
+
|
296
|
+
if result.stdout:
|
297
|
+
# Decode the base64 password
|
298
|
+
decoded_password = base64.b64decode(result.stdout.strip()).decode("utf-8")
|
299
|
+
logger.info("🔐 ArgoCD Login Credentials:")
|
300
|
+
logger.info(" Username: admin")
|
301
|
+
logger.info(" Password: %s", decoded_password)
|
302
|
+
logger.info(" URL: https://argocd.localtest.me")
|
303
|
+
else:
|
304
|
+
logger.error("❌ No password found in ArgoCD initial admin secret")
|
305
|
+
logger.info("💡 Make sure ArgoCD is installed and the initial admin secret exists")
|
306
|
+
|
307
|
+
except subprocess.CalledProcessError as e:
|
308
|
+
logger.error("❌ Failed to get ArgoCD password: %s", e)
|
309
|
+
if "NotFound" in e.stderr:
|
310
|
+
logger.info("💡 ArgoCD may not be installed or the cluster may not exist")
|
311
|
+
error_msg = "Failed to retrieve ArgoCD password"
|
312
|
+
raise click.ClickException(error_msg) from e
|