cufflink-cli 0.7.15

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use crate::config::CliConfig;

pub async fn set_status(name: &str, status: &str, env: Option<&str>) -> eyre::Result<()> {
    let config = CliConfig::load_with_env(env)?;

    if let Some(ref env_name) = config.env_name {
        println!("Environment: {}", env_name);
    }

    let client = config.http_client();

    // Find service ID by name
    let resp = config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services", config.api_url),
        )
        .send()
        .await?;

    if !resp.status().is_success() {
        eyre::bail!("Failed to list services: {}", resp.status());
    }

    let body: serde_json::Value = resp.json().await?;
    let service = body["services"]
        .as_array()
        .and_then(|arr| arr.iter().find(|s| s["name"].as_str() == Some(name)))
        .ok_or_else(|| eyre::eyre!("Service '{}' not found", name))?;

    let service_id = service["id"]
        .as_str()
        .ok_or_else(|| eyre::eyre!("Service has no ID"))?;

    let resp = config
        .auth_request(
            &client,
            reqwest::Method::PATCH,
            &format!("{}/api/services/{}/status", config.api_url, service_id),
        )
        .json(&serde_json::json!({ "status": status }))
        .send()
        .await?;

    if resp.status().is_success() {
        let action = if status == "active" {
            "enabled"
        } else {
            "disabled"
        };
        println!("Service '{}' {}.", name, action);
    } else {
        let status_code = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!(
            "Failed to update service status ({}): {}",
            status_code,
            body
        );
    }

    Ok(())
}

pub async fn delete(name: &str, yes: bool, env: Option<&str>) -> eyre::Result<()> {
    let config = CliConfig::load_with_env(env)?;

    if let Some(ref env_name) = config.env_name {
        println!("Environment: {}", env_name);
    }

    let client = config.http_client();

    // Find service ID by name
    let resp = config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services", config.api_url),
        )
        .send()
        .await?;

    if !resp.status().is_success() {
        eyre::bail!("Failed to list services: {}", resp.status());
    }

    let body: serde_json::Value = resp.json().await?;
    let service = body["services"]
        .as_array()
        .and_then(|arr| arr.iter().find(|s| s["name"].as_str() == Some(name)))
        .ok_or_else(|| eyre::eyre!("Service '{}' not found", name))?;

    let service_id = service["id"]
        .as_str()
        .ok_or_else(|| eyre::eyre!("Service has no ID"))?;

    // Confirm unless --yes
    if !yes {
        println!(
            "This will permanently delete service '{}' and all its configs, deployments, and data.",
            name
        );
        print!("Are you sure? [y/N] ");
        use std::io::Write;
        std::io::stdout().flush()?;
        let mut input = String::new();
        std::io::stdin().read_line(&mut input)?;
        if !input.trim().eq_ignore_ascii_case("y") {
            println!("Aborted.");
            return Ok(());
        }
    }

    let resp = config
        .auth_request(
            &client,
            reqwest::Method::DELETE,
            &format!("{}/api/services/{}", config.api_url, service_id),
        )
        .send()
        .await?;

    if resp.status().is_success() {
        println!("Service '{}' deleted.", name);
    } else {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Failed to delete service ({}): {}", status, body);
    }

    Ok(())
}

pub async fn run(env: Option<&str>) -> eyre::Result<()> {
    let config = CliConfig::load_with_env(env)?;

    if let Some(ref name) = config.env_name {
        println!("Environment: {}", name);
    }

    let client = config.http_client();

    let resp = config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services", config.api_url),
        )
        .send()
        .await?;

    if !resp.status().is_success() {
        eyre::bail!("Failed to list services: {}", resp.status());
    }

    let body: serde_json::Value = resp.json().await?;
    let services = body["services"].as_array();

    match services {
        Some(services) if !services.is_empty() => {
            use comfy_table::{presets::NOTHING, Table, TableComponent};

            let mut table = Table::new();
            table.load_preset(NOTHING);
            table.set_style(TableComponent::HeaderLines, '-');
            table.set_style(TableComponent::MiddleHeaderIntersections, ' ');
            table.set_header(vec![
                "NAME", "VERSION", "MODE", "STATUS", "TENANT", "COMMIT",
            ]);

            for svc in services {
                let commit = svc["commit_hash"]
                    .as_str()
                    .map(|h| if h.len() > 7 { &h[..7] } else { h })
                    .unwrap_or("-");
                table.add_row(vec![
                    svc["name"].as_str().unwrap_or("?"),
                    &format!("v{}", svc["current_version"]),
                    svc["mode"].as_str().unwrap_or("?"),
                    svc["status"].as_str().unwrap_or("?"),
                    svc["tenant_slug"].as_str().unwrap_or("?"),
                    commit,
                ]);
            }

            println!("{table}");
            println!("\n{} service(s)", services.len());
        }
        _ => {
            println!("No services deployed yet.");
            println!("Use `cufflink deploy` to deploy your first service.");
        }
    }

    Ok(())
}