lmrc-cli 0.3.16

CLI tool for scaffolding LMRC Stack infrastructure projects
Documentation
use crate::error::{CliError, Result};
use colored::Colorize;
use lmrc_config_validator::LmrcConfig;
use lmrc_hetzner::{HetznerClient, ServerManager, NetworkManager, LoadBalancerManager, FirewallManager, SshKeyManager};
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;

/// Execute the destroy command
pub async fn execute(skip_confirmation: bool, project: Option<String>) -> Result<()> {
    println!("{}", "Destroying Infrastructure".red().bold());
    println!();

    // Load configuration
    let config_path = if let Some(p) = project {
        PathBuf::from(p)
    } else {
        PathBuf::from("lmrc.toml")
    };

    if !config_path.exists() {
        return Err(CliError::Config(format!(
            "Configuration file not found: {}",
            config_path.display()
        )));
    }

    let config_str = fs::read_to_string(&config_path).map_err(|e| {
        CliError::IoError(format!("Failed to read config file: {}", e))
    })?;

    let config: LmrcConfig = toml::from_str(&config_str).map_err(|e| {
        CliError::Config(format!("Failed to parse config: {}", e))
    })?;

    // Initialize Hetzner client
    let hetzner_token = std::env::var("HETZNER_API_TOKEN").map_err(|_| {
        CliError::MissingEnvironmentVariable(
            "HETZNER_API_TOKEN is required. Set it with: export HETZNER_API_TOKEN=your_token"
                .to_string(),
        )
    })?;

    let client = HetznerClient::builder()
        .api_token(hetzner_token)
        .build()
        .map_err(|e| CliError::Infrastructure(format!("Failed to create Hetzner client: {}", e)))?;

    // Discover infrastructure to destroy
    println!("{}", "Discovering infrastructure...".cyan());
    println!();

    let server_manager = ServerManager::new(client.clone());
    let network_manager = NetworkManager::new(client.clone());
    let lb_manager = LoadBalancerManager::new(client.clone());
    let firewall_manager = FirewallManager::new(client.clone());
    let ssh_key_manager = SshKeyManager::new(client.clone());

    // List all resources with project label
    let project_label = format!("project={}", config.project.name);

    // Get servers
    let all_servers = server_manager
        .list_servers()
        .await
        .map_err(|e| CliError::Infrastructure(format!("Failed to list servers: {}", e)))?;

    let servers: Vec<_> = all_servers
        .into_iter()
        .filter(|s| {
            s.labels.get("project").map(|v| v.as_str()) == Some(&config.project.name)
        })
        .collect();

    // Get load balancers
    let all_lbs = lb_manager
        .list_load_balancers()
        .await
        .map_err(|e| CliError::Infrastructure(format!("Failed to list load balancers: {}", e)))?;

    let load_balancers: Vec<_> = all_lbs
        .into_iter()
        .filter(|lb| {
            lb.labels.get("project").map(|v| v.as_str()) == Some(&config.project.name)
        })
        .collect();

    // Get networks
    let all_networks = network_manager
        .list_networks()
        .await
        .map_err(|e| CliError::Infrastructure(format!("Failed to list networks: {}", e)))?;

    let networks: Vec<_> = all_networks
        .into_iter()
        .filter(|n| {
            n.labels.get("project").map(|v| v.as_str()) == Some(&config.project.name)
        })
        .collect();

    // Get firewalls
    let all_firewalls = firewall_manager
        .list_firewalls()
        .await
        .map_err(|e| CliError::Infrastructure(format!("Failed to list firewalls: {}", e)))?;

    let firewalls: Vec<_> = all_firewalls
        .into_iter()
        .filter(|f| {
            f.labels.get("project").map(|v| v.as_str()) == Some(&config.project.name)
        })
        .collect();

    // Get SSH keys
    let all_ssh_keys = ssh_key_manager
        .list_ssh_keys()
        .await
        .map_err(|e| CliError::Infrastructure(format!("Failed to list SSH keys: {}", e)))?;

    let ssh_keys: Vec<_> = all_ssh_keys
        .into_iter()
        .filter(|k| {
            k.labels.get("project").map(|v| v.as_str()) == Some(&config.project.name)
        })
        .collect();

    // Display what will be destroyed
    if servers.is_empty()
        && load_balancers.is_empty()
        && networks.is_empty()
        && firewalls.is_empty()
        && ssh_keys.is_empty()
    {
        println!("{}", "No infrastructure found to destroy".yellow());
        println!("  Project: {}", config.project.name);
        return Ok(());
    }

    println!(
        "{}",
        format!("Found infrastructure for project: {}", config.project.name)
            .yellow()
            .bold()
    );
    println!();

    if !servers.is_empty() {
        println!("  {} Servers ({}):", "".red(), servers.len());
        for server in &servers {
            println!(
                "    - {} (ID: {}, Type: {}, Status: {})",
                server.name.bright_white(),
                server.id,
                server.server_type.name,
                server.status.cyan()
            );
        }
    }

    if !load_balancers.is_empty() {
        println!("  {} Load Balancers ({}):", "".red(), load_balancers.len());
        for lb in &load_balancers {
            println!(
                "    - {} (ID: {}, Type: {})",
                lb.name.bright_white(),
                lb.id,
                lb.load_balancer_type.name
            );
        }
    }

    if !networks.is_empty() {
        println!("  {} Networks ({}):", "".red(), networks.len());
        for network in &networks {
            println!(
                "    - {} (ID: {}, Subnet: {})",
                network.name.bright_white(),
                network.id,
                network.ip_range
            );
        }
    }

    if !firewalls.is_empty() {
        println!("  {} Firewalls ({}):", "".red(), firewalls.len());
        for firewall in &firewalls {
            println!(
                "    - {} (ID: {}, Rules: {})",
                firewall.name.bright_white(),
                firewall.id,
                firewall.rules.len()
            );
        }
    }

    if !ssh_keys.is_empty() {
        println!("  {} SSH Keys ({}):", "".red(), ssh_keys.len());
        for key in &ssh_keys {
            println!("    - {} (ID: {})", key.name.bright_white(), key.id);
        }
    }

    println!();

    // Confirmation prompt
    if !skip_confirmation {
        print!(
            "{}",
            "⚠️  This will permanently delete all resources listed above. Are you sure? (yes/no): "
                .yellow()
                .bold()
        );
        io::stdout().flush().unwrap();

        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .map_err(|e| CliError::IoError(format!("Failed to read input: {}", e)))?;

        let input = input.trim().to_lowercase();
        if input != "yes" && input != "y" {
            println!("{}", "Destruction cancelled".green());
            return Ok(());
        }
    }

    println!();
    println!("{}", "Starting destruction process...".red().bold());
    println!();

    // Destroy resources in reverse dependency order

    // 1. Delete servers (must be done before networks/firewalls can be deleted)
    if !servers.is_empty() {
        println!("{}", "Deleting servers...".cyan());
        for server in servers {
            print!("  Deleting {} (ID: {})... ", server.name, server.id);
            io::stdout().flush().unwrap();

            server_manager
                .delete_server(server.id)
                .await
                .map_err(|e| {
                    CliError::Infrastructure(format!(
                        "Failed to delete server {}: {}",
                        server.name, e
                    ))
                })?;

            println!("{}", "".green());
        }
        println!();
    }

    // 2. Delete load balancers
    if !load_balancers.is_empty() {
        println!("{}", "Deleting load balancers...".cyan());
        for lb in load_balancers {
            print!("  Deleting {} (ID: {})... ", lb.name, lb.id);
            io::stdout().flush().unwrap();

            lb_manager
                .delete_load_balancer(lb.id)
                .await
                .map_err(|e| {
                    CliError::Infrastructure(format!(
                        "Failed to delete load balancer {}: {}",
                        lb.name, e
                    ))
                })?;

            println!("{}", "".green());
        }
        println!();
    }

    // 3. Delete firewalls (must be done before servers they're attached to are gone, but we already deleted servers)
    // In practice, Hetzner detaches firewalls automatically when servers are deleted
    if !firewalls.is_empty() {
        println!("{}", "Deleting firewalls...".cyan());
        for firewall in firewalls {
            print!("  Deleting {} (ID: {})... ", firewall.name, firewall.id);
            io::stdout().flush().unwrap();

            firewall_manager
                .delete_firewall(firewall.id)
                .await
                .map_err(|e| {
                    CliError::Infrastructure(format!(
                        "Failed to delete firewall {}: {}",
                        firewall.name, e
                    ))
                })?;

            println!("{}", "".green());
        }
        println!();
    }

    // 4. Delete networks (must be done after servers are deleted)
    if !networks.is_empty() {
        println!("{}", "Deleting networks...".cyan());
        for network in networks {
            print!("  Deleting {} (ID: {})... ", network.name, network.id);
            io::stdout().flush().unwrap();

            network_manager
                .delete_network(network.id)
                .await
                .map_err(|e| {
                    CliError::Infrastructure(format!(
                        "Failed to delete network {}: {}",
                        network.name, e
                    ))
                })?;

            println!("{}", "".green());
        }
        println!();
    }

    // 5. Delete SSH keys
    if !ssh_keys.is_empty() {
        println!("{}", "Deleting SSH keys...".cyan());
        for key in ssh_keys {
            print!("  Deleting {} (ID: {})... ", key.name, key.id);
            io::stdout().flush().unwrap();

            client
                .delete(&format!("/ssh_keys/{}", key.id))
                .await
                .map_err(|e| {
                    CliError::Infrastructure(format!("Failed to delete SSH key {}: {}", key.name, e))
                })?;

            println!("{}", "".green());
        }
        println!();
    }

    println!();
    println!(
        "{}",
        "✓ All infrastructure successfully destroyed".green().bold()
    );
    println!();
    println!("Project '{}' cleanup complete", config.project.name);

    Ok(())
}