fundaia 0.4.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::render::logs::{render_line, LineFormat};
use crate::render::theme;

/// A service's log, from whichever deployment is asked for.
///
/// Scoped to a deployment rather than to a service, because that is what the
/// platform stores: the runtime output of one container, plus everything its
/// build printed.
///
/// The *newest* by default, not the active one. Asking for the active one meant
/// a service that was asleep or had failed printed "no deployment to read
/// from" — the two states where somebody is most likely to want the log, and a
/// failed deployment's log is the whole reason to ask for it.
/// Everything the flags decide: which deployment, how much of it, how it looks.
///
/// Grouped rather than passed one by one, because the alternative reads
/// `run(&client, &p, &s, false, 200, None, None, true)` at the call site and
/// only the compiler can tell which `true` is which.
pub struct LogQuery<'a> {
    pub follow: bool,
    pub tail: usize,
    pub stream_filter: Option<&'a str>,
    pub number: Option<u32>,
    pub timestamps: bool,
}

pub async fn run(
    client: &ApiClient,
    project_reference: &str,
    service_reference: &str,
    query: LogQuery<'_>,
) -> Result<()> {
    let format = LineFormat::with_stream().showing_time(query.timestamps);

    let project = client.find_project(project_reference).await?;
    let service = client.find_service(&project.id, service_reference).await?;
    let deployments = client.deployments(&service.id).await?;

    let chosen = match query.number {
        Some(wanted) => deployments.iter().find(|one| one.number == wanted),
        None => deployments.first(),
    };

    let Some(deployment) = chosen.cloned() else {
        match query.number {
            Some(wanted) => println!("`{}` no tiene ningún despliegue #{wanted}.", service.name),
            None => println!("`{}` no se ha desplegado todavía.", service.name),
        }
        return Ok(());
    };

    let matches = |candidate: &str| query.stream_filter.is_none_or(|wanted| candidate == wanted);

    if !query.follow {
        let lines = client.read_log(&deployment.id, 0).await?;
        // Tailed here rather than server-side: the endpoint pages forward from a
        // sequence, so "the last N" is a question only the client can answer
        // without a second round trip to find out how many there are.
        let start = lines.len().saturating_sub(query.tail);

        for line in lines
            .iter()
            .skip(start)
            .filter(|line| matches(&line.stream))
        {
            println!("{}", render_line(line, format));
        }
        return Ok(());
    }

    println!(
        "  {} {} #{} · Ctrl-C para salir",
        theme::ARROW.style(theme::accent()),
        service.name.style(theme::strong()),
        deployment.number,
    );

    client
        .stream_log(&deployment.id, 0, |line| {
            if matches(&line.stream) {
                println!("{}", render_line(&line, format));
            }
        })
        .await
}