Skip to content

Commit ca66f54

Browse files
committed
feat: flash app build uploads and deployments with state manager
1 parent 38df0b9 commit ca66f54

File tree

8 files changed

+164
-49
lines changed

8 files changed

+164
-49
lines changed

src/tetra_rp/cli/commands/build.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import subprocess
66
import sys
77
import tarfile
8+
import asyncio
89
from pathlib import Path
910

1011
import typer
@@ -14,6 +15,8 @@
1415
from rich.table import Table
1516

1617
from ..utils.ignore import get_file_tree, load_ignore_patterns
18+
from ..utils.app import discover_flash_project
19+
from tetra_rp.core.resources.app import FlashApp
1720

1821
console = Console()
1922

@@ -31,6 +34,9 @@ def build_command(
3134
output_name: str | None = typer.Option(
3235
None, "--output", "-o", help="Custom archive name (default: archive.tar.gz)"
3336
),
37+
local_only: bool = typer.Option(
38+
False, "--local-only", help="Only store build tarball locally and dont upload to Runpod"
39+
),
3440
):
3541
"""
3642
Build Flash application for deployment.
@@ -150,6 +156,11 @@ def build_command(
150156

151157
# Success summary
152158
_display_build_summary(archive_path, app_name, len(files), len(requirements))
159+
160+
if local_only:
161+
return
162+
163+
asyncio.run(upload_build(app_name, archive_path))
153164

154165
except KeyboardInterrupt:
155166
console.print("\n[yellow]Build cancelled by user[/yellow]")
@@ -161,21 +172,9 @@ def build_command(
161172
console.print(traceback.format_exc())
162173
raise typer.Exit(1)
163174

164-
165-
def discover_flash_project() -> tuple[Path, str]:
166-
"""
167-
Discover Flash project directory and app name.
168-
169-
Returns:
170-
Tuple of (project_dir, app_name)
171-
172-
Raises:
173-
typer.Exit: If not in a Flash project directory
174-
"""
175-
project_dir = Path.cwd()
176-
app_name = project_dir.name
177-
178-
return project_dir, app_name
175+
async def upload_build(app_name: str, build_path: str | Path):
176+
app = await FlashApp.from_name(app_name)
177+
await app.upload_build(build_path)
179178

180179

181180
def validate_project_structure(project_dir: Path) -> bool:

src/tetra_rp/cli/commands/deploy.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
from rich.console import Console
55
from rich.table import Table
66
from rich.panel import Panel
7+
from rich.progress import Progress, SpinnerColumn, TextColumn
78
import questionary
9+
import asyncio
10+
from typing import Optional
811

12+
from ..utils.app import discover_flash_project
913
from ..utils.deployment import (
1014
get_deployment_environments,
1115
create_deployment_environment,
@@ -15,8 +19,71 @@
1519
get_environment_info,
1620
)
1721

22+
from tetra_rp.core.resources import FlashApp
23+
1824
console = Console()
1925

26+
deploy_app = typer.Typer(short_help="Deploy flash application code to managed environments on Runpod")
27+
28+
@deploy_app.callback(invoke_without_command=True)
29+
def deploy_callback(
30+
ctx: typer.Context,
31+
app_name: str = typer.Option(None, "--app-name", "-a", help="Flash app name to deploy a build for"),
32+
env_name: str = typer.Option("dev", "--env-name", "-e", help="Flash env name to deploy a build to. If not provided, default to 'dev'"),
33+
build_id: str = typer.Option(None, "--build-id", "-b", help="Flash build id to deploy to env. If not provided, default to most recently uploaded build"),
34+
):
35+
if ctx.invoked_subcommand is None:
36+
deploy_build_sync(app_name, env_name, build_id)
37+
38+
def deploy_build_sync(app_name: str, env_name: str, build_id: str):
39+
"""
40+
Convenience wrapper to run async methods from cli
41+
"""
42+
asyncio.run(deploy_build(app_name, env_name, build_id))
43+
44+
async def deploy_build(app_name: Optional[str] = "", env_name: Optional[str] = "dev", build_id: Optional[str] = None):
45+
target_env = env_name or "dev"
46+
progress_columns = [SpinnerColumn(), TextColumn("[progress.description]{task.description}")]
47+
48+
with Progress(*progress_columns, console=console) as progress:
49+
task = progress.add_task("Preparing deployment", start=True)
50+
51+
if not app_name:
52+
progress.update(task, description="Discovering Flash project...")
53+
_, app_name = discover_flash_project()
54+
progress.update(task, description=f"Detected Flash app '{app_name}'")
55+
56+
progress.update(task, description=f"Fetching Flash app '{app_name}'...")
57+
app = await FlashApp.from_name(app_name)
58+
59+
if not build_id:
60+
progress.update(task, description="Fetching available builds...")
61+
builds = await app.list_builds()
62+
if not builds:
63+
progress.update(task, description="No builds found for app")
64+
progress.stop_task(task)
65+
raise ValueError("No builds found for app. Run 'flash build' first.")
66+
build_id = builds[0]["id"]
67+
progress.update(task, description=f"Using latest build {build_id}")
68+
69+
progress.update(
70+
task,
71+
description=f"Promoting build {build_id} to environment '{target_env}'...",
72+
)
73+
result = await app.deploy_build_to_environment(build_id, environment_name=target_env)
74+
progress.update(
75+
task,
76+
description=f"[green]✓ Promoted build {build_id} to '{target_env}'",
77+
)
78+
progress.stop_task(task)
79+
80+
panel_content = (
81+
f"Promoted build [bold]{build_id}[/bold] to environment [bold]{target_env}[/bold]\n"
82+
f"Flash app: [bold]{app_name}[/bold]"
83+
)
84+
console.print(Panel(panel_content, title="Deployment Promotion", expand=False))
85+
86+
return result
2087

2188
def list_command():
2289
"""Show available deployment environments."""

src/tetra_rp/cli/commands/init.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from rich.console import Console
77
from rich.panel import Panel
88
from rich.table import Table
9+
from tetra_rp.core.resources.app import FlashApp
10+
import asyncio
911

1012
from ..utils.skeleton import create_project_skeleton
1113
from ..utils.conda import (
@@ -27,6 +29,9 @@
2729
"aiohttp>=3.9.0",
2830
]
2931

32+
async def init_app(app_name: str):
33+
app = await FlashApp.create(app_name)
34+
await app.create_environment("dev")
3035

3136
def init_command(
3237
project_name: str = typer.Argument(..., help="Project name"),
@@ -36,6 +41,9 @@ def init_command(
3641
force: bool = typer.Option(
3742
False, "--force", "-f", help="Overwrite existing directory"
3843
),
44+
local_only: bool = typer.Option(
45+
False, "--local-only", help="Do not create remote app on Runpod"
46+
),
3947
):
4048
"""Create new Flash project with Flash Server and GPU workers."""
4149

@@ -95,6 +103,9 @@ def init_command(
95103
"You can manually install dependencies: pip install -r requirements.txt"
96104
)
97105

106+
if not local_only:
107+
asyncio.run(init_app(project_name))
108+
98109
# Success output
99110
panel_content = (
100111
f"Flash project '[bold]{project_name}[/bold]' created successfully!\n\n"

src/tetra_rp/cli/commands/state.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,22 @@
11
"""Flash deploy command - Deploy Flash project to production."""
22
import asyncio
3-
import json
4-
from pathlib import Path
5-
from typing import Optional
63

74
from tetra_rp import FlashApp
85

96
import typer
107
from rich.console import Console
11-
from rich.panel import Panel
12-
from rich.progress import Progress, SpinnerColumn, TextColumn
138
from rich.table import Table
149

1510
console = Console()
16-
ls_app = typer.Typer()
1711

18-
# flash ls
19-
# flash ls envs --app-name
20-
# flash ls builds --app-name
12+
ls_app = typer.Typer(short_help="List existing apps, environments, and builds")
2113

2214
@ls_app.callback(invoke_without_command=True)
2315
def list_callback(ctx: typer.Context):
2416
if ctx.invoked_subcommand is None:
2517
asyncio.run(list_flash_apps())
2618

27-
@ls_app.command("ls")
19+
@ls_app.command()
2820
def ls():
2921
return asyncio.run(list_flash_apps())
3022

src/tetra_rp/cli/main.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
build,
1212
resource,
1313
deploy,
14+
state,
1415
)
1516

1617

@@ -39,22 +40,8 @@ def get_version() -> str:
3940
app.command("report")(resource.report_command)
4041
app.command("clean")(resource.clean_command)
4142

42-
# command: flash deploy
43-
deploy_app = typer.Typer(
44-
name="deploy",
45-
help="Deployment environment management commands",
46-
no_args_is_help=True,
47-
)
48-
49-
# command: flash deploy *
50-
deploy_app.command("list")(deploy.list_command)
51-
deploy_app.command("new")(deploy.new_command)
52-
deploy_app.command("send")(deploy.send_command)
53-
deploy_app.command("report")(deploy.report_command)
54-
deploy_app.command("rollback")(deploy.rollback_command)
55-
deploy_app.command("remove")(deploy.remove_command)
56-
57-
app.add_typer(deploy_app, name="deploy")
43+
app.add_typer(deploy.deploy_app, name="deploy")
44+
app.add_typer(state.ls_app, name="ls")
5845

5946

6047
@app.callback(invoke_without_command=True)

src/tetra_rp/cli/utils/app.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from pathlib import Path
2+
3+
def discover_flash_project() -> tuple[Path, str]:
4+
"""
5+
Discover Flash project directory and app name.
6+
7+
Returns:
8+
Tuple of (project_dir, app_name)
9+
10+
Raises:
11+
typer.Exit: If not in a Flash project directory
12+
"""
13+
project_dir = Path.cwd()
14+
app_name = project_dir.name
15+
16+
return project_dir, app_name
17+
18+

src/tetra_rp/core/api/runpod.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import json
77
import logging
88
import os
9-
from typing import Any, Dict, Optional, List
9+
from typing import Any, Dict, Optional
1010

1111
import aiohttp
1212

@@ -356,13 +356,14 @@ async def get_flash_environment_by_name(self, input_data: Dict[str, Any]) -> Dic
356356
name
357357
}
358358
}
359+
}
359360
"""
360361
variables = {"input": input_data}
361362

