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
vantage_cli/cache.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Token caching functionality for the Vantage CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from vantage_cli.constants import USER_TOKEN_CACHE_DIR
|
|
12
|
+
from vantage_cli.exceptions import Abort
|
|
13
|
+
from vantage_cli.schemas import TokenSet
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def init_cache(cache_dir: Path) -> None:
|
|
17
|
+
"""Initialize cache directory."""
|
|
18
|
+
try:
|
|
19
|
+
USER_TOKEN_CACHE_DIR.mkdir(exist_ok=True, parents=True)
|
|
20
|
+
token_dir = USER_TOKEN_CACHE_DIR / "token"
|
|
21
|
+
token_dir.mkdir(exist_ok=True)
|
|
22
|
+
info_file = USER_TOKEN_CACHE_DIR / "info.txt"
|
|
23
|
+
info_file.write_text("This directory is used by Vantage CLI for its cache.")
|
|
24
|
+
except (PermissionError, OSError, FileNotFoundError) as e:
|
|
25
|
+
raise Abort(
|
|
26
|
+
f"""
|
|
27
|
+
Cache directory {USER_TOKEN_CACHE_DIR} doesn't exist, is not writable, or could not be created.
|
|
28
|
+
Error: {e}
|
|
29
|
+
|
|
30
|
+
Please check your home directory permissions and try again.
|
|
31
|
+
""",
|
|
32
|
+
subject="Non-writable cache dir",
|
|
33
|
+
log_message="Non-writable cache dir",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def with_cache(func):
|
|
38
|
+
"""Initialize cache before function execution."""
|
|
39
|
+
if inspect.iscoroutinefunction(func):
|
|
40
|
+
|
|
41
|
+
@wraps(func)
|
|
42
|
+
async def async_wrapper(*args, **kwargs):
|
|
43
|
+
try:
|
|
44
|
+
USER_TOKEN_CACHE_DIR.mkdir(exist_ok=True, parents=True)
|
|
45
|
+
token_dir = USER_TOKEN_CACHE_DIR / "token"
|
|
46
|
+
token_dir.mkdir(exist_ok=True)
|
|
47
|
+
info_file = USER_TOKEN_CACHE_DIR / "info.txt"
|
|
48
|
+
info_file.write_text("This directory is used by Vantage CLI for its cache.")
|
|
49
|
+
except (PermissionError, OSError, FileNotFoundError) as e:
|
|
50
|
+
raise Abort(
|
|
51
|
+
f"""
|
|
52
|
+
Cache directory {USER_TOKEN_CACHE_DIR} doesn't exist, is not writable, or could not be created.
|
|
53
|
+
Error: {e}
|
|
54
|
+
|
|
55
|
+
Please check your home directory permissions and try again.
|
|
56
|
+
""",
|
|
57
|
+
subject="Non-writable cache dir",
|
|
58
|
+
log_message=f"Non-writable cache dir: {e}",
|
|
59
|
+
)
|
|
60
|
+
return await func(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
return async_wrapper
|
|
63
|
+
else:
|
|
64
|
+
|
|
65
|
+
@wraps(func)
|
|
66
|
+
def wrapper(*args, **kwargs):
|
|
67
|
+
try:
|
|
68
|
+
USER_TOKEN_CACHE_DIR.mkdir(exist_ok=True, parents=True)
|
|
69
|
+
token_dir = USER_TOKEN_CACHE_DIR / "token"
|
|
70
|
+
token_dir.mkdir(exist_ok=True)
|
|
71
|
+
info_file = USER_TOKEN_CACHE_DIR / "info.txt"
|
|
72
|
+
info_file.write_text("This directory is used by Vantage CLI for its cache.")
|
|
73
|
+
except (PermissionError, OSError, FileNotFoundError) as e:
|
|
74
|
+
raise Abort(
|
|
75
|
+
f"""
|
|
76
|
+
Cache directory {USER_TOKEN_CACHE_DIR} doesn't exist, is not writable, or could not be created.
|
|
77
|
+
Error: {e}
|
|
78
|
+
|
|
79
|
+
Please check your home directory permissions and try again.
|
|
80
|
+
""",
|
|
81
|
+
subject="Non-writable cache dir",
|
|
82
|
+
log_message="Non-writable cache dir",
|
|
83
|
+
)
|
|
84
|
+
return func(*args, **kwargs)
|
|
85
|
+
|
|
86
|
+
return wrapper
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_token_paths(profile: str) -> tuple[Path, Path]:
|
|
90
|
+
token_dir = USER_TOKEN_CACHE_DIR / profile
|
|
91
|
+
access_token_path: Path = token_dir / "access.token"
|
|
92
|
+
refresh_token_path: Path = token_dir / "refresh.token"
|
|
93
|
+
return (access_token_path, refresh_token_path)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_tokens_from_cache(profile: str) -> TokenSet:
|
|
97
|
+
"""Load access token and refresh token from the cache."""
|
|
98
|
+
(access_token_path, refresh_token_path) = _get_token_paths(profile)
|
|
99
|
+
|
|
100
|
+
Abort.require_condition(
|
|
101
|
+
access_token_path.exists(),
|
|
102
|
+
"Please login with your auth token first using the `vantage login` command",
|
|
103
|
+
raise_kwargs={"subject": "You need to login"},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
logger.debug("Retrieving access token from cache")
|
|
107
|
+
token_set: TokenSet = TokenSet(access_token=access_token_path.read_text())
|
|
108
|
+
|
|
109
|
+
if refresh_token_path.exists():
|
|
110
|
+
logger.debug("Retrieving refresh token from cache")
|
|
111
|
+
token_set.refresh_token = refresh_token_path.read_text()
|
|
112
|
+
|
|
113
|
+
return token_set
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def save_tokens_to_cache(profile: str, token_set: TokenSet):
|
|
117
|
+
"""Save tokens from a token_set to the cache."""
|
|
118
|
+
(access_token_path, refresh_token_path) = _get_token_paths(profile)
|
|
119
|
+
# make sure the parent directory exists
|
|
120
|
+
access_token_path.parent.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
logger.debug(f"Caching access token at {access_token_path}")
|
|
123
|
+
access_token_path.write_text(token_set.access_token)
|
|
124
|
+
access_token_path.chmod(0o600)
|
|
125
|
+
|
|
126
|
+
if token_set.refresh_token is not None:
|
|
127
|
+
logger.debug(f"Caching refresh token at {refresh_token_path}")
|
|
128
|
+
refresh_token_path.write_text(token_set.refresh_token)
|
|
129
|
+
refresh_token_path.chmod(0o600)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def clear_token_cache(profile: str):
|
|
133
|
+
"""Clear the token cache."""
|
|
134
|
+
logger.debug("Clearing cached tokens")
|
|
135
|
+
(access_token_path, refresh_token_path) = _get_token_paths(profile)
|
|
136
|
+
|
|
137
|
+
logger.debug(f"Removing access token at {access_token_path}")
|
|
138
|
+
if access_token_path.exists():
|
|
139
|
+
access_token_path.unlink()
|
|
140
|
+
|
|
141
|
+
logger.debug(f"Removing refresh token at {refresh_token_path}")
|
|
142
|
+
if refresh_token_path.exists():
|
|
143
|
+
refresh_token_path.unlink()
|
vantage_cli/client.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Simple async HTTP client for OAuth token operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any, Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pydantic
|
|
10
|
+
import typer
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from vantage_cli.exceptions import Abort
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def attach_client(func: Callable) -> Callable:
|
|
17
|
+
"""Create async HTTP client for OAuth operations."""
|
|
18
|
+
|
|
19
|
+
@wraps(func)
|
|
20
|
+
async def wrapper(ctx: typer.Context, *args: Any, **kwargs: Any) -> Any:
|
|
21
|
+
if ctx.obj.settings is None:
|
|
22
|
+
raise Abort(
|
|
23
|
+
"Cannot attach client before settings!",
|
|
24
|
+
subject="Configuration Error",
|
|
25
|
+
log_message="Settings not configured before client attachment",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger.debug("Creating async HTTP client for OAuth operations")
|
|
29
|
+
async with httpx.AsyncClient(
|
|
30
|
+
base_url=ctx.obj.settings.oidc_base_url,
|
|
31
|
+
headers={"content-type": "application/x-www-form-urlencoded"},
|
|
32
|
+
) as client:
|
|
33
|
+
ctx.obj.client = client
|
|
34
|
+
return await func(ctx, *args, **kwargs)
|
|
35
|
+
|
|
36
|
+
return wrapper
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
ResponseModel = TypeVar("ResponseModel", bound=pydantic.BaseModel)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def make_oauth_request(
|
|
43
|
+
client: httpx.AsyncClient,
|
|
44
|
+
url_path: str,
|
|
45
|
+
data: dict[str, Any],
|
|
46
|
+
response_model_cls: type[ResponseModel],
|
|
47
|
+
abort_message: str = "OAuth request failed",
|
|
48
|
+
abort_subject: str = "AUTHENTICATION ERROR",
|
|
49
|
+
) -> ResponseModel:
|
|
50
|
+
"""Make an async OAuth token request.
|
|
51
|
+
|
|
52
|
+
Simplified version focused only on OAuth POST requests with form data.
|
|
53
|
+
"""
|
|
54
|
+
logger.debug(f"Making OAuth request to {url_path}")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = await client.post(url_path, data=data)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
|
|
60
|
+
response_data = response.json()
|
|
61
|
+
logger.debug(f"OAuth response received: {response_data}")
|
|
62
|
+
|
|
63
|
+
return response_model_cls(**response_data)
|
|
64
|
+
|
|
65
|
+
except httpx.HTTPStatusError as e:
|
|
66
|
+
logger.error(
|
|
67
|
+
f"OAuth request failed with status {e.response.status_code}: {e.response.text}"
|
|
68
|
+
)
|
|
69
|
+
if abort_message == "IGNORE":
|
|
70
|
+
raise e
|
|
71
|
+
raise Abort(
|
|
72
|
+
f"{abort_message}: Received error response",
|
|
73
|
+
subject=abort_subject,
|
|
74
|
+
log_message=f"OAuth request failed: {e.response.status_code} - {e.response.text}",
|
|
75
|
+
)
|
|
76
|
+
except (httpx.RequestError, httpx.ConnectError, httpx.TimeoutException) as e:
|
|
77
|
+
logger.error(f"OAuth request failed: {e}")
|
|
78
|
+
if abort_message == "IGNORE":
|
|
79
|
+
raise e
|
|
80
|
+
raise Abort(
|
|
81
|
+
f"{abort_message}: Request failed",
|
|
82
|
+
subject=abort_subject,
|
|
83
|
+
log_message=f"OAuth request error: {e}",
|
|
84
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# © 2025 Vantage Compute, Inc. All rights reserved.
|
|
2
|
+
# Confidential and proprietary. Unauthorized use prohibited.
|
|
3
|
+
"""Base command utilities for consistent global option handling."""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from typing_extensions import Annotated
|
|
9
|
+
|
|
10
|
+
# Reusable type annotations for global options
|
|
11
|
+
JsonOption = Annotated[bool, typer.Option("--json", "-j", help="Output in JSON format")]
|
|
12
|
+
VerboseOption = Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose output")]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GlobalOptions:
|
|
16
|
+
"""Helper class to manage global options consistently across commands."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, ctx: typer.Context, local_json: bool = False, local_verbose: bool = False):
|
|
19
|
+
self.ctx = ctx
|
|
20
|
+
self.local_json = local_json
|
|
21
|
+
self.local_verbose = local_verbose
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def json_output(self) -> bool:
|
|
25
|
+
"""Get effective JSON output setting."""
|
|
26
|
+
global_json = getattr(self.ctx.obj, "json_output", False) if self.ctx.obj else False
|
|
27
|
+
return self.local_json or global_json
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def verbose(self) -> bool:
|
|
31
|
+
"""Get effective verbose setting."""
|
|
32
|
+
global_verbose = getattr(self.ctx.obj, "verbose", False) if self.ctx.obj else False
|
|
33
|
+
return self.local_verbose or global_verbose
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def profile(self) -> Optional[str]:
|
|
37
|
+
"""Get the active profile."""
|
|
38
|
+
return getattr(self.ctx.obj, "profile", None) if self.ctx.obj else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_global_options(
|
|
42
|
+
ctx: typer.Context, json_output: bool = False, verbose: bool = False
|
|
43
|
+
) -> GlobalOptions:
|
|
44
|
+
"""Create GlobalOptions instance."""
|
|
45
|
+
return GlobalOptions(ctx, json_output, verbose)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Utility functions for individual options
|
|
49
|
+
def get_effective_json_output(ctx: typer.Context, local_json: bool = False) -> bool:
|
|
50
|
+
"""Get the effective JSON output setting with precedence: local > global > default."""
|
|
51
|
+
global_json = getattr(ctx.obj, "json_output", False) if ctx.obj else False
|
|
52
|
+
return local_json or global_json
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_effective_verbose(ctx: typer.Context, local_verbose: bool = False) -> bool:
|
|
56
|
+
"""Get the effective verbose setting with precedence: local > global > default."""
|
|
57
|
+
global_verbose = getattr(ctx.obj, "verbose", False) if ctx.obj else False
|
|
58
|
+
return local_verbose or global_verbose
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_active_profile(ctx: typer.Context) -> Optional[str]:
|
|
62
|
+
"""Get the active profile from context."""
|
|
63
|
+
return getattr(ctx.obj, "profile", None) if ctx.obj else None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Vantage CLI command modules."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# © 2025 Vantage Compute, Inc. All rights reserved.
|
|
2
|
+
# Confidential and proprietary. Unauthorized use prohibited.
|
|
3
|
+
"""Clouds management commands package."""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .add import add_command
|
|
8
|
+
from .delete import delete_command
|
|
9
|
+
from .update import update_command
|
|
10
|
+
|
|
11
|
+
clouds_app = typer.Typer(
|
|
12
|
+
name="clouds",
|
|
13
|
+
help="Manage cloud provider configurations and integrations for your Vantage infrastructure.",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Register all commands
|
|
18
|
+
clouds_app.command("add")(add_command)
|
|
19
|
+
clouds_app.command("delete")(delete_command)
|
|
20
|
+
clouds_app.command("update")(update_command)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# © 2025 Vantage Compute, Inc. All rights reserved.
|
|
2
|
+
# Confidential and proprietary. Unauthorized use prohibited.
|
|
3
|
+
"""Add cloud command."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_command(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
cloud_name: Annotated[str, typer.Argument(help="Name of the cloud to add")],
|
|
18
|
+
provider: Annotated[
|
|
19
|
+
str, typer.Option("--provider", "-p", help="Cloud provider (aws, gcp, azure, etc.)")
|
|
20
|
+
],
|
|
21
|
+
region: Annotated[
|
|
22
|
+
Optional[str], typer.Option("--region", "-r", help="Default region for the cloud")
|
|
23
|
+
] = None,
|
|
24
|
+
config_file: Annotated[
|
|
25
|
+
Optional[Path],
|
|
26
|
+
typer.Option(
|
|
27
|
+
"--config-file",
|
|
28
|
+
help="Path to cloud configuration file",
|
|
29
|
+
exists=True,
|
|
30
|
+
file_okay=True,
|
|
31
|
+
dir_okay=False,
|
|
32
|
+
readable=True,
|
|
33
|
+
),
|
|
34
|
+
] = None,
|
|
35
|
+
credentials_file: Annotated[
|
|
36
|
+
Optional[Path],
|
|
37
|
+
typer.Option(
|
|
38
|
+
"--credentials-file",
|
|
39
|
+
help="Path to credentials file",
|
|
40
|
+
exists=True,
|
|
41
|
+
file_okay=True,
|
|
42
|
+
dir_okay=False,
|
|
43
|
+
readable=True,
|
|
44
|
+
),
|
|
45
|
+
] = None,
|
|
46
|
+
):
|
|
47
|
+
"""Add a new cloud configuration."""
|
|
48
|
+
verbose = ctx.obj.get("verbose", False)
|
|
49
|
+
settings = ctx.obj.get("settings")
|
|
50
|
+
|
|
51
|
+
logger.info(f"Adding cloud configuration: {cloud_name}")
|
|
52
|
+
|
|
53
|
+
if verbose:
|
|
54
|
+
logger.debug(f"Provider: {provider}")
|
|
55
|
+
logger.debug(f"Region: {region}")
|
|
56
|
+
logger.debug(f"Config file: {config_file}")
|
|
57
|
+
logger.debug(f"Credentials file: {credentials_file}")
|
|
58
|
+
logger.debug(f"Settings: {settings}")
|
|
59
|
+
|
|
60
|
+
# TODO: Validate that cloud doesn't already exist
|
|
61
|
+
|
|
62
|
+
if config_file:
|
|
63
|
+
logger.info(f"Using config file: {config_file}")
|
|
64
|
+
typer.echo(f"Adding cloud '{cloud_name}' with config file: {config_file}")
|
|
65
|
+
else:
|
|
66
|
+
typer.echo(f"Adding cloud '{cloud_name}' with provider: {provider}")
|
|
67
|
+
if region:
|
|
68
|
+
typer.echo(f"Default region: {region}")
|
|
69
|
+
|
|
70
|
+
if credentials_file:
|
|
71
|
+
logger.info(f"Using credentials file: {credentials_file}")
|
|
72
|
+
typer.echo(f"Credentials file: {credentials_file}")
|
|
73
|
+
|
|
74
|
+
# TODO: Implement actual cloud configuration addition logic
|
|
75
|
+
# This would typically:
|
|
76
|
+
# 1. Validate the cloud configuration
|
|
77
|
+
# 2. Store the cloud config in the appropriate location
|
|
78
|
+
# 3. Update the supported clouds list
|
|
79
|
+
|
|
80
|
+
logger.info(f"Cloud {cloud_name} added successfully")
|
|
81
|
+
typer.echo(f"✅ Cloud '{cloud_name}' added successfully")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# © 2025 Vantage Compute, Inc. All rights reserved.
|
|
2
|
+
# Confidential and proprietary. Unauthorized use prohibited.
|
|
3
|
+
"""Delete cloud command."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from typing_extensions import Annotated
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def delete_command(
|
|
14
|
+
ctx: typer.Context,
|
|
15
|
+
cloud_name: Annotated[str, typer.Argument(help="Name of the cloud to delete")],
|
|
16
|
+
force: Annotated[
|
|
17
|
+
bool, typer.Option("--force", help="Force deletion without confirmation")
|
|
18
|
+
] = False,
|
|
19
|
+
remove_credentials: Annotated[
|
|
20
|
+
bool, typer.Option("--remove-credentials", help="Also remove stored credentials")
|
|
21
|
+
] = False,
|
|
22
|
+
):
|
|
23
|
+
"""Delete a cloud configuration."""
|
|
24
|
+
verbose = ctx.obj.get("verbose", False)
|
|
25
|
+
settings = ctx.obj.get("settings")
|
|
26
|
+
|
|
27
|
+
logger.info(f"Deleting cloud configuration: {cloud_name}")
|
|
28
|
+
|
|
29
|
+
if verbose:
|
|
30
|
+
logger.debug(f"Force: {force}")
|
|
31
|
+
logger.debug(f"Remove credentials: {remove_credentials}")
|
|
32
|
+
logger.debug(f"Settings: {settings}")
|
|
33
|
+
|
|
34
|
+
# TODO: Check if cloud exists
|
|
35
|
+
# TODO: Check if cloud is in use by any clusters
|
|
36
|
+
|
|
37
|
+
if not force:
|
|
38
|
+
warning_msg = f"Are you sure you want to delete cloud configuration '{cloud_name}'?"
|
|
39
|
+
if remove_credentials:
|
|
40
|
+
warning_msg += " This will also remove stored credentials."
|
|
41
|
+
|
|
42
|
+
confirm = typer.confirm(warning_msg)
|
|
43
|
+
if not confirm:
|
|
44
|
+
typer.echo("Operation cancelled.")
|
|
45
|
+
raise typer.Abort()
|
|
46
|
+
|
|
47
|
+
typer.echo(f"Deleting cloud configuration: {cloud_name}")
|
|
48
|
+
|
|
49
|
+
if remove_credentials:
|
|
50
|
+
typer.echo("Removing stored credentials...")
|
|
51
|
+
logger.info("Removing stored credentials")
|
|
52
|
+
|
|
53
|
+
# TODO: Implement actual cloud configuration deletion logic
|
|
54
|
+
# This would typically:
|
|
55
|
+
# 1. Remove cloud config from storage
|
|
56
|
+
# 2. Update the supported clouds list
|
|
57
|
+
# 3. Optionally remove credentials
|
|
58
|
+
# 4. Clean up any related files
|
|
59
|
+
|
|
60
|
+
logger.info(f"Cloud {cloud_name} deleted successfully")
|
|
61
|
+
typer.echo(f"✅ Cloud '{cloud_name}' deleted successfully")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# © 2025 Vantage Compute, Inc. All rights reserved.
|
|
2
|
+
# Confidential and proprietary. Unauthorized use prohibited.
|
|
3
|
+
"""Render helpers for cloud 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_clouds_table(
|
|
16
|
+
clouds: List[Dict[str, Any]],
|
|
17
|
+
title: str = "Cloud Accounts",
|
|
18
|
+
total_count: Optional[int] = None,
|
|
19
|
+
json_output: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Render a list of cloud accounts in a Rich table format.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
clouds: List of cloud account dictionaries
|
|
25
|
+
title: Title for the table
|
|
26
|
+
total_count: Total number of cloud accounts available
|
|
27
|
+
json_output: If True, output as JSON instead of a table
|
|
28
|
+
"""
|
|
29
|
+
if json_output:
|
|
30
|
+
output = {"clouds": clouds, "total": total_count or len(clouds)}
|
|
31
|
+
print_json(data=output)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if not clouds:
|
|
35
|
+
console = Console()
|
|
36
|
+
console.print()
|
|
37
|
+
console.print(Panel("No cloud accounts found.", title="[yellow]No Results"))
|
|
38
|
+
console.print()
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Define cloud-specific styling
|
|
42
|
+
style_mapper = StyleMapper(
|
|
43
|
+
name="bold cyan", provider="blue", status="green", accountId="white", region="yellow"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Create the table
|
|
47
|
+
table = Table(
|
|
48
|
+
title=title,
|
|
49
|
+
caption=f"Items: {len(clouds)}{f' of {total_count}' if total_count else ''}",
|
|
50
|
+
show_header=True,
|
|
51
|
+
header_style="bold white",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Add columns based on cloud data structure
|
|
55
|
+
column_mapping = {
|
|
56
|
+
"name": "Name",
|
|
57
|
+
"provider": "Provider",
|
|
58
|
+
"status": "Status",
|
|
59
|
+
"accountId": "Account ID",
|
|
60
|
+
"region": "Region",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Add columns that exist in the first cloud entry
|
|
64
|
+
if clouds:
|
|
65
|
+
first_cloud = clouds[0]
|
|
66
|
+
for key, display_name in column_mapping.items():
|
|
67
|
+
if key in first_cloud:
|
|
68
|
+
table.add_column(display_name, **style_mapper.map_style(key))
|
|
69
|
+
|
|
70
|
+
# Add rows
|
|
71
|
+
for cloud in clouds:
|
|
72
|
+
row_values = []
|
|
73
|
+
for key in column_mapping.keys():
|
|
74
|
+
if key in cloud:
|
|
75
|
+
value = cloud.get(key, "")
|
|
76
|
+
if value is None:
|
|
77
|
+
value = ""
|
|
78
|
+
row_values.append(str(value))
|
|
79
|
+
if row_values: # Only add row if we have values
|
|
80
|
+
table.add_row(*row_values)
|
|
81
|
+
|
|
82
|
+
# Print the table
|
|
83
|
+
console = Console()
|
|
84
|
+
console.print()
|
|
85
|
+
console.print(table)
|
|
86
|
+
console.print()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_cloud_operation_result(
|
|
90
|
+
operation: str,
|
|
91
|
+
cloud_name: str,
|
|
92
|
+
success: bool = True,
|
|
93
|
+
details: Optional[Dict[str, Any]] = None,
|
|
94
|
+
json_output: bool = False,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Render the result of a cloud operation (add, update, delete).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
operation: The operation performed (add, update, delete)
|
|
100
|
+
cloud_name: Name of the cloud account
|
|
101
|
+
success: Whether the operation was successful
|
|
102
|
+
details: Additional details about the operation
|
|
103
|
+
json_output: If True, output as JSON instead of a formatted view
|
|
104
|
+
"""
|
|
105
|
+
if json_output:
|
|
106
|
+
result = {
|
|
107
|
+
"operation": operation,
|
|
108
|
+
"cloud_name": cloud_name,
|
|
109
|
+
"success": success,
|
|
110
|
+
"details": details or {},
|
|
111
|
+
}
|
|
112
|
+
print_json(data=result)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
console = Console()
|
|
116
|
+
console.print()
|
|
117
|
+
|
|
118
|
+
status_icon = "✅" if success else "❌"
|
|
119
|
+
status_color = "green" if success else "red"
|
|
120
|
+
action_text = f"{operation.title()}d" if success else f"{operation.title()} Failed"
|
|
121
|
+
|
|
122
|
+
console.print(
|
|
123
|
+
Panel(
|
|
124
|
+
f"{status_icon} Cloud account '[bold cyan]{cloud_name}[/bold cyan]' {operation} {'successful' if success else 'failed'}!",
|
|
125
|
+
title=f"[{status_color}]{action_text}[/{status_color}]",
|
|
126
|
+
border_style=status_color,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if details and not json_output:
|
|
131
|
+
# Show additional details if provided
|
|
132
|
+
details_table = Table(
|
|
133
|
+
title="Operation Details", show_header=False, box=None, padding=(0, 1)
|
|
134
|
+
)
|
|
135
|
+
details_table.add_column("Field", style="bold blue", width=20)
|
|
136
|
+
details_table.add_column("Value", style="white")
|
|
137
|
+
|
|
138
|
+
for key, value in details.items():
|
|
139
|
+
if value is not None:
|
|
140
|
+
display_key = key.replace("_", " ").title()
|
|
141
|
+
details_table.add_row(display_key, str(value))
|
|
142
|
+
|
|
143
|
+
console.print()
|
|
144
|
+
console.print(details_table)
|
|
145
|
+
|
|
146
|
+
console.print()
|