use std::time::SystemTime;
use isothrum_runner::{EventKind, Events, Orphans, Place, RunOptions, RunRecord, Runner, Secrets, wire_name};
use serde_json::json;
use crate::args::ReapArgs;
use crate::commands::Out;
use crate::error::Result;
use crate::exit::Verdict;
use crate::term::{Trail, failure};
use crate::values::{self, Var};
#[derive(Debug, Default)]
struct Swept {
destroyed: Vec<String>,
pending: Vec<String>,
failed: Vec<(String, String)>,
}
pub(crate) async fn run(args: &ReapArgs, runner: &Runner, out: &Out<'_>, var: &Var<'_>) -> Result<Verdict> {
let Out { json, term, events } = *out;
let cloud = args.cloud;
let connection = values::secrets(&args.secrets, var)?;
let now = SystemTime::now();
let expired: Vec<RunRecord> = runner.expired(now)?.into_iter().filter(|r| r.cloud == Some(cloud)).collect();
let mut swept = Swept::default();
for record in &expired {
let name = record.name.to_string();
if args.dry_run {
swept.pending.push(name);
continue;
}
match destroy(runner, record, &connection, events).await {
Ok(()) => swept.destroyed.push(name),
Err(e) => swept.failed.push((name, e.to_string())),
}
}
let orphans = runner.orphans(cloud, &Place::new(args.regions.clone(), args.project.clone()), now).await;
let searched = orphans.as_ref().ok();
let clean = swept.failed.is_empty() && swept.pending.is_empty() && searched.is_some_and(|o| o.resources.is_empty() && o.unread.is_empty());
let sweep = Trail::default();
if json {
let data = summary(args, &swept, searched);
term.json(&sweep.follow(None, None, EventKind::Result { ok: clean, data }));
} else {
for line in lines(args, &swept, searched) {
term.out(&line);
}
}
if let Err(e) = &orphans {
if json {
term.json(&sweep.follow(None, None, failure(e.code(), e.to_string())));
} else {
term.err(&format!("error: {e}"));
}
}
Ok(Verdict::of(clean))
}
async fn destroy(runner: &Runner, record: &RunRecord, connection: &Secrets, events: &dyn Events) -> isothrum_runner::Result<()> {
let run = runner.open(&record.manifest, Some(record.name.clone()))?.with_connection(connection.clone());
run.down(&RunOptions::new(), events).await
}
fn summary(args: &ReapArgs, swept: &Swept, orphans: Option<&Orphans>) -> serde_json::Value {
let failed: Vec<serde_json::Value> = swept.failed.iter().map(|(run, why)| json!({ "run": run, "error": why })).collect();
json!({
"cloud": wire_name(&args.cloud),
"destroyed": swept.destroyed,
"pending": swept.pending,
"failed": failed,
"orphans": orphans.map(|o| &o.resources),
"unread": orphans.map(|o| &o.unread),
})
}
fn lines(args: &ReapArgs, swept: &Swept, orphans: Option<&Orphans>) -> Vec<String> {
let mut out = Vec::new();
out.extend(swept.destroyed.iter().map(|r| format!("destroyed {r}")));
out.extend(swept.pending.iter().map(|r| format!("would destroy {r} (--dry-run)")));
out.extend(swept.failed.iter().map(|(r, why)| format!("failed {r}: {why}")));
let Some(orphans) = orphans else {
return out;
};
for o in &orphans.resources {
let env = o.instance.as_deref().unwrap_or("unknown");
out.push(format!(
"left {} {} (environment {env}): no run records it; remove it in the cloud's console",
o.kind, o.id
));
}
out.extend(orphans.unread.iter().map(|why| format!("not checked: {why}")));
if out.is_empty() {
out.push(format!("nothing to reap on {}", wire_name(&args.cloud)));
}
out
}
#[cfg(test)]
mod tests;