362363
log.debug(f"Fetching flash environment by name for input: {variables}")
363364
result = await self._execute_graphql(query, variables)
364365

365-
return result["flashEnvironment"]
366+
return result["flashEnvironmentByName"]
366367

367368
async def get_flash_artifact_url(self, environment_id: str) -> Dict[str, Any]:
368369
result = await self.get_flash_environment({"flashEnvironmentId": environment_id})
@@ -502,6 +503,21 @@ async def set_environment_state(self, input_data: Dict[str, Any]) -> Dict[str, A
502503

503504
return result["updateFlashEnvironment"]
504505

506+
async def get_flash_build(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
507+
query = """
508+
query getFlashBuild($input: String!) {
509+
flashBuild(flashBuildId: $input) {
510+
id
511+
name
512+
}
513+
}
514+
"""
515+
variables = {"input": input_data}
516+
517+
log.debug(f"Fetching flash build for input: {input_data}")
518+
result = await self._execute_graphql(query, variables)
519+
return result["flashBuild"]
520+
505521
async def close(self):
506522
"""Close the HTTP session."""
507523
if self.session and not self.session.closed:
@@ -514,7 +530,6 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
514530
await self.close()
515531

516532

517-
518533
class RunpodRestClient:
519534
"""
520535
Runpod REST client for Runpod API.

0 commit comments

Comments
 (0)