tetra-rp 0.6.0__py3-none-any.whl → 0.24.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.
- tetra_rp/__init__.py +109 -19
- tetra_rp/cli/commands/__init__.py +1 -0
- tetra_rp/cli/commands/apps.py +143 -0
- tetra_rp/cli/commands/build.py +1082 -0
- tetra_rp/cli/commands/build_utils/__init__.py +1 -0
- tetra_rp/cli/commands/build_utils/handler_generator.py +176 -0
- tetra_rp/cli/commands/build_utils/lb_handler_generator.py +309 -0
- tetra_rp/cli/commands/build_utils/manifest.py +430 -0
- tetra_rp/cli/commands/build_utils/mothership_handler_generator.py +75 -0
- tetra_rp/cli/commands/build_utils/scanner.py +596 -0
- tetra_rp/cli/commands/deploy.py +580 -0
- tetra_rp/cli/commands/init.py +123 -0
- tetra_rp/cli/commands/resource.py +108 -0
- tetra_rp/cli/commands/run.py +296 -0
- tetra_rp/cli/commands/test_mothership.py +458 -0
- tetra_rp/cli/commands/undeploy.py +533 -0
- tetra_rp/cli/main.py +97 -0
- tetra_rp/cli/utils/__init__.py +1 -0
- tetra_rp/cli/utils/app.py +15 -0
- tetra_rp/cli/utils/conda.py +127 -0
- tetra_rp/cli/utils/deployment.py +530 -0
- tetra_rp/cli/utils/ignore.py +143 -0
- tetra_rp/cli/utils/skeleton.py +184 -0
- tetra_rp/cli/utils/skeleton_template/.env.example +4 -0
- tetra_rp/cli/utils/skeleton_template/.flashignore +40 -0
- tetra_rp/cli/utils/skeleton_template/.gitignore +44 -0
- tetra_rp/cli/utils/skeleton_template/README.md +263 -0
- tetra_rp/cli/utils/skeleton_template/main.py +44 -0
- tetra_rp/cli/utils/skeleton_template/mothership.py +55 -0
- tetra_rp/cli/utils/skeleton_template/pyproject.toml +58 -0
- tetra_rp/cli/utils/skeleton_template/requirements.txt +1 -0
- tetra_rp/cli/utils/skeleton_template/workers/__init__.py +0 -0
- tetra_rp/cli/utils/skeleton_template/workers/cpu/__init__.py +19 -0
- tetra_rp/cli/utils/skeleton_template/workers/cpu/endpoint.py +36 -0
- tetra_rp/cli/utils/skeleton_template/workers/gpu/__init__.py +19 -0
- tetra_rp/cli/utils/skeleton_template/workers/gpu/endpoint.py +61 -0
- tetra_rp/client.py +136 -33
- tetra_rp/config.py +29 -0
- tetra_rp/core/api/runpod.py +591 -39
- tetra_rp/core/deployment.py +232 -0
- tetra_rp/core/discovery.py +425 -0
- tetra_rp/core/exceptions.py +50 -0
- tetra_rp/core/resources/__init__.py +27 -9
- tetra_rp/core/resources/app.py +738 -0
- tetra_rp/core/resources/base.py +139 -4
- tetra_rp/core/resources/constants.py +21 -0
- tetra_rp/core/resources/cpu.py +115 -13
- tetra_rp/core/resources/gpu.py +182 -16
- tetra_rp/core/resources/live_serverless.py +153 -16
- tetra_rp/core/resources/load_balancer_sls_resource.py +440 -0
- tetra_rp/core/resources/network_volume.py +126 -31
- tetra_rp/core/resources/resource_manager.py +436 -35
- tetra_rp/core/resources/serverless.py +537 -120
- tetra_rp/core/resources/serverless_cpu.py +201 -0
- tetra_rp/core/resources/template.py +1 -59
- tetra_rp/core/utils/constants.py +10 -0
- tetra_rp/core/utils/file_lock.py +260 -0
- tetra_rp/core/utils/http.py +67 -0
- tetra_rp/core/utils/lru_cache.py +75 -0
- tetra_rp/core/utils/singleton.py +36 -1
- tetra_rp/core/validation.py +44 -0
- tetra_rp/execute_class.py +301 -0
- tetra_rp/protos/remote_execution.py +98 -9
- tetra_rp/runtime/__init__.py +1 -0
- tetra_rp/runtime/circuit_breaker.py +274 -0
- tetra_rp/runtime/config.py +12 -0
- tetra_rp/runtime/exceptions.py +49 -0
- tetra_rp/runtime/generic_handler.py +206 -0
- tetra_rp/runtime/lb_handler.py +189 -0
- tetra_rp/runtime/load_balancer.py +160 -0
- tetra_rp/runtime/manifest_fetcher.py +192 -0
- tetra_rp/runtime/metrics.py +325 -0
- tetra_rp/runtime/models.py +73 -0
- tetra_rp/runtime/mothership_provisioner.py +512 -0
- tetra_rp/runtime/production_wrapper.py +266 -0
- tetra_rp/runtime/reliability_config.py +149 -0
- tetra_rp/runtime/retry_manager.py +118 -0
- tetra_rp/runtime/serialization.py +124 -0
- tetra_rp/runtime/service_registry.py +346 -0
- tetra_rp/runtime/state_manager_client.py +248 -0
- tetra_rp/stubs/live_serverless.py +35 -17
- tetra_rp/stubs/load_balancer_sls.py +357 -0
- tetra_rp/stubs/registry.py +145 -19
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/METADATA +398 -60
- tetra_rp-0.24.0.dist-info/RECORD +99 -0
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/WHEEL +1 -1
- tetra_rp-0.24.0.dist-info/entry_points.txt +2 -0
- tetra_rp/core/pool/cluster_manager.py +0 -177
- tetra_rp/core/pool/dataclass.py +0 -18
- tetra_rp/core/pool/ex.py +0 -38
- tetra_rp/core/pool/job.py +0 -22
- tetra_rp/core/pool/worker.py +0 -19
- tetra_rp/core/resources/utils.py +0 -50
- tetra_rp/core/utils/json.py +0 -33
- tetra_rp-0.6.0.dist-info/RECORD +0 -39
- /tetra_rp/{core/pool → cli}/__init__.py +0 -0
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"""Deployment environment management commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import questionary
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
11
|
+
from ..utils.deployment import (
|
|
12
|
+
get_deployment_environments,
|
|
13
|
+
create_deployment_environment,
|
|
14
|
+
remove_deployment_environment,
|
|
15
|
+
deploy_to_environment,
|
|
16
|
+
rollback_deployment,
|
|
17
|
+
get_environment_info,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ..utils.app import discover_flash_project
|
|
21
|
+
|
|
22
|
+
from tetra_rp.core.resources.app import FlashApp
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_resource_manager():
|
|
28
|
+
from tetra_rp.core.resources.resource_manager import ResourceManager
|
|
29
|
+
|
|
30
|
+
return ResourceManager()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _undeploy_environment_resources(env_name: str, env: dict) -> None:
|
|
34
|
+
"""Undeploy resources tied to a flash environment before deletion."""
|
|
35
|
+
endpoints = env.get("endpoints") or []
|
|
36
|
+
network_volumes = env.get("networkVolumes") or []
|
|
37
|
+
|
|
38
|
+
if not endpoints and not network_volumes:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
manager = _get_resource_manager()
|
|
42
|
+
failures = []
|
|
43
|
+
undeployed = 0
|
|
44
|
+
seen_resource_ids = set()
|
|
45
|
+
|
|
46
|
+
with console.status(f"Undeploying resources for '{env_name}'..."):
|
|
47
|
+
for label, items in (
|
|
48
|
+
("Endpoint", endpoints),
|
|
49
|
+
("Network volume", network_volumes),
|
|
50
|
+
):
|
|
51
|
+
for item in items:
|
|
52
|
+
provider_id = item.get("id") if isinstance(item, dict) else None
|
|
53
|
+
name = item.get("name") if isinstance(item, dict) else None
|
|
54
|
+
if not provider_id:
|
|
55
|
+
failures.append(f"{label} missing id in environment '{env_name}'")
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
matches = manager.find_resources_by_provider_id(provider_id)
|
|
59
|
+
if not matches:
|
|
60
|
+
display_name = name if name else provider_id
|
|
61
|
+
failures.append(
|
|
62
|
+
f"{label} '{display_name}' ({provider_id}) not found in local tracking"
|
|
63
|
+
)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
for resource_id, resource in matches:
|
|
67
|
+
if resource_id in seen_resource_ids:
|
|
68
|
+
continue
|
|
69
|
+
seen_resource_ids.add(resource_id)
|
|
70
|
+
resource_name = getattr(resource, "name", name) or provider_id
|
|
71
|
+
result = await manager.undeploy_resource(resource_id, resource_name)
|
|
72
|
+
if result.get("success"):
|
|
73
|
+
undeployed += 1
|
|
74
|
+
else:
|
|
75
|
+
failures.append(
|
|
76
|
+
result.get(
|
|
77
|
+
"message",
|
|
78
|
+
f"Failed to undeploy {label.lower()} '{resource_name}'",
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if failures:
|
|
83
|
+
console.print(
|
|
84
|
+
"❌ Failed to undeploy all resources; environment deletion aborted."
|
|
85
|
+
)
|
|
86
|
+
for message in failures:
|
|
87
|
+
console.print(f" • {message}")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
if undeployed:
|
|
91
|
+
console.print(f"✅ Undeployed {undeployed} resource(s) for '{env_name}'")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def list_command(
|
|
95
|
+
app_name: str | None = typer.Option(
|
|
96
|
+
None, "--app-name", "-a", help="flash app name to inspect"
|
|
97
|
+
),
|
|
98
|
+
):
|
|
99
|
+
"""Show available deployment environments."""
|
|
100
|
+
if not app_name:
|
|
101
|
+
_, app_name = discover_flash_project()
|
|
102
|
+
asyncio.run(list_flash_environments(app_name))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def new_flash_deployment_environment(app_name: str, env_name: str):
|
|
106
|
+
"""
|
|
107
|
+
Create a new flash deployment environment. Creates a flash app if it doesn't already exist.
|
|
108
|
+
"""
|
|
109
|
+
app, env = await FlashApp.create_environment_and_app(app_name, env_name)
|
|
110
|
+
|
|
111
|
+
panel_content = (
|
|
112
|
+
f"Environment '[bold]{env_name}[/bold]' created successfully\n\n"
|
|
113
|
+
f"App: {app_name}\n"
|
|
114
|
+
f"Environment ID: {env.get('id')}\n"
|
|
115
|
+
f"Status: {env.get('state', 'PENDING')}"
|
|
116
|
+
)
|
|
117
|
+
console.print(Panel(panel_content, title="✅ Environment Created", expand=False))
|
|
118
|
+
|
|
119
|
+
table = Table(show_header=True, header_style="bold")
|
|
120
|
+
table.add_column("Name", style="bold")
|
|
121
|
+
table.add_column("ID", overflow="fold")
|
|
122
|
+
table.add_column("Status", overflow="fold")
|
|
123
|
+
table.add_column("Created At", overflow="fold")
|
|
124
|
+
|
|
125
|
+
table.add_row(
|
|
126
|
+
env.get("name"),
|
|
127
|
+
env.get("id"),
|
|
128
|
+
env.get("state", "PENDING"),
|
|
129
|
+
env.get("createdAt", "Just now"),
|
|
130
|
+
)
|
|
131
|
+
console.print(table)
|
|
132
|
+
|
|
133
|
+
console.print(f"\nNext: [bold]flash deploy send {env_name}[/bold]")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def info_flash_environment(app_name: str, env_name: str):
|
|
137
|
+
"""
|
|
138
|
+
Get detailed information about a flash deployment environment.
|
|
139
|
+
"""
|
|
140
|
+
app = await FlashApp.from_name(app_name)
|
|
141
|
+
env = await app.get_environment_by_name(env_name)
|
|
142
|
+
|
|
143
|
+
main_info = f"Environment: {env.get('name')}\n"
|
|
144
|
+
main_info += f"ID: {env.get('id')}\n"
|
|
145
|
+
main_info += f"State: {env.get('state', 'UNKNOWN')}\n"
|
|
146
|
+
main_info += f"Active Build: {env.get('activeBuildId', 'None')}\n"
|
|
147
|
+
|
|
148
|
+
if env.get("createdAt"):
|
|
149
|
+
main_info += f"Created: {env.get('createdAt')}\n"
|
|
150
|
+
|
|
151
|
+
console.print(Panel(main_info, title=f"📊 Environment: {env_name}", expand=False))
|
|
152
|
+
|
|
153
|
+
endpoints = env.get("endpoints") or []
|
|
154
|
+
if endpoints:
|
|
155
|
+
endpoint_table = Table(title="Associated Endpoints")
|
|
156
|
+
endpoint_table.add_column("Name", style="cyan")
|
|
157
|
+
endpoint_table.add_column("ID", overflow="fold")
|
|
158
|
+
|
|
159
|
+
for endpoint in endpoints:
|
|
160
|
+
endpoint_table.add_row(
|
|
161
|
+
endpoint.get("name", "—"),
|
|
162
|
+
endpoint.get("id", "—"),
|
|
163
|
+
)
|
|
164
|
+
console.print(endpoint_table)
|
|
165
|
+
|
|
166
|
+
network_volumes = env.get("networkVolumes") or []
|
|
167
|
+
if network_volumes:
|
|
168
|
+
nv_table = Table(title="Associated Network Volumes")
|
|
169
|
+
nv_table.add_column("Name", style="cyan")
|
|
170
|
+
nv_table.add_column("ID", overflow="fold")
|
|
171
|
+
|
|
172
|
+
for nv in network_volumes:
|
|
173
|
+
nv_table.add_row(
|
|
174
|
+
nv.get("name", "—"),
|
|
175
|
+
nv.get("id", "—"),
|
|
176
|
+
)
|
|
177
|
+
console.print(nv_table)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def list_flash_environments(app_name: str):
|
|
181
|
+
app = await FlashApp.from_name(app_name)
|
|
182
|
+
envs = await app.list_environments()
|
|
183
|
+
|
|
184
|
+
if not envs:
|
|
185
|
+
console.print(f"No environments found for '{app_name}'.")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
table = Table(show_header=True, header_style="bold")
|
|
189
|
+
table.add_column("Name", style="bold")
|
|
190
|
+
table.add_column("ID", overflow="fold")
|
|
191
|
+
table.add_column("Active Build", overflow="fold")
|
|
192
|
+
table.add_column("Created At", overflow="fold")
|
|
193
|
+
|
|
194
|
+
for env in envs:
|
|
195
|
+
table.add_row(
|
|
196
|
+
env.get("name"),
|
|
197
|
+
env.get("id"),
|
|
198
|
+
env.get("activeBuildId", "-"),
|
|
199
|
+
env.get("createdAt"),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
console.print(table)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def new_command(
|
|
206
|
+
app_name: str | None = typer.Option(
|
|
207
|
+
None, "--app-name", "-a", help="Flash app name to create a new environment in"
|
|
208
|
+
),
|
|
209
|
+
name: str = typer.Argument(
|
|
210
|
+
..., help="Name of the deployment environment to create"
|
|
211
|
+
),
|
|
212
|
+
):
|
|
213
|
+
"""Create a new deployment environment."""
|
|
214
|
+
if not app_name:
|
|
215
|
+
_, app_name = discover_flash_project()
|
|
216
|
+
assert app_name is not None
|
|
217
|
+
asyncio.run(new_flash_deployment_environment(app_name, name))
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
environments = get_deployment_environments()
|
|
221
|
+
|
|
222
|
+
if name in environments:
|
|
223
|
+
console.print(f"Environment '{name}' already exists")
|
|
224
|
+
raise typer.Exit(1)
|
|
225
|
+
|
|
226
|
+
# Interactive configuration
|
|
227
|
+
config = {}
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
config["region"] = questionary.select(
|
|
231
|
+
"Select region:",
|
|
232
|
+
choices=["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"],
|
|
233
|
+
).ask()
|
|
234
|
+
|
|
235
|
+
config["instance_type"] = questionary.select(
|
|
236
|
+
"Instance type:", choices=["A40", "A100", "H100", "RTX4090"]
|
|
237
|
+
).ask()
|
|
238
|
+
|
|
239
|
+
config["auto_scale"] = questionary.confirm("Enable auto-scaling?").ask()
|
|
240
|
+
|
|
241
|
+
if not all([config["region"], config["instance_type"]]):
|
|
242
|
+
console.print("Configuration cancelled")
|
|
243
|
+
raise typer.Exit(1)
|
|
244
|
+
|
|
245
|
+
except KeyboardInterrupt:
|
|
246
|
+
console.print("\nEnvironment creation cancelled")
|
|
247
|
+
raise typer.Exit(1)
|
|
248
|
+
|
|
249
|
+
# Create environment
|
|
250
|
+
with console.status(f"Creating environment '{name}'..."):
|
|
251
|
+
create_deployment_environment(name, config)
|
|
252
|
+
|
|
253
|
+
# Success message
|
|
254
|
+
panel_content = f"Environment '[bold]{name}[/bold]' created successfully\n\n"
|
|
255
|
+
panel_content += f"Region: {config['region']}\n"
|
|
256
|
+
panel_content += f"Instance: {config['instance_type']}\n"
|
|
257
|
+
panel_content += f"Auto-scale: {'Enabled' if config['auto_scale'] else 'Disabled'}"
|
|
258
|
+
|
|
259
|
+
console.print(Panel(panel_content, title="🚀 Environment Created", expand=False))
|
|
260
|
+
|
|
261
|
+
console.print(f"\nNext: [bold]flash deploy send {name}[/bold]")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def send_command(
|
|
265
|
+
env_name: str = typer.Argument(..., help="Name of the deployment environment"),
|
|
266
|
+
app_name: str = typer.Option(None, "--app-name", "-a", help="Flash app name"),
|
|
267
|
+
):
|
|
268
|
+
"""Deploy project to deployment environment."""
|
|
269
|
+
|
|
270
|
+
if not app_name:
|
|
271
|
+
_, app_name = discover_flash_project()
|
|
272
|
+
|
|
273
|
+
build_path = Path(".flash/archive.tar.gz")
|
|
274
|
+
if not build_path.exists():
|
|
275
|
+
console.print(
|
|
276
|
+
"no build path found in current directory. Build your project with flash build first"
|
|
277
|
+
)
|
|
278
|
+
raise typer.Exit(1)
|
|
279
|
+
|
|
280
|
+
console.print(f"🚀 Deploying to '[bold]{env_name}[/bold]'...")
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
asyncio.run(deploy_to_environment(app_name, env_name, build_path))
|
|
284
|
+
|
|
285
|
+
panel_content = f"Deployed to '[bold]{env_name}[/bold]' successfully\n\n"
|
|
286
|
+
|
|
287
|
+
console.print(Panel(panel_content, title="Deployment Complete", expand=False))
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
console.print(f"Deployment failed: {e}")
|
|
291
|
+
raise typer.Exit(1)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def info_command(
|
|
295
|
+
env_name: str = typer.Argument(..., help="Name of the deployment environment"),
|
|
296
|
+
app_name: str = typer.Option(None, "--app-name", "-a", help="Flash app name"),
|
|
297
|
+
):
|
|
298
|
+
"""Show detailed information about a deployment environment."""
|
|
299
|
+
if not app_name:
|
|
300
|
+
_, app_name = discover_flash_project()
|
|
301
|
+
asyncio.run(info_flash_environment(app_name, env_name))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def _fetch_environment_info(app_name: str, env_name: str) -> dict:
|
|
305
|
+
"""Fetch environment information for display.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
app_name: Flash application name
|
|
309
|
+
env_name: Environment name to fetch
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Environment dictionary with id, name, activeBuildId, etc.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
Exception: If environment doesn't exist or API call fails
|
|
316
|
+
"""
|
|
317
|
+
app = await FlashApp.from_name(app_name)
|
|
318
|
+
env = await app.get_environment_by_name(env_name)
|
|
319
|
+
return env
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def delete_flash_environment(app_name: str, env_name: str):
|
|
323
|
+
"""Delete a flash deployment environment.
|
|
324
|
+
|
|
325
|
+
Note: User confirmation should be handled by caller before calling this function.
|
|
326
|
+
This function only performs the deletion operation.
|
|
327
|
+
|
|
328
|
+
This design ensures questionary prompts run in sync context, avoiding
|
|
329
|
+
event loop conflicts between asyncio.run() and prompt_toolkit's Application.run().
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
app_name: Flash application name
|
|
333
|
+
env_name: Environment name to delete
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
typer.Exit: If deletion fails
|
|
337
|
+
"""
|
|
338
|
+
app = await FlashApp.from_name(app_name)
|
|
339
|
+
env = await app.get_environment_by_name(env_name)
|
|
340
|
+
|
|
341
|
+
await _undeploy_environment_resources(env_name, env)
|
|
342
|
+
|
|
343
|
+
with console.status(f"Deleting environment '{env_name}'..."):
|
|
344
|
+
success = await app.delete_environment(env_name)
|
|
345
|
+
|
|
346
|
+
if success:
|
|
347
|
+
console.print(f"✅ Environment '{env_name}' deleted successfully")
|
|
348
|
+
else:
|
|
349
|
+
console.print(f"❌ Failed to delete environment '{env_name}'")
|
|
350
|
+
raise typer.Exit(1)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def delete_command(
|
|
354
|
+
env_name: str = typer.Argument(
|
|
355
|
+
..., help="Name of the deployment environment to delete"
|
|
356
|
+
),
|
|
357
|
+
app_name: str = typer.Option(None, "--app-name", "-a", help="Flash app name"),
|
|
358
|
+
):
|
|
359
|
+
"""Delete a deployment environment."""
|
|
360
|
+
if not app_name:
|
|
361
|
+
_, app_name = discover_flash_project()
|
|
362
|
+
|
|
363
|
+
# Fetch environment info in async context for display
|
|
364
|
+
try:
|
|
365
|
+
env = asyncio.run(_fetch_environment_info(app_name, env_name))
|
|
366
|
+
except Exception as e:
|
|
367
|
+
console.print(f"[red]Error:[/red] Failed to fetch environment info: {e}")
|
|
368
|
+
raise typer.Exit(1)
|
|
369
|
+
|
|
370
|
+
# Display deletion preview in sync context
|
|
371
|
+
panel_content = (
|
|
372
|
+
f"Environment '[bold]{env_name}[/bold]' will be deleted\n\n"
|
|
373
|
+
f"Environment ID: {env.get('id')}\n"
|
|
374
|
+
f"App: {app_name}\n"
|
|
375
|
+
f"Active Build: {env.get('activeBuildId', 'None')}"
|
|
376
|
+
)
|
|
377
|
+
console.print(Panel(panel_content, title="⚠️ Delete Confirmation", expand=False))
|
|
378
|
+
|
|
379
|
+
# Get user confirmation in sync context (BEFORE asyncio.run for deletion)
|
|
380
|
+
try:
|
|
381
|
+
confirmed = questionary.confirm(
|
|
382
|
+
f"Are you sure you want to delete environment '{env_name}'? This will delete all resources associated with this environment!"
|
|
383
|
+
).ask()
|
|
384
|
+
|
|
385
|
+
if not confirmed:
|
|
386
|
+
console.print("Deletion cancelled")
|
|
387
|
+
raise typer.Exit(0)
|
|
388
|
+
except KeyboardInterrupt:
|
|
389
|
+
console.print("\nDeletion cancelled")
|
|
390
|
+
raise typer.Exit(0)
|
|
391
|
+
|
|
392
|
+
# Perform async deletion after confirmation
|
|
393
|
+
asyncio.run(delete_flash_environment(app_name, env_name))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def report_command(
|
|
397
|
+
name: str = typer.Argument(..., help="Name of the deployment environment"),
|
|
398
|
+
):
|
|
399
|
+
"""Show detailed environment status and metrics."""
|
|
400
|
+
|
|
401
|
+
environments = get_deployment_environments()
|
|
402
|
+
|
|
403
|
+
if name not in environments:
|
|
404
|
+
console.print(f"Environment '{name}' not found")
|
|
405
|
+
raise typer.Exit(1)
|
|
406
|
+
|
|
407
|
+
env_info = get_environment_info(name)
|
|
408
|
+
|
|
409
|
+
# Environment status
|
|
410
|
+
status = env_info.get("status", "unknown")
|
|
411
|
+
status_display = {
|
|
412
|
+
"active": "🟢 Active",
|
|
413
|
+
"idle": "🟡 Idle",
|
|
414
|
+
"error": "🔴 Error",
|
|
415
|
+
}.get(status, "❓ Unknown")
|
|
416
|
+
|
|
417
|
+
# Main info panel
|
|
418
|
+
main_info = f"Status: {status_display}\n"
|
|
419
|
+
main_info += f"Current Version: {env_info.get('current_version', 'N/A')}\n"
|
|
420
|
+
main_info += f"URL: {env_info.get('url', 'N/A')}\n"
|
|
421
|
+
main_info += f"Last Deployed: {env_info.get('last_deployed', 'Never')}\n"
|
|
422
|
+
main_info += f"Uptime: {env_info.get('uptime', 'N/A')}"
|
|
423
|
+
|
|
424
|
+
console.print(
|
|
425
|
+
Panel(main_info, title=f"📊 Environment Report: {name}", expand=False)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Version history
|
|
429
|
+
versions = env_info.get("version_history", [])
|
|
430
|
+
if versions:
|
|
431
|
+
version_table = Table(title="Version History")
|
|
432
|
+
version_table.add_column("Version", style="cyan")
|
|
433
|
+
version_table.add_column("Status", justify="center")
|
|
434
|
+
version_table.add_column("Deployed", style="yellow")
|
|
435
|
+
version_table.add_column("Description", style="white")
|
|
436
|
+
|
|
437
|
+
for version in versions[:5]: # Show last 5 versions
|
|
438
|
+
version_status = (
|
|
439
|
+
"🟢 Current" if version.get("is_current") else "📦 Previous"
|
|
440
|
+
)
|
|
441
|
+
version_table.add_row(
|
|
442
|
+
version.get("version", "N/A"),
|
|
443
|
+
version_status,
|
|
444
|
+
version.get("deployed_at", "N/A"),
|
|
445
|
+
version.get("description", "No description"),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
console.print(version_table)
|
|
449
|
+
|
|
450
|
+
# Mock metrics
|
|
451
|
+
console.print("\n[bold]Metrics (Last 24h):[/bold]")
|
|
452
|
+
metrics_info = [
|
|
453
|
+
"• Requests: 145,234",
|
|
454
|
+
"• Avg Response Time: 245ms",
|
|
455
|
+
"• Error Rate: 0.02%",
|
|
456
|
+
"• CPU Usage: 45%",
|
|
457
|
+
"• Memory Usage: 62%",
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
for metric in metrics_info:
|
|
461
|
+
console.print(f" {metric}")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def rollback_command(
|
|
465
|
+
name: str = typer.Argument(..., help="Name of the deployment environment"),
|
|
466
|
+
):
|
|
467
|
+
"""Rollback deployment to previous version."""
|
|
468
|
+
|
|
469
|
+
environments = get_deployment_environments()
|
|
470
|
+
|
|
471
|
+
if name not in environments:
|
|
472
|
+
console.print(f"Environment '{name}' not found")
|
|
473
|
+
raise typer.Exit(1)
|
|
474
|
+
|
|
475
|
+
env_info = get_environment_info(name)
|
|
476
|
+
versions = env_info.get("version_history", [])
|
|
477
|
+
|
|
478
|
+
if len(versions) < 2:
|
|
479
|
+
console.print("No previous versions available for rollback")
|
|
480
|
+
raise typer.Exit(1)
|
|
481
|
+
|
|
482
|
+
# Show available versions (excluding current)
|
|
483
|
+
previous_versions = [v for v in versions if not v.get("is_current")]
|
|
484
|
+
|
|
485
|
+
if not previous_versions:
|
|
486
|
+
console.print("No previous versions available for rollback")
|
|
487
|
+
raise typer.Exit(1)
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
version_choices = [
|
|
491
|
+
f"{v['version']} - {v.get('description', 'No description')}"
|
|
492
|
+
for v in previous_versions[:5]
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
selected = questionary.select(
|
|
496
|
+
"Select version to rollback to:", choices=version_choices
|
|
497
|
+
).ask()
|
|
498
|
+
|
|
499
|
+
if not selected:
|
|
500
|
+
console.print("Rollback cancelled")
|
|
501
|
+
raise typer.Exit(1)
|
|
502
|
+
|
|
503
|
+
target_version = selected.split(" - ")[0]
|
|
504
|
+
|
|
505
|
+
# Confirmation
|
|
506
|
+
confirmed = questionary.confirm(
|
|
507
|
+
f"Rollback environment '{name}' to version {target_version}?"
|
|
508
|
+
).ask()
|
|
509
|
+
|
|
510
|
+
if not confirmed:
|
|
511
|
+
console.print("Rollback cancelled")
|
|
512
|
+
raise typer.Exit(1)
|
|
513
|
+
|
|
514
|
+
except KeyboardInterrupt:
|
|
515
|
+
console.print("\nRollback cancelled")
|
|
516
|
+
raise typer.Exit(1)
|
|
517
|
+
|
|
518
|
+
# Perform rollback
|
|
519
|
+
with console.status(f"Rolling back to {target_version}..."):
|
|
520
|
+
rollback_deployment(name, target_version)
|
|
521
|
+
|
|
522
|
+
console.print(f"Rolled back to version {target_version}")
|
|
523
|
+
console.print(f"Environment '{name}' is now running the previous version.")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def remove_command(
|
|
527
|
+
name: str = typer.Argument(
|
|
528
|
+
..., help="Name of the deployment environment to remove"
|
|
529
|
+
),
|
|
530
|
+
):
|
|
531
|
+
"""Remove deployment environment."""
|
|
532
|
+
|
|
533
|
+
environments = get_deployment_environments()
|
|
534
|
+
|
|
535
|
+
if name not in environments:
|
|
536
|
+
console.print(f"Environment '{name}' not found")
|
|
537
|
+
raise typer.Exit(1)
|
|
538
|
+
|
|
539
|
+
env_info = get_environment_info(name)
|
|
540
|
+
|
|
541
|
+
# Show removal preview
|
|
542
|
+
preview_content = f"Environment: {name}\n"
|
|
543
|
+
preview_content += f"Status: {env_info.get('status', 'unknown')}\n"
|
|
544
|
+
preview_content += f"URL: {env_info.get('url', 'N/A')}\n"
|
|
545
|
+
preview_content += f"Current Version: {env_info.get('current_version', 'N/A')}\n\n"
|
|
546
|
+
preview_content += "⚠️ This will permanently remove:\n"
|
|
547
|
+
preview_content += " • All deployment history\n"
|
|
548
|
+
preview_content += " • All associated resources\n"
|
|
549
|
+
preview_content += " • Environment configuration\n"
|
|
550
|
+
preview_content += " • Access URLs\n\n"
|
|
551
|
+
preview_content += "🚨 This action cannot be undone!"
|
|
552
|
+
|
|
553
|
+
console.print(Panel(preview_content, title="⚠️ Removal Preview", expand=False))
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
# Double confirmation for safety
|
|
557
|
+
confirmed = questionary.confirm(
|
|
558
|
+
f"Are you sure you want to remove environment '{name}'?"
|
|
559
|
+
).ask()
|
|
560
|
+
|
|
561
|
+
if not confirmed:
|
|
562
|
+
console.print("Removal cancelled")
|
|
563
|
+
raise typer.Exit(1)
|
|
564
|
+
|
|
565
|
+
# Type confirmation
|
|
566
|
+
typed_name = questionary.text(f"Type '{name}' to confirm removal:").ask()
|
|
567
|
+
|
|
568
|
+
if typed_name != name:
|
|
569
|
+
console.print("Confirmation failed - names do not match")
|
|
570
|
+
raise typer.Exit(1)
|
|
571
|
+
|
|
572
|
+
except KeyboardInterrupt:
|
|
573
|
+
console.print("\nRemoval cancelled")
|
|
574
|
+
raise typer.Exit(1)
|
|
575
|
+
|
|
576
|
+
# Remove environment
|
|
577
|
+
with console.status(f"Removing environment '{name}'..."):
|
|
578
|
+
remove_deployment_environment(name)
|
|
579
|
+
|
|
580
|
+
console.print(f"Environment '{name}' removed successfully")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Project initialization command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ..utils.skeleton import create_project_skeleton, detect_file_conflicts
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def init_command(
|
|
17
|
+
project_name: Optional[str] = typer.Argument(
|
|
18
|
+
None, help="Project name or '.' for current directory"
|
|
19
|
+
),
|
|
20
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
21
|
+
):
|
|
22
|
+
"""Create new Flash project with Flash Server and GPU workers."""
|
|
23
|
+
|
|
24
|
+
# Determine target directory and initialization mode
|
|
25
|
+
if project_name is None or project_name == ".":
|
|
26
|
+
# Initialize in current directory
|
|
27
|
+
project_dir = Path.cwd()
|
|
28
|
+
is_current_dir = True
|
|
29
|
+
# Use current directory name as project name
|
|
30
|
+
actual_project_name = project_dir.name
|
|
31
|
+
else:
|
|
32
|
+
# Create new directory
|
|
33
|
+
project_dir = Path(project_name)
|
|
34
|
+
is_current_dir = False
|
|
35
|
+
actual_project_name = project_name
|
|
36
|
+
|
|
37
|
+
# Create project directory if needed
|
|
38
|
+
if not is_current_dir:
|
|
39
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
# Check for file conflicts in target directory
|
|
42
|
+
conflicts = detect_file_conflicts(project_dir)
|
|
43
|
+
should_overwrite = force # Start with force flag value
|
|
44
|
+
|
|
45
|
+
if conflicts and not force:
|
|
46
|
+
# Show warning and prompt user
|
|
47
|
+
console.print(
|
|
48
|
+
Panel(
|
|
49
|
+
"[yellow]Warning: The following files will be overwritten:[/yellow]\n\n"
|
|
50
|
+
+ "\n".join(f" • {conflict}" for conflict in conflicts),
|
|
51
|
+
title="File Conflicts Detected",
|
|
52
|
+
expand=False,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Prompt user for confirmation
|
|
57
|
+
proceed = typer.confirm("Continue and overwrite these files?", default=False)
|
|
58
|
+
if not proceed:
|
|
59
|
+
console.print("[yellow]Initialization aborted.[/yellow]")
|
|
60
|
+
raise typer.Exit(0)
|
|
61
|
+
|
|
62
|
+
# User confirmed, so we should overwrite
|
|
63
|
+
should_overwrite = True
|
|
64
|
+
|
|
65
|
+
# Create project skeleton
|
|
66
|
+
status_msg = (
|
|
67
|
+
"Initializing Flash project in current directory..."
|
|
68
|
+
if is_current_dir
|
|
69
|
+
else f"Creating Flash project '{project_name}'..."
|
|
70
|
+
)
|
|
71
|
+
with console.status(status_msg):
|
|
72
|
+
create_project_skeleton(project_dir, should_overwrite)
|
|
73
|
+
|
|
74
|
+
# Success output
|
|
75
|
+
if is_current_dir:
|
|
76
|
+
panel_content = f"Flash project '[bold]{actual_project_name}[/bold]' initialized in current directory!\n\n"
|
|
77
|
+
panel_content += "Project structure:\n"
|
|
78
|
+
panel_content += " ./\n"
|
|
79
|
+
else:
|
|
80
|
+
panel_content = f"Flash project '[bold]{actual_project_name}[/bold]' created successfully!\n\n"
|
|
81
|
+
panel_content += "Project structure:\n"
|
|
82
|
+
panel_content += f" {actual_project_name}/\n"
|
|
83
|
+
|
|
84
|
+
panel_content += " ├── main.py # Flash Server (FastAPI)\n"
|
|
85
|
+
panel_content += " ├── mothership.py # Mothership endpoint config\n"
|
|
86
|
+
panel_content += " ├── pyproject.toml # Python project config\n"
|
|
87
|
+
panel_content += " ├── workers/\n"
|
|
88
|
+
panel_content += " │ ├── gpu/ # GPU worker\n"
|
|
89
|
+
panel_content += " │ └── cpu/ # CPU worker\n"
|
|
90
|
+
panel_content += " ├── .env.example\n"
|
|
91
|
+
panel_content += " ├── requirements.txt\n"
|
|
92
|
+
panel_content += " └── README.md\n"
|
|
93
|
+
|
|
94
|
+
title = "Project Initialized" if is_current_dir else "Project Created"
|
|
95
|
+
console.print(Panel(panel_content, title=title, expand=False))
|
|
96
|
+
|
|
97
|
+
# Next steps
|
|
98
|
+
console.print("\n[bold]Next steps:[/bold]")
|
|
99
|
+
steps_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
100
|
+
steps_table.add_column("Step", style="bold cyan")
|
|
101
|
+
steps_table.add_column("Description")
|
|
102
|
+
|
|
103
|
+
step_num = 1
|
|
104
|
+
if not is_current_dir:
|
|
105
|
+
steps_table.add_row(f"{step_num}.", f"cd {actual_project_name}")
|
|
106
|
+
step_num += 1
|
|
107
|
+
|
|
108
|
+
steps_table.add_row(f"{step_num}.", "Review and customize mothership.py (optional)")
|
|
109
|
+
step_num += 1
|
|
110
|
+
steps_table.add_row(f"{step_num}.", "pip install -r requirements.txt")
|
|
111
|
+
step_num += 1
|
|
112
|
+
steps_table.add_row(f"{step_num}.", "cp .env.example .env")
|
|
113
|
+
step_num += 1
|
|
114
|
+
steps_table.add_row(f"{step_num}.", "Add your RUNPOD_API_KEY to .env")
|
|
115
|
+
step_num += 1
|
|
116
|
+
steps_table.add_row(f"{step_num}.", "flash run")
|
|
117
|
+
|
|
118
|
+
console.print(steps_table)
|
|
119
|
+
|
|
120
|
+
console.print("\n[bold]Get your API key:[/bold]")
|
|
121
|
+
console.print(" https://docs.runpod.io/get-started/api-keys")
|
|
122
|
+
console.print("\nVisit http://localhost:8888/docs after running")
|
|
123
|
+
console.print("\nCheck out the README.md for more")
|