forked from innovacion/Mayacontigo
338 lines
10 KiB
Python
Executable File
338 lines
10 KiB
Python
Executable File
#!/usr/bin/env -S uv run --script
|
|
#MISE silent=true
|
|
#MISE description="Run the container for a project"
|
|
# /// script
|
|
# dependencies = ["rich"]
|
|
# ///
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from typing import Optional
|
|
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
from rich.prompt import Prompt, Confirm
|
|
from rich.table import Table
|
|
|
|
console = Console()
|
|
|
|
# Configuration
|
|
VAULT_ADDRESS = "https://vault.ia-innovacion.work"
|
|
DOCKER_REGISTRY = "mayacontigo"
|
|
DEFAULT_PORT = 9000
|
|
CONTAINER_PORT = 80
|
|
|
|
|
|
def run_command(command: list[str], description: str, capture_output: bool = True) -> tuple[bool, str]:
|
|
"""Run a command with rich progress indication."""
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
console=console,
|
|
transient=True,
|
|
) as progress:
|
|
task = progress.add_task(description, total=None)
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
command,
|
|
capture_output=capture_output,
|
|
text=True,
|
|
check=True
|
|
)
|
|
progress.update(task, description=f"✅ {description}")
|
|
return True, result.stdout.strip() if capture_output else ""
|
|
except subprocess.CalledProcessError as e:
|
|
progress.update(task, description=f"❌ {description}")
|
|
error_msg = e.stderr if capture_output else str(e)
|
|
console.print(f"[red]Error running command: {' '.join(command)}[/red]")
|
|
if error_msg:
|
|
console.print(f"[red]Error output: {error_msg}[/red]")
|
|
return False, ""
|
|
except FileNotFoundError:
|
|
progress.update(task, description=f"❌ {description}")
|
|
console.print(f"[red]Command not found: {command[0]}. Please ensure it's installed and in your PATH.[/red]")
|
|
return False, ""
|
|
|
|
|
|
def get_vault_token() -> Optional[str]:
|
|
"""Get vault token from Vault CLI."""
|
|
success, output = run_command(
|
|
["vault", "token", "lookup", "-format=json", f"-address={VAULT_ADDRESS}"],
|
|
"Fetching Vault token"
|
|
)
|
|
|
|
if not success:
|
|
return None
|
|
|
|
try:
|
|
data = json.loads(output)
|
|
return data.get("data", {}).get("id")
|
|
except json.JSONDecodeError:
|
|
console.print("[red]Failed to parse Vault token response[/red]")
|
|
return None
|
|
|
|
|
|
def check_docker_image_exists(app_name: str) -> bool:
|
|
"""Check if the Docker image exists locally or can be pulled."""
|
|
image_name = f"{DOCKER_REGISTRY}/{app_name}"
|
|
|
|
# First check if image exists locally
|
|
success, _ = run_command(
|
|
["docker", "images", "-q", image_name],
|
|
f"Checking if {image_name} exists locally"
|
|
)
|
|
|
|
if success:
|
|
return True
|
|
|
|
# If not local, try to pull it
|
|
console.print(f"[yellow]Image {image_name} not found locally. Attempting to pull...[/yellow]")
|
|
success, _ = run_command(
|
|
["docker", "pull", image_name],
|
|
f"Pulling {image_name}"
|
|
)
|
|
|
|
return success
|
|
|
|
|
|
def get_available_apps() -> list[str]:
|
|
"""Get list of available Docker images from the registry."""
|
|
try:
|
|
result = subprocess.run(
|
|
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
images = [line.strip() for line in result.stdout.split('\n') if line.strip()]
|
|
# Filter for banortegpt registry images
|
|
apps = []
|
|
for img in images:
|
|
if img.startswith(f"{DOCKER_REGISTRY}/"):
|
|
app_name = img.replace(f"{DOCKER_REGISTRY}/", "").split(":")[0]
|
|
if app_name not in apps:
|
|
apps.append(app_name)
|
|
return apps
|
|
except subprocess.CalledProcessError:
|
|
return []
|
|
|
|
|
|
def display_available_apps(apps: list[str]) -> None:
|
|
"""Display available apps in a nice table."""
|
|
if not apps:
|
|
console.print("[yellow]No apps found locally.[/yellow]")
|
|
console.print("[dim]Try pulling an image first or check your Docker registry connection.[/dim]")
|
|
return
|
|
|
|
table = Table(title="Available Apps")
|
|
table.add_column("App Name", style="cyan")
|
|
table.add_column("Image", style="magenta")
|
|
table.add_column("Status", style="green")
|
|
|
|
for app in apps:
|
|
image = f"{DOCKER_REGISTRY}/{app}"
|
|
status = "🚀 Ready to run"
|
|
table.add_row(app, image, status)
|
|
|
|
console.print(table)
|
|
|
|
|
|
def interactive_mode() -> Optional[str]:
|
|
"""Run in interactive mode to select an app."""
|
|
console.print(Panel.fit(
|
|
"[bold blue]🐳 Docker Container Starter[/bold blue]\n"
|
|
"[dim]Interactive mode - Let's select an app to run[/dim]",
|
|
border_style="blue"
|
|
))
|
|
|
|
# Get available apps
|
|
console.print("\n[bold]Fetching available apps...[/bold]")
|
|
apps = get_available_apps()
|
|
|
|
if not apps:
|
|
console.print("[red]No apps found. Please ensure you have Docker images available.[/red]")
|
|
return None
|
|
|
|
display_available_apps(apps)
|
|
|
|
# Prompt for app selection
|
|
console.print("\n[bold]Select an app to run:[/bold]")
|
|
app_name = Prompt.ask(
|
|
"Enter the app name (or press Enter to see suggestions)",
|
|
default="",
|
|
show_default=False
|
|
)
|
|
|
|
if not app_name:
|
|
# Show suggestions
|
|
console.print("\n[dim]Here are your available apps:[/dim]")
|
|
for i, app in enumerate(apps, 1):
|
|
console.print(f" {i}. [cyan]{app}[/cyan]")
|
|
|
|
choice = Prompt.ask(
|
|
"Enter app name or number from suggestions",
|
|
default="1"
|
|
)
|
|
|
|
try:
|
|
choice_num = int(choice)
|
|
if 1 <= choice_num <= len(apps):
|
|
app_name = apps[choice_num - 1]
|
|
else:
|
|
console.print("[red]Invalid choice.[/red]")
|
|
return None
|
|
except ValueError:
|
|
app_name = choice
|
|
|
|
return app_name
|
|
|
|
|
|
def start_container(app_name: str, port: int = DEFAULT_PORT, force: bool = False) -> bool:
|
|
"""Start a Docker container for the specified app."""
|
|
# Validate app name
|
|
if not app_name:
|
|
console.print("[red]Error: App name cannot be empty[/red]")
|
|
return False
|
|
|
|
# Check if image exists
|
|
if not check_docker_image_exists(app_name):
|
|
console.print(f"[red]Error: Image for app '{app_name}' not found[/red]")
|
|
return False
|
|
|
|
# Get vault token
|
|
console.print("\n[bold]Getting Vault token...[/bold]")
|
|
vault_token = get_vault_token()
|
|
if not vault_token:
|
|
console.print("[red]Failed to get Vault token[/red]")
|
|
return False
|
|
|
|
# Prepare container configuration
|
|
image_name = f"{DOCKER_REGISTRY}/{app_name}"
|
|
|
|
# Show what we're about to do
|
|
info_panel = Panel.fit(
|
|
f"[bold]App:[/bold] {app_name}\n"
|
|
f"[bold]Image:[/bold] {image_name}\n"
|
|
f"[bold]Port:[/bold] {port}:{CONTAINER_PORT}\n"
|
|
f"[bold]Vault:[/bold] {VAULT_ADDRESS}\n"
|
|
f"[bold]Token:[/bold] {'*' * 8}...{vault_token[-4:] if len(vault_token) > 8 else '****'}",
|
|
title="🚀 Container Configuration",
|
|
border_style="green"
|
|
)
|
|
console.print(info_panel)
|
|
|
|
# Confirm action unless force is specified
|
|
if not force:
|
|
if not Confirm.ask(f"\nStart container for [cyan]{app_name}[/cyan] on port {port}?"):
|
|
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
return False
|
|
|
|
console.print("\n[bold]Starting container...[/bold]")
|
|
|
|
# Build docker run command
|
|
docker_cmd = [
|
|
"docker", "run",
|
|
"-p", f"{port}:{CONTAINER_PORT}",
|
|
"--rm",
|
|
"-it",
|
|
"-e", f"VAULT_TOKEN={vault_token}",
|
|
image_name
|
|
]
|
|
|
|
# Run the container (this will be interactive)
|
|
console.print(f"[green]Running: {' '.join(docker_cmd[:7])}... {image_name}[/green]")
|
|
console.print(f"[dim]Container will be available at: http://localhost:{port}[/dim]")
|
|
console.print("[dim]Press Ctrl+C to stop the container[/dim]\n")
|
|
|
|
try:
|
|
subprocess.run(docker_cmd, check=True)
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
console.print(f"[red]Container failed to start: {e}[/red]")
|
|
return False
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]Container stopped by user.[/yellow]")
|
|
return True
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Start Docker containers for Banorte GPT apps",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python start.py my-app # Start specific app
|
|
python start.py --interactive # Interactive mode
|
|
python start.py my-app --port 8080 # Custom port
|
|
python start.py my-app --force # Skip confirmation
|
|
"""
|
|
)
|
|
|
|
parser.add_argument(
|
|
"app_name",
|
|
nargs="?",
|
|
help="Name of the app to start"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-i", "--interactive",
|
|
action="store_true",
|
|
help="Run in interactive mode"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-p", "--port",
|
|
type=int,
|
|
default=DEFAULT_PORT,
|
|
help=f"Port to bind to (default: {DEFAULT_PORT})"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-f", "--force",
|
|
action="store_true",
|
|
help="Skip confirmation prompts"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--list",
|
|
action="store_true",
|
|
help="List available apps and exit"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Handle --list option
|
|
if args.list:
|
|
console.print("[bold]Available Apps:[/bold]")
|
|
apps = get_available_apps()
|
|
display_available_apps(apps)
|
|
return
|
|
|
|
# Determine app name
|
|
app_name = args.app_name
|
|
|
|
# Interactive mode or no app provided
|
|
if args.interactive or not app_name:
|
|
app_name = interactive_mode()
|
|
if not app_name:
|
|
sys.exit(1)
|
|
|
|
# Start the container
|
|
success = start_container(app_name, args.port, args.force)
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]Operation cancelled by user.[/yellow]")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
console.print(f"[red]Unexpected error: {e}[/red]")
|
|
sys.exit(1)
|