fundaia 0.55.3

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
use anyhow::Result;
use owo_colors::OwoColorize;

use crate::api::client::{ApiClient, ComposeDraft};
use crate::api::models::{ComposeImport, PlacedStack, StacksPage};
use crate::commands::environments;
use crate::output::Format;
use crate::render::sanitise;
use crate::render::theme;

pub fn is_compose(path: &str, text: &str) -> bool {
    let lower = path.to_lowercase();
    if lower.ends_with(".yml") || lower.ends_with(".yaml") {
        return true;
    }
    if lower.ends_with(".json") {
        return false;
    }
    !text.trim_start().starts_with('{')
}

pub struct ComposeRequest<'a> {
    pub text: &'a str,
    pub name: Option<&'a str>,
    pub workspace: Option<&'a str>,
    pub project: Option<&'a str>,
    pub repository: Option<&'a str>,
    pub branch: Option<&'a str>,
    pub deploy: bool,
}

pub async fn import_compose(
    client: &ApiClient,
    request: ComposeRequest<'_>,
    format: Format,
) -> Result<()> {
    let workspace = match request.workspace {
        Some(reference) => Some(client.find_workspace(Some(reference)).await?.id),
        None => None,
    };
    let project = match request.project {
        Some(reference) => Some(client.find_project(reference).await?.id),
        None => None,
    };
    let answer = client
        .import_compose(&ComposeDraft {
            compose: request.text,
            name: request.name,
            workspace_id: workspace.as_deref(),
            project_id: project.as_deref(),
            repository: request.repository,
            branch: request.branch,
            deploy: request.deploy,
        })
        .await?;

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    print_compose(&answer.body);
    Ok(())
}

fn print_compose(result: &ComposeImport) {
    println!();
    println!(
        "  {} {} {} {}",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&result.project.name).style(theme::strong()),
        if result.created_project {
            "creado desde el docker-compose"
        } else {
            "recibe los servicios del docker-compose"
        },
        format!("({})", sanitise::plain(&result.environment.name)).style(theme::muted()),
    );

    for service in &result.services {
        let (mark, word) = match service.status.as_str() {
            "deploying" => (
                theme::CHECK.style(theme::success()).to_string(),
                "desplegando",
            ),
            "created" => (theme::CHECK.style(theme::success()).to_string(), "creado"),
            _ => (theme::CROSS.style(theme::danger()).to_string(), "omitido"),
        };
        println!(
            "    {mark} {} {}",
            sanitise::plain(&service.name).style(theme::strong()),
            word.style(theme::muted())
        );
        if let Some(host) = &service.host {
            println!(
                "        {} https://{}",
                theme::ARROW.style(theme::muted()),
                sanitise::plain(host)
            );
        }
        if let Some(reason) = &service.reason {
            println!("        {}", sanitise::plain(reason).style(theme::muted()));
        }
        for note in &service.notes {
            println!("        {}", sanitise::plain(note).style(theme::muted()));
        }
    }

    if !result.pending_variables.is_empty() {
        println!();
        println!("  Variables que el docker-compose no resuelve:");
        for pending in &result.pending_variables {
            println!(
                "    {}",
                format!(
                    "fundaia vars set {} {} {} …",
                    result.project.slug,
                    sanitise::plain(&pending.service),
                    sanitise::plain(&pending.key)
                )
                .style(theme::muted())
            );
        }
    }
    println!();
}

pub async fn list_stacks(client: &ApiClient, format: Format) -> Result<()> {
    let answer = client.stacks().await?;

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    print_stacks(&answer.body);
    Ok(())
}

fn print_stacks(page: &StacksPage) {
    for stack in &page.stacks {
        println!();
        println!(
            "  {}  {}  {}",
            stack.key.style(theme::accent()),
            sanitise::plain(&stack.display_name).style(theme::strong()),
            format!("{} MB", stack.memory_mb).style(theme::muted()),
        );
        println!(
            "      {}",
            sanitise::plain(&stack.summary).style(theme::muted())
        );
        let members: Vec<String> = stack
            .services
            .iter()
            .map(|member| {
                let name = sanitise::plain(&member.name);
                if member.public {
                    format!("{name} (público)")
                } else {
                    name
                }
            })
            .collect();
        println!("      {}", members.join(" · "));
    }
    println!();
}

pub struct StackRequest<'a> {
    pub key: &'a str,
    pub project: Option<&'a str>,
    pub name: Option<&'a str>,
    pub workspace: Option<&'a str>,
    pub deploy: bool,
}

pub async fn add_stack(
    client: &ApiClient,
    request: StackRequest<'_>,
    format: Format,
) -> Result<()> {
    let answer = match request.project {
        Some(reference) => {
            let project = client.find_project(reference).await?;
            client
                .add_stack(&project.id, request.key, request.deploy)
                .await?
        }
        None => {
            let workspace = match request.workspace {
                Some(reference) => Some(client.find_workspace(Some(reference)).await?.id),
                None => None,
            };
            client
                .create_from_stack(
                    request.key,
                    request.name,
                    workspace.as_deref(),
                    request.deploy,
                )
                .await?
        }
    };

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    print_stack(&answer.body);
    Ok(())
}

fn print_stack(placed: &PlacedStack) {
    println!();
    println!(
        "  {} {} en {} {}",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&placed.stack.display_name).style(theme::strong()),
        sanitise::plain(&placed.project.name).style(theme::strong()),
        format!("({})", sanitise::plain(&placed.environment.name)).style(theme::muted()),
    );
    for service in &placed.services {
        let word = if service.status == "deploying" {
            "desplegando"
        } else {
            "creado"
        };
        println!(
            "    {} {} {}",
            theme::CHECK.style(theme::success()),
            sanitise::plain(&service.name),
            word.style(theme::muted())
        );
        if let Some(reason) = &service.reason {
            println!("        {}", sanitise::plain(reason).style(theme::muted()));
        }
        for note in &service.notes {
            println!("        {}", sanitise::plain(note).style(theme::muted()));
        }
    }
    if let Some(host) = &placed.host {
        println!(
            "    {} https://{}",
            theme::ARROW.style(theme::muted()),
            sanitise::plain(host)
        );
    }
    if !placed.stack.sign_in.is_empty() {
        println!(
            "    {}",
            sanitise::plain(&placed.stack.sign_in).style(theme::muted())
        );
    }
    println!();
}

pub async fn duplicate(
    client: &ApiClient,
    project_reference: &str,
    service_reference: &str,
    name: &str,
    deploy: bool,
    format: Format,
) -> Result<()> {
    let (_, service) = client.locate(project_reference, service_reference).await?;
    let answer = client.duplicate_service(&service.id, name, deploy).await?;

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    println!();
    environments::copies(std::slice::from_ref(&answer.body.service));
    println!();
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_should_read_a_yaml_file_as_a_compose_file() {
        assert!(is_compose("docker-compose.yml", "{}"));
    }

    #[test]
    fn it_should_read_a_json_file_as_a_manifest() {
        assert!(!is_compose("tienda.fundaia.json", "services:"));
    }

    #[test]
    fn it_should_read_a_compose_file_of_another_name_by_its_content() {
        assert!(is_compose(
            "compose",
            "services:\n  web:\n    image: nginx\n"
        ));
    }

    #[test]
    fn it_should_read_a_manifest_of_another_name_by_its_content() {
        assert!(!is_compose("tienda", "  {\"manifest\": {}}"));
    }

    #[test]
    fn it_should_read_the_extension_whatever_its_case() {
        assert!(is_compose("COMPOSE.YAML", "{}"));
    }
}