fundaia 0.53.0

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;
use crate::api::models::{BulkAnswer, BulkRow};
use crate::exit;
use crate::output::Format;
use crate::render::sanitise;
use crate::render::table::{Cell, Table};
use crate::render::theme;

pub async fn run(
    client: &ApiClient,
    action: &str,
    project_reference: &str,
    services: &[String],
    format: Format,
) -> Result<()> {
    let project = client.find_project(project_reference).await?;
    let answer = client.bulk(&project.id, action, services).await?;

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

    let body = &answer.body;
    println!();
    println!(
        "  {} {} en {} · {}",
        theme::ARROW.style(theme::accent()),
        verb(&body.action).style(theme::strong()),
        sanitise::plain(&body.environment.name).style(theme::strong()),
        summary(body).style(theme::muted()),
    );
    println!();

    let mut table = Table::new(&["servicio", "resultado", "detalle"]);
    for row in &body.results {
        table.push(vec![
            Cell::styled(sanitise::plain(&row.name), theme::strong()),
            outcome_cell(row),
            Cell::styled(detail(row), theme::muted()),
        ]);
    }
    table.print_or(format_args!(
        "`{}` no tiene servicios en este entorno.",
        project.name
    ));

    if body.started > 0 && (body.action == "deploy" || body.action == "restart") {
        println!();
        println!(
            "  {}",
            format!(
                "Sigue cada uno con: fundaia history {} <servicio>",
                project.slug
            )
            .style(theme::muted()),
        );
    }
    println!();

    outcome(body)
}

fn outcome(answer: &BulkAnswer) -> Result<()> {
    if answer.started == 0 && answer.refused > 0 {
        return Err(exit::Refused::invalid(summary(answer)).into());
    }
    Ok(())
}

fn verb(action: &str) -> &str {
    match action {
        "deploy" => "Desplegar",
        "restart" => "Reiniciar",
        "sleep" => "Dormir",
        "wake" => "Despertar",
        other => other,
    }
}

fn summary(answer: &BulkAnswer) -> String {
    let started = match answer.started {
        1 => "1 en marcha".to_owned(),
        count => format!("{count} en marcha"),
    };
    match answer.refused {
        0 => started,
        1 => format!("{started}, 1 rechazado"),
        count => format!("{started}, {count} rechazados"),
    }
}

fn outcome_cell(row: &BulkRow) -> Cell {
    if row.outcome == "started" {
        Cell::styled(format!("{} en marcha", theme::CHECK), theme::success())
    } else {
        Cell::styled(format!("{} rechazado", theme::CROSS), theme::danger())
    }
}

fn detail(row: &BulkRow) -> String {
    match (&row.deployment, &row.reason) {
        (Some(deployment), _) => format!("despliegue #{}", deployment.number),
        (None, Some(reason)) => sanitise::plain(reason),
        (None, None) => "—".to_owned(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::api::models::{BulkDeployment, BulkEnvironment};

    fn answer(started: u32, refused: u32) -> BulkAnswer {
        BulkAnswer {
            action: "deploy".to_owned(),
            environment: BulkEnvironment {
                id: "env_1".to_owned(),
                name: "production".to_owned(),
                slug: "production".to_owned(),
            },
            started,
            refused,
            results: Vec::new(),
        }
    }

    fn row(deployment: Option<u32>, reason: Option<&str>) -> BulkRow {
        BulkRow {
            service_environment_id: "sve_1".to_owned(),
            name: "web".to_owned(),
            outcome: if deployment.is_some() {
                "started".to_owned()
            } else {
                "refused".to_owned()
            },
            reason: reason.map(str::to_owned),
            deployment: deployment.map(|number| BulkDeployment {
                id: "dep_1".to_owned(),
                number,
            }),
        }
    }

    #[test]
    fn it_should_count_what_started_and_what_was_refused() {
        assert_eq!(summary(&answer(2, 1)), "2 en marcha, 1 rechazado");
    }

    #[test]
    fn it_should_say_only_what_started_when_nothing_was_refused() {
        assert_eq!(summary(&answer(3, 0)), "3 en marcha");
    }

    #[test]
    fn it_should_name_the_deployment_it_started() {
        assert_eq!(detail(&row(Some(7), None)), "despliegue #7");
    }

    #[test]
    fn it_should_give_the_reason_for_a_refusal() {
        assert_eq!(
            detail(&row(None, Some("Ya está dormido"))),
            "Ya está dormido"
        );
    }

    #[test]
    fn it_should_fail_when_every_service_was_refused() {
        assert!(outcome(&answer(0, 2)).is_err());
    }

    #[test]
    fn it_should_succeed_when_some_started() {
        assert!(outcome(&answer(1, 2)).is_ok());
    }
}