Skip to content
Open
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
85 changes: 1 addition & 84 deletions src/tetra_rp/cli/commands/resource.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"""Resource management commands."""

import time
import typer
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.live import Live
from rich.progress import Progress, SpinnerColumn, TextColumn
import questionary
import time

from ...core.resources.resource_manager import ResourceManager

Expand Down Expand Up @@ -42,58 +40,6 @@ def report_command(
console.print(table)


def clean_command(
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
):
"""Remove all tracked resources after confirmation."""

resource_manager = ResourceManager()
resources = resource_manager._resources

if not resources:
console.print("🧹 No resources to clean")
return

# Show cleanup preview
console.print(generate_cleanup_preview(resources))

# Confirmation unless forced
if not force:
try:
confirmed = questionary.confirm(
"Are you sure you want to clean all resources?"
).ask()

if not confirmed:
console.print("Cleanup cancelled")
return
except KeyboardInterrupt:
console.print("\nCleanup cancelled")
return

# Clean resources with progress
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("Cleaning resources...", total=len(resources))

for uid in list(resources.keys()):
resource = resources[uid]
progress.update(
task, description=f"Removing {resource.__class__.__name__}..."
)

# Remove resource (this will also clean up remotely if needed)
resource_manager.remove_resource(uid)

progress.advance(task)
time.sleep(0.1) # Small delay for visual feedback

console.print("All resources cleaned successfully")


def generate_resource_table(resource_manager: ResourceManager) -> Panel:
"""Generate a formatted table of resources."""

Expand Down Expand Up @@ -160,32 +106,3 @@ def generate_resource_table(resource_manager: ResourceManager) -> Panel:
summary += ")"

return Panel(table, subtitle=summary, expand=False)


def generate_cleanup_preview(resources: dict) -> Panel:
"""Generate a preview of resources to be cleaned."""

content = "The following resources will be removed:\n\n"

for uid, resource in resources.items():
resource_type = resource.__class__.__name__

try:
status = "Active" if resource.is_deployed() else "Inactive"
except Exception:
status = "Unknown"

try:
url = (
f" - {resource.url}"
if hasattr(resource, "url") and resource.url != "N/A"
else ""
)
except Exception:
url = ""

content += f" • {resource_type} ({status}){url}\n"

content += "\n⚠️ This action cannot be undone!"

return Panel(content, title="🧹 Cleanup Preview", expand=False)
93 changes: 7 additions & 86 deletions src/tetra_rp/cli/commands/undeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from ...core.resources.base import DeployableResource
from ...core.resources.resource_manager import ResourceManager
from ...core.api.runpod import RunpodGraphQLClient

console = Console()

Expand Down Expand Up @@ -130,47 +129,6 @@ def list_command():
)


async def _delete_endpoint(endpoint_id: str, resource_id: str, name: str) -> dict:
"""Delete an endpoint via RunPod API and remove from tracking.

Args:
endpoint_id: RunPod endpoint ID
resource_id: Local resource ID for tracking
name: Human-readable name

Returns:
Dict with success status and message
"""
try:
async with RunpodGraphQLClient() as client:
result = await client.delete_endpoint(endpoint_id)

if result.get("success"):
# Remove from tracking
manager = ResourceManager()
manager.remove_resource(resource_id)
return {
"success": True,
"name": name,
"endpoint_id": endpoint_id,
"message": f"Successfully deleted endpoint '{name}' ({endpoint_id})",
}
else:
return {
"success": False,
"name": name,
"endpoint_id": endpoint_id,
"message": f"Failed to delete endpoint '{name}' ({endpoint_id})",
}
except Exception as e:
return {
"success": False,
"name": name,
"endpoint_id": endpoint_id,
"message": f"Error deleting endpoint '{name}': {str(e)}",
}


