Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
397 changes: 111 additions & 286 deletions src/runpod_flash/cli/commands/build.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/runpod_flash/cli/commands/build_utils/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ def build(self) -> Dict[str, Any]:

if explicit_mothership:
# Use explicit configuration
logger.info("Found explicit mothership configuration in mothership.py")
logger.debug("Found explicit mothership configuration in mothership.py")

# Check for name conflict
mothership_name = explicit_mothership.get("name", "mothership")
Expand Down
154 changes: 65 additions & 89 deletions src/runpod_flash/cli/commands/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import json
import logging
import shutil
import textwrap

import typer
from pathlib import Path
from rich.console import Console

from ..utils.app import discover_flash_project
from ..utils.deployment import deploy_to_environment
from ..utils.deployment import deploy_from_uploaded_build, validate_local_manifest
from .build import run_build

from runpod_flash.core.resources.app import FlashApp
Expand Down Expand Up @@ -95,11 +94,11 @@ def deploy_command(
raise typer.Exit(1)


def _display_post_deployment_guidance(env_name: str) -> None:
def _display_post_deployment_guidance(
env_name: str, mothership_url: str | None = None
) -> None:
"""Display helpful next steps after successful deployment."""
# Try to read manifest for endpoint information
manifest_path = Path.cwd() / ".flash" / "flash_manifest.json"
mothership_url = None
mothership_routes = {}

try:
Expand All @@ -109,90 +108,43 @@ def _display_post_deployment_guidance(env_name: str) -> None:
resources = manifest.get("resources", {})
routes = manifest.get("routes", {})

# Find mothership URL and routes
for resource_name, url in resources_endpoints.items():
for resource_name in resources_endpoints:
if resources.get(resource_name, {}).get("is_mothership", False):
mothership_url = url
mothership_routes = routes.get(resource_name, {})
break
except (FileNotFoundError, json.JSONDecodeError) as e:
logger.debug(f"Could not read manifest: {e}")

console.print("\n[bold]Next Steps:[/bold]\n")

# 1. Authentication
console.print("[bold cyan]1. Authentication Required[/bold cyan]")
console.print(
" All endpoints require authentication. Set your API key as an environment "
"variable. Avoid typing secrets directly into shell commands, as they may be "
"stored in your shell history."
)
console.print(
" [dim]# Recommended: store RUNPOD_API_KEY in a .env file or your shell profile[/dim]"
)
console.print(
" [dim]# Or securely prompt for it without echo (Bash example):[/dim]"
)
console.print(" [dim]read -s RUNPOD_API_KEY && export RUNPOD_API_KEY[/dim]\n")

# 2. Calling functions
console.print("[bold cyan]2. Call Your Functions[/bold cyan]")

if mothership_url:
console.print(
f" Your mothership is deployed at:\n [link]{mothership_url}[/link]\n"
)

console.print(" [bold]Using HTTP/curl:[/bold]")
if mothership_url:
curl_example = textwrap.dedent(f"""
curl -X POST {mothership_url}/YOUR_PATH \\
-H "Authorization: Bearer $RUNPOD_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{{"param1": "value1"}}'
""").strip()
else:
curl_example = textwrap.dedent("""
curl -X POST https://YOUR_ENDPOINT_URL/YOUR_PATH \\
-H "Authorization: Bearer $RUNPOD_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"param1": "value1"}'
""").strip()
console.print(f" [dim]{curl_example}[/dim]\n")

# 3. Available routes
console.print("[bold cyan]3. Available Routes[/bold cyan]")
if mothership_routes:
console.print("\n[bold]Routes:[/bold]")
for route_key in sorted(mothership_routes.keys()):
# route_key format: "POST /api/hello"
method, path = route_key.split(" ", 1)
console.print(f" [cyan]{method:6s}[/cyan] {path}")
console.print()
else:
# Routes not found - could mean manifest missing, no LB endpoints, or no routes defined
if mothership_url:
console.print(
" [dim]No routes found in manifest. Check @remote decorators in your code.[/dim]\n"
)
else:
console.print(
" Check your code for @remote decorators to find available endpoints:"
)
console.print(
' [dim]@remote(mothership, method="POST", path="/api/process")[/dim]\n'
console.print(f" {method:6s} {path}")

# curl example using the first POST route
if mothership_url and mothership_routes:
post_routes = [
k.split(" ", 1)[1]
for k in sorted(mothership_routes.keys())
if k.startswith("POST ")
]
if post_routes:
example_route = post_routes[0]
curl_cmd = (
f"curl -X POST {mothership_url}{example_route} \\\n"
f' -H "Content-Type: application/json" \\\n'
' -H "Authorization: Bearer $RUNPOD_API_KEY" \\\n'
" -d '{\"input\": {}}'"
)
console.print("\n[bold]Try it:[/bold]")
console.print(f" [dim]{curl_cmd}[/dim]")

# 4. Monitor & Debug
console.print("[bold cyan]4. Monitor & Debug[/bold cyan]")
console.print(f" [dim]flash env get {env_name}[/dim] - View environment status")
console.print("\n[bold]Useful commands:[/bold]")
console.print(
" [dim]Runpod Console[/dim] - View logs and metrics at https://console.runpod.io/serverless\n"
f" [dim]flash env get {env_name}[/dim] View environment status"
)

# 5. Update & Teardown
console.print("[bold cyan]5. Update or Remove Deployment[/bold cyan]")
console.print(f" [dim]flash deploy --env {env_name}[/dim] - Update deployment")
console.print(f" [dim]flash env delete {env_name}[/dim] - Remove deployment\n")
console.print(f" [dim]flash deploy --env {env_name}[/dim] Update deployment")
console.print(f" [dim]flash env delete {env_name}[/dim] Remove deployment")


def _launch_preview(project_dir):
Expand All @@ -216,17 +168,43 @@ def _launch_preview(project_dir):
async def _resolve_and_deploy(
app_name: str, env_name: str | None, archive_path
) -> None:
resolved_env_name = await _resolve_environment(app_name, env_name)
app, resolved_env_name = await _resolve_environment(app_name, env_name)

console.print(f"\nDeploying to '[bold]{resolved_env_name}[/bold]'...")
local_manifest = validate_local_manifest()

await deploy_to_environment(app_name, resolved_env_name, archive_path)
with console.status("Uploading build..."):
build = await app.upload_build(archive_path)

# Display next steps guidance
_display_post_deployment_guidance(resolved_env_name)
with console.status("Deploying resources..."):
result = await deploy_from_uploaded_build(
app, build["id"], resolved_env_name, local_manifest
)
console.print(f"[green]Deployed[/green] to [bold]{resolved_env_name}[/bold]")

resources_endpoints = result.get("resources_endpoints", {})
local_manifest = result.get("local_manifest", {})
resources = local_manifest.get("resources", {})

async def _resolve_environment(app_name: str, env_name: str | None) -> str:
# mothership first, then workers
mothership_url = None
if resources_endpoints:
console.print()
other_items = []
for resource_name, url in resources_endpoints.items():
if resources.get(resource_name, {}).get("is_mothership", False):
mothership_url = url
console.print(f" [bold]{url}[/bold] [dim]({resource_name})[/dim]")
else:
other_items.append((resource_name, url))
for resource_name, url in other_items:
console.print(f" [dim]{url} ({resource_name})[/dim]")

_display_post_deployment_guidance(resolved_env_name, mothership_url=mothership_url)


async def _resolve_environment(
app_name: str, env_name: str | None
) -> tuple[FlashApp, str]:
try:
app = await FlashApp.from_name(app_name)
except Exception as exc:
Expand All @@ -236,8 +214,8 @@ async def _resolve_environment(app_name: str, env_name: str | None) -> str:
console.print(
f"[dim]No app '{app_name}' found. Creating app and '{target}' environment...[/dim]"
)
await FlashApp.create_environment_and_app(app_name, target)
return target
app, _ = await FlashApp.create_environment_and_app(app_name, target)
return app, target

if env_name:
envs = await app.list_environments()
Expand All @@ -247,21 +225,19 @@ async def _resolve_environment(app_name: str, env_name: str | None) -> str:
f"[dim]Environment '{env_name}' not found. Creating it...[/dim]"
)
await app.create_environment(env_name)
return env_name
return app, env_name

envs = await app.list_environments()

if len(envs) == 1:
resolved = envs[0].get("name")
console.print(f"[dim]Auto-selected environment: {resolved}[/dim]")
return resolved
return app, envs[0].get("name")

if len(envs) == 0:
console.print(
"[dim]No environments found. Creating 'production' environment...[/dim]"
)
await app.create_environment("production")
return "production"
return app, "production"

env_names = [e.get("name", "?") for e in envs]
console.print(
Expand Down
Loading
Loading