fundaia 0.52.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, NewEnvironment};
use crate::api::models::{Environment, ServiceCopy};
use crate::exit;
use crate::output::Format;
use crate::render::sanitise;
use crate::render::table::{Cell, Table};
use crate::render::theme;

pub async fn list(client: &ApiClient, project_reference: &str, format: Format) -> Result<()> {
    let project = client.find_project(project_reference).await?;
    let answer = client.environments(&project.id).await?;

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

    let mut table = Table::new(&["entorno", "slug", "servicios", ""]);
    for environment in &answer.body.environments {
        table.push(vec![
            Cell::styled(environment.name.clone(), theme::strong()),
            Cell::styled(environment.slug.clone(), theme::muted()),
            Cell::plain(
                environment
                    .service_count
                    .map_or_else(|| "—".to_owned(), |count| count.to_string()),
            ),
            Cell::styled(
                if environment.is_default {
                    "por defecto"
                } else {
                    ""
                },
                theme::muted(),
            ),
        ]);
    }

    table.print_or(format_args!("`{}` no tiene entornos.", project.name));
    Ok(())
}

pub struct EnvironmentRequest<'a> {
    pub project: &'a str,
    pub name: &'a str,
    pub from: Option<&'a str>,
    pub empty: bool,
    pub no_deploy: bool,
}

pub async fn create(
    client: &ApiClient,
    request: EnvironmentRequest<'_>,
    format: Format,
) -> Result<()> {
    let project = client.find_project(request.project).await?;
    let answer = client
        .create_environment(
            &project.id,
            &NewEnvironment {
                name: request.name,
                copy_from: request.from,
                empty: request.empty,
                deploy: !request.no_deploy,
            },
        )
        .await?;

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

    let created = &answer.body;
    println!();
    println!(
        "  {} {} creado en {}{}",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&created.environment.name).style(theme::strong()),
        sanitise::plain(&project.name).style(theme::strong()),
        created
            .copied_from
            .as_ref()
            .map_or_else(String::new, |source| format!(
                ", copia de {}",
                sanitise::plain(&source.name)
            )),
    );
    copies(&created.services);
    println!(
        "  {}",
        "Los volúmenes empiezan vacíos: los datos no se copian.".style(theme::muted())
    );
    println!(
        "  {}",
        format!(
            "fundaia --env {} services {}",
            created.environment.slug, project.slug
        )
        .style(theme::muted())
    );
    println!();
    Ok(())
}

pub async fn rename(
    client: &ApiClient,
    project_reference: &str,
    environment: &str,
    name: &str,
    format: Format,
) -> Result<()> {
    let project = client.find_project(project_reference).await?;
    let answer = client
        .rename_environment(&project.id, environment, name)
        .await?;

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

    println!(
        "  {} {} {}",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&answer.body.environment.name).style(theme::strong()),
        format!("(sigue siendo {})", answer.body.environment.slug).style(theme::muted()),
    );
    Ok(())
}

pub async fn remove(
    client: &ApiClient,
    project_reference: &str,
    environment_reference: &str,
    confirmation: Option<&str>,
    format: Format,
) -> Result<()> {
    let project = client.find_project(project_reference).await?;
    let environment = named(
        client.environments(&project.id).await?.body.environments,
        environment_reference,
    )?;

    if confirmation != Some(environment.name.as_str())
        && confirmation != Some(environment.slug.as_str())
    {
        return Err(exit::Refused::invalid(format!(
            "eliminar el entorno `{}` borra sus servicios, sus volúmenes con lo que tengan dentro y sus copias: repite su nombre con --confirm {}",
            environment.name, environment.slug,
        ))
        .into());
    }

    client
        .delete_environment(
            &project.id,
            &environment.id,
            confirmation.unwrap_or_default(),
        )
        .await?;

    if format.is_json() {
        crate::output::emit(&serde_json::json!({ "deleted": environment.id }));
        return Ok(());
    }

    println!(
        "  {} {} eliminado de {}",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&environment.name).style(theme::strong()),
        sanitise::plain(&project.name).style(theme::strong()),
    );
    Ok(())
}

pub struct CopyRequest<'a> {
    pub project: &'a str,
    pub service: &'a str,
    pub from: Option<&'a str>,
    pub to: &'a str,
    pub no_deploy: bool,
}

pub async fn copy(client: &ApiClient, request: CopyRequest<'_>, format: Format) -> Result<()> {
    let source = match request.from {
        Some(from) => client.with_environment(Some(from)),
        None => client.clone(),
    };
    let (project, service) = source.locate(request.project, request.service).await?;
    let answer = client
        .copy_service_to_environment(&project.id, request.to, &service.id, !request.no_deploy)
        .await?;

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

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

fn copies(services: &[ServiceCopy]) {
    for copy in services {
        let (mark, word) = match copy.status.as_str() {
            "deploying" => (theme::CHECK.style(theme::success()), "desplegando"),
            "copied" => (theme::PENDING.style(theme::muted()), "copiado"),
            _ => (theme::CROSS.style(theme::danger()), "no copiado"),
        };
        println!(
            "  {mark} {} {}",
            sanitise::plain(&copy.name).style(theme::strong()),
            word.style(theme::muted()),
        );
        if let Some(host) = &copy.host {
            println!(
                "    {} {}",
                theme::ARROW.style(theme::muted()),
                sanitise::plain(host)
            );
        }
        if let Some(reason) = &copy.reason {
            println!("    {}", sanitise::plain(reason).style(theme::muted()));
        }
        for note in &copy.notes {
            println!("    {}", sanitise::plain(note).style(theme::muted()));
        }
    }
}

fn named(environments: Vec<Environment>, reference: &str) -> Result<Environment> {
    let needle = reference.trim().to_lowercase();
    environments
        .into_iter()
        .find(|one| one.id == reference || one.slug == needle || one.name.to_lowercase() == needle)
        .ok_or_else(|| {
            exit::Refused::not_found(format!("there is no environment called `{reference}`")).into()
        })
}

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

    fn staging() -> Environment {
        Environment {
            id: "env_1".to_owned(),
            name: "Staging".to_owned(),
            slug: "staging".to_owned(),
            is_default: false,
            service_count: Some(2),
        }
    }

    #[test]
    fn it_should_find_an_environment_by_its_name_in_any_case() {
        assert_eq!(named(vec![staging()], "STAGING").unwrap().id, "env_1");
    }

    #[test]
    fn it_should_find_an_environment_by_its_id() {
        assert_eq!(named(vec![staging()], "env_1").unwrap().slug, "staging");
    }

    #[test]
    fn it_should_refuse_an_environment_that_is_not_there() {
        assert!(named(vec![staging()], "qa").is_err());
    }
}