def _cleanup_stale_endpoints(
resources: Dict[str, DeployableResource], manager: ResourceManager
) -> None:
Expand Down Expand Up @@ -219,7 +177,7 @@ def _cleanup_stale_endpoints(
removed_count = 0
for resource_id, resource in inactive:
try:
manager.remove_resource(resource_id)
manager._remove_resource(resource_id)
removed_count += 1
console.print(
f"[green]✓[/green] Removed [cyan]{resource.name}[/cyan] from tracking"
Expand Down Expand Up @@ -359,24 +317,11 @@ def _undeploy_by_name(name: str, resources: dict):
raise typer.Exit(0)

# Delete endpoints
manager = ResourceManager()
with console.status("Deleting endpoint(s)..."):
results = []
for resource_id, resource in matches:
endpoint_id = getattr(resource, "id", None)
if not endpoint_id:
results.append(
{
"success": False,
"name": resource.name,
"endpoint_id": "N/A",
"message": f"Skipped '{resource.name}': No endpoint ID found",
}
)
continue

result = asyncio.run(
_delete_endpoint(endpoint_id, resource_id, resource.name)
)
result = asyncio.run(manager.undeploy_resource(resource_id, resource.name))
results.append(result)

# Show results
Expand Down Expand Up @@ -437,24 +382,12 @@ def _undeploy_all(resources: dict):
raise typer.Exit(0)

# Delete all endpoints
manager = ResourceManager()
with console.status(f"Deleting {len(resources)} endpoint(s)..."):
results = []
for resource_id, resource in resources.items():
endpoint_id = getattr(resource, "id", None)
name = getattr(resource, "name", "N/A")

if not endpoint_id:
results.append(
{
"success": False,
"name": name,
"endpoint_id": "N/A",
"message": f"Skipped '{name}': No endpoint ID found",
}
)
continue

result = asyncio.run(_delete_endpoint(endpoint_id, resource_id, name))
result = asyncio.run(manager.undeploy_resource(resource_id, name))
results.append(result)

# Show results
Expand Down Expand Up @@ -534,24 +467,12 @@ def _interactive_undeploy(resources: dict):
raise typer.Exit(0)

# Delete selected endpoints
manager = ResourceManager()
with console.status(f"Deleting {len(selected_resources)} endpoint(s)..."):
results = []
for resource_id, resource in selected_resources:
endpoint_id = getattr(resource, "id", None)
name = getattr(resource, "name", "N/A")

if not endpoint_id:
results.append(
{
"success": False,
"name": name,
"endpoint_id": "N/A",
"message": f"Skipped '{name}': No endpoint ID found",
}
)
continue

result = asyncio.run(_delete_endpoint(endpoint_id, resource_id, name))
result = asyncio.run(manager.undeploy_resource(resource_id, name))
results.append(result)

# Show results
Expand Down
1 change: 0 additions & 1 deletion src/tetra_rp/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def get_version() -> str:
app.command("run")(run.run_command)
app.command("build")(build.build_command)
# app.command("report")(resource.report_command)
# app.command("clean")(resource.clean_command)


# command: flash deploy
Expand Down
9 changes: 9 additions & 0 deletions src/tetra_rp/core/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,12 @@ def is_deployed(self) -> bool:
async def deploy(self) -> "DeployableResource":
"""Deploy the resource."""
raise NotImplementedError("Subclasses should implement this method.")

@abstractmethod
async def undeploy(self) -> bool:
"""Undeploy/delete the resource.

Returns:
True if successfully undeployed, False otherwise
"""
raise NotImplementedError("Subclasses should implement this method.")
15 changes: 15 additions & 0 deletions src/tetra_rp/core/resources/network_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,18 @@ async def deploy(self) -> "DeployableResource":
except Exception as e:
log.error(f"{self} failed to deploy: {e}")
raise

async def undeploy(self) -> bool:
"""
Undeploy network volume.

Returns:
True if successfully undeployed, False otherwise

Raises:
NotImplementedError: NetworkVolume undeploy is not yet supported
"""
raise NotImplementedError(
f"{self.__class__.__name__} undeploy is not yet supported. "
"Network volumes must be manually deleted via RunPod UI or API."
)
Loading
Loading