use std::time::SystemTime;
use isothrum_runner::{Events, Orphans, Place, RunOptions, RunRecord, Runner, Secrets};
use serde_json::json;
use crate::args::ReapArgs;
use crate::error::Result;
use crate::exit::Verdict;
use crate::term::{Human, Json, Term};
use crate::values;
#[derive(Debug, Default)]
struct Swept {
destroyed: Vec<String>,
pending: Vec<String>,
failed: Vec<(String, String)>,
}
pub(crate) async fn run(args: &ReapArgs, runner: &Runner, json: bool, term: &Term, var: &dyn Fn(&str) -> Option<String>) -> Result<Verdict> {
let (as_json, for_people) = (Json(term), Human(term));
let events: &dyn Events = if json { &as_json } else { &for_people };
let cloud = args.cloud.into();
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?;
if json {
term.out(&summary(args, &swept, &orphans).to_string());
} else {
for line in lines(args, &swept, &orphans) {
term.out(&line);
}
}
Ok(Verdict::of(swept.failed.is_empty() && orphans.resources.is_empty()))
}
async fn destroy(runner: &Runner, record: &RunRecord, connection: &Secrets, events: &dyn Events) -> isothrum_runner::Result<()> {
let lab = runner.open(&record.manifest, Some(record.name.clone()))?.with_connection(connection.clone());
lab.down(&RunOptions::new(), events).await
}
fn summary(args: &ReapArgs, swept: &Swept, orphans: &Orphans) -> serde_json::Value {
let failed: Vec<serde_json::Value> = swept.failed.iter().map(|(run, why)| json!({ "run": run, "error": why })).collect();
json!({
"cloud": args.cloud.id(),
"destroyed": swept.destroyed,
"pending": swept.pending,
"failed": failed,
"orphans": orphans.resources,
"unread": orphans.unread,
})
}
fn lines(args: &ReapArgs, swept: &Swept, orphans: &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}")));
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 {}", args.cloud.id()));
}
out
}
#[cfg(test)]
mod tests;