vantage-cli 0.1.1__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.
- vantage_cli/__init__.py +131 -0
- vantage_cli/apps/__init__.py +22 -0
- vantage_cli/apps/common.py +78 -0
- vantage_cli/apps/juju_localhost/__init__.py +17 -0
- vantage_cli/apps/juju_localhost/app.py +255 -0
- vantage_cli/apps/juju_localhost/bundle_yaml.py +143 -0
- vantage_cli/apps/microk8s/README.md +47 -0
- vantage_cli/apps/microk8s/__init__.py +3 -0
- vantage_cli/apps/microk8s/app.py +301 -0
- vantage_cli/apps/multipass_singlenode/__init__.py +12 -0
- vantage_cli/apps/multipass_singlenode/app.py +173 -0
- vantage_cli/apps/templates.py +178 -0
- vantage_cli/auth.py +429 -0
- vantage_cli/cache.py +143 -0
- vantage_cli/client.py +84 -0
- vantage_cli/command_base.py +63 -0
- vantage_cli/commands/__init__.py +1 -0
- vantage_cli/commands/clouds/__init__.py +20 -0
- vantage_cli/commands/clouds/add.py +81 -0
- vantage_cli/commands/clouds/delete.py +61 -0
- vantage_cli/commands/clouds/render.py +146 -0
- vantage_cli/commands/clouds/update.py +97 -0
- vantage_cli/commands/clusters/__init__.py +27 -0
- vantage_cli/commands/clusters/create.py +270 -0
- vantage_cli/commands/clusters/delete.py +101 -0
- vantage_cli/commands/clusters/get.py +30 -0
- vantage_cli/commands/clusters/list.py +84 -0
- vantage_cli/commands/clusters/render.py +233 -0
- vantage_cli/commands/clusters/schema.py +31 -0
- vantage_cli/commands/clusters/utils.py +248 -0
- vantage_cli/commands/profile/__init__.py +30 -0
- vantage_cli/commands/profile/crud.py +529 -0
- vantage_cli/commands/profile/render.py +55 -0
- vantage_cli/config.py +161 -0
- vantage_cli/constants.py +40 -0
- vantage_cli/exceptions.py +127 -0
- vantage_cli/format.py +39 -0
- vantage_cli/gql_client.py +655 -0
- vantage_cli/main.py +303 -0
- vantage_cli/render.py +56 -0
- vantage_cli/schemas.py +48 -0
- vantage_cli/time_loop.py +124 -0
- vantage_cli-0.1.1.dist-info/METADATA +30 -0
- vantage_cli-0.1.1.dist-info/RECORD +46 -0
- vantage_cli-0.1.1.dist-info/WHEEL +4 -0
- vantage_cli-0.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# © 2025 Vantage Compute, Inc. All rights reserved.
|
|
2
|
+
# Confidential and proprietary. Unauthorized use prohibited.
|
|
3
|
+
"""Render helpers for cluster command output formatting."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from rich import print_json
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from vantage_cli.render import StyleMapper
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_clusters_table(
|
|
16
|
+
clusters: List[Dict[str, Any]],
|
|
17
|
+
title: str = "Clusters List",
|
|
18
|
+
total_count: Optional[int] = None,
|
|
19
|
+
json_output: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Render a list of clusters in a Rich table format.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
clusters: List of cluster dictionaries
|
|
25
|
+
title: Title for the table
|
|
26
|
+
total_count: Total number of clusters available
|
|
27
|
+
json_output: If True, output as JSON instead of a table
|
|
28
|
+
"""
|
|
29
|
+
if json_output:
|
|
30
|
+
# Output as JSON with Rich formatting
|
|
31
|
+
output = {"clusters": clusters, "total": total_count or len(clusters)}
|
|
32
|
+
print_json(data=output)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if not clusters:
|
|
36
|
+
console = Console()
|
|
37
|
+
console.print()
|
|
38
|
+
console.print(Panel("No clusters found.", title="[yellow]No Results"))
|
|
39
|
+
console.print()
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Define cluster-specific styling
|
|
43
|
+
style_mapper = StyleMapper(
|
|
44
|
+
name="bold cyan",
|
|
45
|
+
status="green",
|
|
46
|
+
provider="blue",
|
|
47
|
+
ownerEmail="yellow",
|
|
48
|
+
clientId="white",
|
|
49
|
+
description="dim white",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Create the table
|
|
53
|
+
table = Table(
|
|
54
|
+
title=title,
|
|
55
|
+
caption=f"Items: {len(clusters)}{f' of {total_count}' if total_count else ''}",
|
|
56
|
+
show_header=True,
|
|
57
|
+
header_style="bold white",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Add columns based on the first cluster's keys
|
|
61
|
+
first_cluster = clusters[0]
|
|
62
|
+
|
|
63
|
+
# Define column order and display names
|
|
64
|
+
column_mapping = {
|
|
65
|
+
"name": "Name",
|
|
66
|
+
"status": "Status",
|
|
67
|
+
"provider": "Provider",
|
|
68
|
+
"ownerEmail": "Owner Email",
|
|
69
|
+
"clientId": "Client ID",
|
|
70
|
+
"description": "Description",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Add columns in the specified order
|
|
74
|
+
for key, display_name in column_mapping.items():
|
|
75
|
+
if key in first_cluster:
|
|
76
|
+
table.add_column(display_name, **style_mapper.map_style(key))
|
|
77
|
+
|
|
78
|
+
# Add rows
|
|
79
|
+
for cluster in clusters:
|
|
80
|
+
row_values = []
|
|
81
|
+
for key in column_mapping.keys():
|
|
82
|
+
if key in cluster:
|
|
83
|
+
value = cluster.get(key, "")
|
|
84
|
+
# Handle None values and long descriptions
|
|
85
|
+
if value is None:
|
|
86
|
+
value = ""
|
|
87
|
+
elif key == "description" and isinstance(value, str) and len(value) > 50:
|
|
88
|
+
value = value[:47] + "..."
|
|
89
|
+
row_values.append(str(value))
|
|
90
|
+
table.add_row(*row_values)
|
|
91
|
+
|
|
92
|
+
# Print the table
|
|
93
|
+
console = Console()
|
|
94
|
+
console.print()
|
|
95
|
+
console.print(table)
|
|
96
|
+
console.print()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def render_cluster_details(cluster: Dict[str, Any], json_output: bool = False) -> None:
|
|
100
|
+
"""Render detailed information for a single cluster.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
cluster: Cluster dictionary with details
|
|
104
|
+
json_output: If True, output as JSON instead of a formatted view
|
|
105
|
+
"""
|
|
106
|
+
if json_output:
|
|
107
|
+
# Output as JSON with Rich formatting
|
|
108
|
+
print_json(data=cluster)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if not cluster:
|
|
112
|
+
console = Console()
|
|
113
|
+
console.print()
|
|
114
|
+
console.print(Panel("Cluster not found.", title="[red]Not Found"))
|
|
115
|
+
console.print()
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
console = Console()
|
|
119
|
+
cluster_name = cluster.get("name", "Unknown")
|
|
120
|
+
|
|
121
|
+
# Create a table matching the profile get command style
|
|
122
|
+
table = Table(
|
|
123
|
+
title=f"Cluster Details: {cluster_name}", show_header=True, header_style="bold white"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
table.add_column("Property", style="bold cyan")
|
|
127
|
+
table.add_column("Value", style="white")
|
|
128
|
+
|
|
129
|
+
# Add basic cluster information
|
|
130
|
+
table.add_row("Name", cluster.get("name", "N/A"))
|
|
131
|
+
table.add_row("Status", cluster.get("status", "N/A"))
|
|
132
|
+
table.add_row("Provider", cluster.get("provider", "N/A"))
|
|
133
|
+
table.add_row("Owner Email", cluster.get("ownerEmail", "N/A"))
|
|
134
|
+
table.add_row("Client ID", cluster.get("clientId", "N/A"))
|
|
135
|
+
|
|
136
|
+
# Add client secret if available
|
|
137
|
+
client_secret = cluster.get("client_secret")
|
|
138
|
+
if client_secret:
|
|
139
|
+
table.add_row("Client Secret", f"[dim]{client_secret}[/dim]")
|
|
140
|
+
else:
|
|
141
|
+
table.add_row("Client Secret", "[dim]Not available[/dim]")
|
|
142
|
+
|
|
143
|
+
table.add_row("Description", cluster.get("description", "N/A"))
|
|
144
|
+
|
|
145
|
+
cloud_account_id = cluster.get("cloudAccountId")
|
|
146
|
+
table.add_row(
|
|
147
|
+
"Cloud Account ID", str(cloud_account_id) if cloud_account_id is not None else "None"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Add creation parameters if available
|
|
151
|
+
creation_params = cluster.get("creationParameters", {})
|
|
152
|
+
if creation_params:
|
|
153
|
+
# Add a separator row
|
|
154
|
+
table.add_row("", "") # Empty row for spacing
|
|
155
|
+
table.add_row("[bold]Creation Parameters[/bold]", "")
|
|
156
|
+
|
|
157
|
+
for key, value in creation_params.items():
|
|
158
|
+
if value is not None:
|
|
159
|
+
# Format key for display
|
|
160
|
+
display_key = key.replace("_", " ").title()
|
|
161
|
+
display_value = str(value) if not isinstance(value, dict) else "Complex Object"
|
|
162
|
+
table.add_row(f" {display_key}", display_value)
|
|
163
|
+
|
|
164
|
+
console.print()
|
|
165
|
+
console.print(table)
|
|
166
|
+
console.print()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def render_cluster_creation_result(cluster: Dict[str, Any], json_output: bool = False) -> None:
|
|
170
|
+
"""Render the result of cluster creation.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
cluster: Created cluster dictionary
|
|
174
|
+
json_output: If True, output as JSON instead of a formatted view
|
|
175
|
+
"""
|
|
176
|
+
if json_output:
|
|
177
|
+
print_json(data=cluster)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
console = Console()
|
|
181
|
+
console.print()
|
|
182
|
+
console.print(
|
|
183
|
+
Panel(
|
|
184
|
+
f"✅ Cluster '[bold cyan]{cluster.get('name', 'Unknown')}[/bold cyan]' created successfully!",
|
|
185
|
+
title="[green]Cluster Created[/green]",
|
|
186
|
+
border_style="green",
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Show basic details
|
|
191
|
+
render_cluster_details(cluster, json_output=False)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def render_cluster_deletion_result(
|
|
195
|
+
cluster_name: str, success: bool = True, json_output: bool = False
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Render the result of cluster deletion.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
cluster_name: Name of the deleted cluster
|
|
201
|
+
success: Whether deletion was successful
|
|
202
|
+
json_output: If True, output as JSON instead of a formatted view
|
|
203
|
+
"""
|
|
204
|
+
if json_output:
|
|
205
|
+
result = {
|
|
206
|
+
"cluster_name": cluster_name,
|
|
207
|
+
"deleted": success,
|
|
208
|
+
"message": f"Cluster '{cluster_name}' {'deleted' if success else 'deletion failed'}",
|
|
209
|
+
}
|
|
210
|
+
print_json(data=result)
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
console = Console()
|
|
214
|
+
console.print()
|
|
215
|
+
|
|
216
|
+
if success:
|
|
217
|
+
console.print(
|
|
218
|
+
Panel(
|
|
219
|
+
f"✅ Cluster '[bold cyan]{cluster_name}[/bold cyan]' deleted successfully!",
|
|
220
|
+
title="[green]Cluster Deleted[/green]",
|
|
221
|
+
border_style="green",
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
console.print(
|
|
226
|
+
Panel(
|
|
227
|
+
f"❌ Failed to delete cluster '[bold cyan]{cluster_name}[/bold cyan]'",
|
|
228
|
+
title="[red]Deletion Failed[/red]",
|
|
229
|
+
border_style="red",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
console.print()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Schema definitions for cluster commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VantageClusterContext(BaseModel):
|
|
9
|
+
"""Vantage cluster context for generating juju configuration."""
|
|
10
|
+
|
|
11
|
+
client_id: str
|
|
12
|
+
client_secret: str
|
|
13
|
+
oidc_domain: str
|
|
14
|
+
oidc_base_url: str
|
|
15
|
+
base_api_url: str
|
|
16
|
+
tunnel_api_url: str
|
|
17
|
+
jupyterhub_token: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClusterDetailSchema(BaseModel):
|
|
21
|
+
"""Schema for detailed cluster information."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
status: str
|
|
25
|
+
client_id: str
|
|
26
|
+
client_secret: Optional[str] = None
|
|
27
|
+
description: str
|
|
28
|
+
owner_mail: str
|
|
29
|
+
provider: str
|
|
30
|
+
cloud_account_id: Optional[str] = None
|
|
31
|
+
creation_parameters: Dict[str, Any]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# © 2025 Vantage Compute, Inc. All rights reserved.
|
|
2
|
+
# Confidential and proprietary. Unauthorized use prohibited.
|
|
3
|
+
"""Shared utilities for cluster commands."""
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import logging
|
|
7
|
+
import textwrap
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import typer
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
from vantage_cli.auth import extract_persona
|
|
16
|
+
from vantage_cli.config import Settings
|
|
17
|
+
from vantage_cli.exceptions import Abort
|
|
18
|
+
from vantage_cli.gql_client import create_async_graphql_client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_available_apps() -> Dict[str, Dict[str, Any]]:
|
|
22
|
+
"""Dynamically discover available deployment apps."""
|
|
23
|
+
apps: Dict[str, Dict[str, Any]] = {}
|
|
24
|
+
|
|
25
|
+
# Register the apps maintained with the vantage cli
|
|
26
|
+
apps_dir = Path(__file__).parent.parent.parent / "apps"
|
|
27
|
+
|
|
28
|
+
if not apps_dir.exists():
|
|
29
|
+
logging.warning(f"Apps directory not found: {apps_dir}")
|
|
30
|
+
return apps
|
|
31
|
+
|
|
32
|
+
# Scan each app directory
|
|
33
|
+
for app_path in apps_dir.iterdir():
|
|
34
|
+
if app_path.is_dir() and not app_path.name.startswith("__"):
|
|
35
|
+
app_name = app_path.name
|
|
36
|
+
app_module_path = app_path / "app.py"
|
|
37
|
+
|
|
38
|
+
if app_module_path.exists():
|
|
39
|
+
# Convert directory name to the command name (e.g., "juju_localhost" -> "juju-localhost")
|
|
40
|
+
command_name = app_name.replace("_", "-")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Dynamically import the app module
|
|
44
|
+
app_module = importlib.import_module(f"vantage_cli.apps.{app_name}.app")
|
|
45
|
+
|
|
46
|
+
# Check if deploy function exists
|
|
47
|
+
if hasattr(app_module, "deploy"):
|
|
48
|
+
deploy_function = getattr(app_module, "deploy")
|
|
49
|
+
# Use command_name (with hyphens) as key so CLI can find it
|
|
50
|
+
apps[command_name] = {
|
|
51
|
+
"module": app_module,
|
|
52
|
+
"deploy_function": deploy_function,
|
|
53
|
+
}
|
|
54
|
+
except (ImportError, AttributeError, ModuleNotFoundError) as e:
|
|
55
|
+
logger.warning(f"Failed to import app '{app_name}': {e}")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Unexpected error importing app '{app_name}': {e}")
|
|
58
|
+
logger.debug(f"Full traceback for {app_name}", exc_info=True)
|
|
59
|
+
|
|
60
|
+
return apps
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_cloud_choices() -> list[str]:
|
|
64
|
+
"""Get the list of supported clouds from settings."""
|
|
65
|
+
settings = Settings()
|
|
66
|
+
return settings.supported_clouds
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_app_choices() -> list[str]:
|
|
70
|
+
"""Get the list of available deployment apps."""
|
|
71
|
+
try:
|
|
72
|
+
apps = get_available_apps()
|
|
73
|
+
return list(apps.keys())
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.warning(f"Failed to get available apps: {e}")
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def get_cluster_client_secret(ctx: typer.Context, client_id: str) -> Optional[str]:
|
|
80
|
+
"""Get the client secret for the cluster from vantage-api using GraphQL client auth.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
ctx: Typer context carrying settings/profile
|
|
84
|
+
client_id: The client ID of the cluster
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The client secret if found, None otherwise
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
# Get user authentication using the same method as GraphQL client
|
|
91
|
+
persona = extract_persona(ctx.obj.profile)
|
|
92
|
+
access_token = persona.token_set.access_token
|
|
93
|
+
|
|
94
|
+
# Use vantage-api admin/management/clients endpoint
|
|
95
|
+
api_url = f"{ctx.obj.settings.api_base_url}/admin/management/clients"
|
|
96
|
+
|
|
97
|
+
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
|
|
98
|
+
|
|
99
|
+
async with httpx.AsyncClient() as client:
|
|
100
|
+
# First, search for the client by clientId
|
|
101
|
+
params = {"client_id": client_id}
|
|
102
|
+
logger.debug(f"Searching for client with ID: {client_id} at {api_url}")
|
|
103
|
+
response = await client.get(api_url, headers=headers, params=params)
|
|
104
|
+
|
|
105
|
+
if response.status_code != 200:
|
|
106
|
+
logger.error(
|
|
107
|
+
f"Failed to query vantage-api clients: {response.status_code} - {response.text}"
|
|
108
|
+
)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
response_data = response.json()
|
|
112
|
+
clients = response_data.get("clients", [])
|
|
113
|
+
|
|
114
|
+
if not clients:
|
|
115
|
+
logger.warning(f"No client found with clientId: {client_id}")
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
# Get the first matching client's internal ID
|
|
119
|
+
vantage_client = clients[0]
|
|
120
|
+
internal_id = vantage_client.get("id")
|
|
121
|
+
|
|
122
|
+
if not internal_id:
|
|
123
|
+
logger.error(f"No internal ID found for client: {client_id}")
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
# Get the client secret using the internal ID
|
|
127
|
+
secret_url = f"{api_url}/{internal_id}"
|
|
128
|
+
logger.debug(f"Fetching client secret from: {secret_url}")
|
|
129
|
+
secret_response = await client.get(secret_url, headers=headers)
|
|
130
|
+
|
|
131
|
+
if secret_response.status_code != 200:
|
|
132
|
+
logger.error(
|
|
133
|
+
f"Failed to get client secret: {secret_response.status_code} - {secret_response.text}"
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
secret_data = secret_response.json()
|
|
138
|
+
logger.debug(f"Client secret response data: {secret_data}")
|
|
139
|
+
# Try both camelCase and snake_case field names
|
|
140
|
+
client_secret = secret_data.get("client_secret")
|
|
141
|
+
|
|
142
|
+
if client_secret:
|
|
143
|
+
logger.debug(f"Successfully retrieved client secret for {client_id}")
|
|
144
|
+
else:
|
|
145
|
+
logger.warning(f"Client secret not found in response for {client_id}")
|
|
146
|
+
logger.debug(f"Available keys in response: {list(secret_data.keys())}")
|
|
147
|
+
|
|
148
|
+
return client_secret
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Error retrieving client secret for {client_id}: {e}")
|
|
152
|
+
typer.Exit(code=1)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def get_cluster_by_name(ctx: typer.Context, cluster_name: str) -> Dict[str, Any] | None:
|
|
156
|
+
"""Get cluster details by name from vantage-api using GraphQL client auth.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
ctx: Typer context carrying settings/profile
|
|
160
|
+
cluster_name: The name of the cluster to retrieve
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The cluster data if found, None otherwise
|
|
164
|
+
"""
|
|
165
|
+
# Ensure we have settings configured
|
|
166
|
+
if not ctx.obj or not ctx.obj.settings:
|
|
167
|
+
raise Abort(
|
|
168
|
+
"No settings configured. Please run 'vantage config set' first.",
|
|
169
|
+
subject="Configuration Required",
|
|
170
|
+
log_message="Settings not configured",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
query = textwrap.dedent("""\
|
|
174
|
+
query getClusters($first: Int!) {
|
|
175
|
+
clusters(first: $first) {
|
|
176
|
+
edges {
|
|
177
|
+
node {
|
|
178
|
+
name
|
|
179
|
+
status
|
|
180
|
+
clientId
|
|
181
|
+
description
|
|
182
|
+
ownerEmail
|
|
183
|
+
provider
|
|
184
|
+
cloudAccountId
|
|
185
|
+
creationParameters
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
""")
|
|
191
|
+
|
|
192
|
+
variables = {"first": 100} # Fetch up to 100 clusters
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Create async GraphQL client
|
|
196
|
+
graphql_client = create_async_graphql_client(ctx.obj.settings, ctx.obj.profile)
|
|
197
|
+
|
|
198
|
+
# Execute the query
|
|
199
|
+
logger.debug(f"Executing cluster get query for: {cluster_name}")
|
|
200
|
+
response_data = await graphql_client.execute_async(query, variables)
|
|
201
|
+
|
|
202
|
+
# Extract cluster data
|
|
203
|
+
clusters_data = response_data.get("clusters", {})
|
|
204
|
+
clusters = [edge["node"] for edge in clusters_data.get("edges", [])]
|
|
205
|
+
|
|
206
|
+
# Filter clusters by name (case-insensitive)
|
|
207
|
+
matching_clusters = [
|
|
208
|
+
cluster
|
|
209
|
+
for cluster in clusters
|
|
210
|
+
if cluster.get("name", "").lower() == cluster_name.lower()
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
if not matching_clusters:
|
|
214
|
+
raise Abort(
|
|
215
|
+
f"No cluster found with name '{cluster_name}'.",
|
|
216
|
+
subject="Cluster Not Found",
|
|
217
|
+
log_message=f"Cluster '{cluster_name}' not found",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Get the first (and should be only) cluster
|
|
221
|
+
cluster = matching_clusters[0]
|
|
222
|
+
|
|
223
|
+
# Fetch client secret from API if clientId is available
|
|
224
|
+
client_id = cluster.get("clientId")
|
|
225
|
+
if client_id:
|
|
226
|
+
try:
|
|
227
|
+
client_secret = await get_cluster_client_secret(ctx, client_id)
|
|
228
|
+
cluster["client_secret"] = client_secret
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.warning(
|
|
231
|
+
f"Failed to retrieve client secret for cluster '{cluster_name}': {e}"
|
|
232
|
+
)
|
|
233
|
+
cluster["client_secret"] = None
|
|
234
|
+
else:
|
|
235
|
+
cluster["client_secret"] = None
|
|
236
|
+
|
|
237
|
+
return cluster
|
|
238
|
+
|
|
239
|
+
except Abort:
|
|
240
|
+
# Re-raise Abort exceptions as they contain user-friendly messages
|
|
241
|
+
raise
|
|
242
|
+
except (httpx.RequestError, ValueError, KeyError, AttributeError) as e:
|
|
243
|
+
logger.error(f"Unexpected error getting cluster '{cluster_name}': {e}")
|
|
244
|
+
raise Abort(
|
|
245
|
+
f"An unexpected error occurred while getting cluster '{cluster_name}'.",
|
|
246
|
+
subject="Unexpected Error",
|
|
247
|
+
log_message=f"Unexpected error: {e}",
|
|
248
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# © 2025 Vantage Compute, Inc. All rights reserved.
|
|
2
|
+
# Confidential and proprietary. Unauthorized use prohibited.
|
|
3
|
+
"""Profile management commands for Vantage CLI."""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from .crud import create_profile, delete_profile, get_profile, list_profiles, use_profile
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Create the profile app
|
|
13
|
+
profile_app = typer.Typer(
|
|
14
|
+
name="profile",
|
|
15
|
+
help="Manage Vantage CLI profiles to work with different environments and configurations.",
|
|
16
|
+
invoke_without_command=True,
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
context_settings={
|
|
19
|
+
"allow_extra_args": True,
|
|
20
|
+
"allow_interspersed_args": True,
|
|
21
|
+
"ignore_unknown_options": True,
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Register subcommands
|
|
26
|
+
profile_app.command("create")(create_profile)
|
|
27
|
+
profile_app.command("delete")(delete_profile)
|
|
28
|
+
profile_app.command("get")(get_profile)
|
|
29
|
+
profile_app.command("list")(list_profiles)
|
|
30
|
+
profile_app.command("use")(use_profile)
|