use std::time::Duration;
use anyhow::Result;
use owo_colors::OwoColorize;
use crate::api::client::{ApiClient, TaskDraft};
use crate::api::models::{ScheduledTask, TaskRun};
use crate::exit;
use crate::output::{self, Format};
use crate::render::sanitise;
use crate::render::table::{Cell, Table};
use crate::render::theme;
use crate::render::time;
const WAIT_LIMIT: Duration = Duration::from_secs(16 * 60);
const POLL_EVERY: Duration = Duration::from_secs(1);
pub async fn list(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
format: Format,
) -> Result<()> {
let (project, service) = client.locate(project_reference, service_reference).await?;
let answer = client.tasks(&service.id).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let mut table = Table::new(&["tarea", "cuándo (UTC)", "próxima", "última", "comando"]);
for task in &answer.body.tasks {
table.push(vec![
Cell::styled(
sanitise::plain(&task.name),
if task.enabled {
theme::strong()
} else {
theme::muted()
},
),
Cell::plain(sanitise::plain(&task.schedule)),
Cell::styled(next_word(task), theme::muted()),
last_run_cell(task.last_run.as_ref()),
Cell::styled(sanitise::plain(&task.command), theme::muted()),
]);
}
table.print_or(format_args!(
"`{}` no tiene tareas. fundaia tasks add {} {} <nombre> \"0 3 * * *\" -- <comando>",
service.name, project.slug, service.slug
));
Ok(())
}
pub struct AddRequest<'a> {
pub project: &'a str,
pub service: &'a str,
pub name: &'a str,
pub schedule: &'a str,
pub command: &'a [String],
pub paused: bool,
}
pub async fn add(client: &ApiClient, request: AddRequest<'_>, format: Format) -> Result<()> {
let (_, service) = client.locate(request.project, request.service).await?;
let command = request.command.join(" ");
let answer = client
.create_task(
&service.id,
&TaskDraft {
name: Some(request.name),
command: Some(&command),
schedule: Some(request.schedule),
enabled: request.paused.then_some(false),
},
)
.await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let task = &answer.body.task;
println!(
" {} {} programada: {}",
theme::CHECK.style(theme::success()),
sanitise::plain(&task.name).style(theme::strong()),
next_word(task).style(theme::muted())
);
Ok(())
}
pub struct EditRequest<'a> {
pub project: &'a str,
pub service: &'a str,
pub task: &'a str,
pub name: Option<&'a str>,
pub schedule: Option<&'a str>,
pub command: &'a [String],
pub enabled: Option<bool>,
}
pub async fn edit(client: &ApiClient, request: EditRequest<'_>, format: Format) -> Result<()> {
let (_, service) = client.locate(request.project, request.service).await?;
let task = find(client, &service.id, request.task).await?;
let command = (!request.command.is_empty()).then(|| request.command.join(" "));
let answer = client
.update_task(
&task.id,
&TaskDraft {
name: request.name,
command: command.as_deref(),
schedule: request.schedule,
enabled: request.enabled,
},
)
.await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let task = &answer.body.task;
println!(
" {} {} {}",
theme::CHECK.style(theme::success()),
sanitise::plain(&task.name).style(theme::strong()),
next_word(task).style(theme::muted())
);
Ok(())
}
pub async fn remove(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
task_reference: &str,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
let task = find(client, &service.id, task_reference).await?;
client.delete_task(&task.id).await?;
if format.is_json() {
output::emit(&serde_json::json!({ "deleted": task.id, "name": task.name }));
return Ok(());
}
println!(
" {} {} quitada",
theme::CHECK.style(theme::success()),
sanitise::plain(&task.name).style(theme::strong())
);
Ok(())
}
pub async fn run(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
task_reference: &str,
wait: bool,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
let task = find(client, &service.id, task_reference).await?;
let started = client.run_task(&task.id).await?;
let mut answer = started;
if wait {
let deadline = std::time::Instant::now() + WAIT_LIMIT;
while answer.body.run.status == "running" && std::time::Instant::now() < deadline {
tokio::time::sleep(POLL_EVERY).await;
answer = client.task_run(&answer.body.run.id).await?;
}
}
if format.answered_in_json(&answer.raw) {
return Ok(());
}
print_run(&answer.body.run);
Ok(())
}
pub async fn runs(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
task_reference: &str,
format: Format,
) -> Result<()> {
let (_, service) = client.locate(project_reference, service_reference).await?;
let task = find(client, &service.id, task_reference).await?;
let answer = client.task_runs(&task.id).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let mut table = Table::new(&["resultado", "empezó", "duró", "origen", "motivo"]);
for run in &answer.body.runs {
table.push(vec![
status_cell(run),
Cell::plain(time::when(&run.started_at)),
Cell::styled(
run.duration_millis
.map(time::format_millis)
.unwrap_or_default(),
theme::muted(),
),
Cell::styled(
if run.trigger == "manual" {
"a mano"
} else {
"programada"
},
theme::muted(),
),
Cell::styled(
sanitise::plain(run.reason.as_deref().unwrap_or("")),
theme::muted(),
),
]);
}
table.print_or("Todavía no se ha ejecutado.");
if let Some(newest) = answer.body.runs.first() {
if !newest.output.is_empty() {
println!();
print!("{}", sanitise::with_colour(&newest.output));
if !newest.output.ends_with('\n') {
println!();
}
}
}
Ok(())
}
async fn find(
client: &ApiClient,
service_environment_id: &str,
reference: &str,
) -> Result<ScheduledTask> {
let tasks = client.tasks(service_environment_id).await?.body.tasks;
let wanted = reference.trim();
let known: Vec<String> = tasks.iter().map(|task| task.name.clone()).collect();
tasks
.into_iter()
.find(|task| task.id == wanted || task.name.eq_ignore_ascii_case(wanted))
.ok_or_else(|| {
exit::Refused::not_found(if known.is_empty() {
format!("No hay ninguna tarea `{wanted}`: este servicio no tiene tareas")
} else {
format!("No hay ninguna tarea `{wanted}`. Hay: {}", known.join(", "))
})
.into()
})
}
fn next_word(task: &ScheduledTask) -> String {
if !task.enabled {
return "en pausa".to_owned();
}
task.next_run_at
.as_deref()
.map_or_else(|| "no vuelve a tocar".to_owned(), time::until)
}
fn last_run_cell(run: Option<&TaskRun>) -> Cell {
match run {
None => Cell::styled("—", theme::muted()),
Some(run) => status_cell(run),
}
}
fn status_cell(run: &TaskRun) -> Cell {
match run.status.as_str() {
"succeeded" => Cell::styled("bien", theme::success()),
"running" => Cell::styled("en marcha", theme::accent()),
"skipped" => Cell::styled("saltada", theme::muted()),
_ => Cell::styled(
run.exit_code
.map_or_else(|| "falló".to_owned(), |code| format!("falló (exit {code})")),
theme::danger(),
),
}
}
fn print_run(run: &TaskRun) {
if !run.output.is_empty() {
print!("{}", sanitise::with_colour(&run.output));
if !run.output.ends_with('\n') {
println!();
}
}
let took = run
.duration_millis
.map(time::format_millis)
.unwrap_or_default();
match run.status.as_str() {
"succeeded" => eprintln!(" {}", format!("bien · {took}").style(theme::muted())),
"running" => eprintln!(
" {}",
"Sigue en marcha: `fundaia tasks runs` dirá cómo acaba.".style(theme::muted())
),
_ => {
let why = run
.reason
.clone()
.or_else(|| run.exit_code.map(|code| format!("exit {code}")))
.unwrap_or_else(|| "falló".to_owned());
eprintln!(
" {} {}",
theme::CROSS.style(theme::danger()),
format!("{} · {took}", sanitise::plain(&why)).style(theme::danger())
);
}
}
}