1mod tags;
14
15use std::collections::BTreeSet;
16use std::time::{Duration, SystemTime};
17
18use isoloom_manifest::Cloud;
19pub use tags::{Place, Tagged};
20
21use crate::Result;
22use crate::clock::unix_seconds;
23use crate::process::{ProcessRunner, ProcessSpec, capture};
24use crate::runner::Runner;
25use crate::state::RunState;
26use crate::store::RunRecord;
27
28const QUERY_LIMIT: Duration = Duration::from_secs(120);
30
31#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
33#[non_exhaustive]
34pub struct Orphans {
35 pub resources: Vec<Tagged>,
37 pub unread: Vec<String>,
40}
41
42impl Runner {
43 pub fn expired(&self, now: SystemTime) -> Result<Vec<RunRecord>> {
50 let now = unix_seconds(now);
51 let records = self.store.records()?;
52 Ok(records
53 .into_iter()
54 .filter(|r| r.state != RunState::Absent && r.expires_at.is_some_and(|at| at <= now))
55 .collect())
56 }
57
58 pub async fn orphans(&self, cloud: Cloud, place: &Place, now: SystemTime) -> Result<Orphans> {
66 let covered = self.covered(cloud)?;
67 let now = unix_seconds(now);
68 let mut out = Orphans::default();
69 for spec in tags::queries(cloud, place) {
70 let answer = query(self.config.processes.as_ref(), &spec).await.and_then(|text| tags::parse(cloud, &text));
71 match answer {
72 Ok(found) => out.resources.extend(found.into_iter().filter(|t| orphaned(t, now, &covered))),
73 Err(why) => out.unread.push(format!("`{}`: {why}", spec.line())),
74 }
75 }
76 Ok(out)
77 }
78
79 fn covered(&self, cloud: Cloud) -> Result<BTreeSet<String>> {
81 let mut out = BTreeSet::new();
82 for record in self.store.records()?.into_iter().filter(|r| r.cloud == Some(cloud)) {
83 let path = self.store.run_dir(&record.name)?.join("terraform.tfstate");
84 match std::fs::read_to_string(&path) {
85 Ok(text) => {
86 if let Ok(state) = serde_json::from_str(&text) {
87 tags::instances_in_state(&state, &mut out);
88 }
89 }
90 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
91 Err(e) => return Err(crate::error::Error::io(&path, "read")(e)),
92 }
93 }
94 Ok(out)
95 }
96}
97
98fn orphaned(t: &Tagged, now: u64, covered: &BTreeSet<String>) -> bool {
100 t.expires_at.is_some_and(|at| at <= now) && t.instance.as_ref().is_none_or(|i| !covered.contains(i))
101}
102
103async fn query(processes: &dyn ProcessRunner, spec: &ProcessSpec) -> std::result::Result<String, String> {
105 let ran = tokio::time::timeout(QUERY_LIMIT, capture(processes, spec)).await;
106 let out = ran
107 .map_err(|_| format!("no answer within {}s", QUERY_LIMIT.as_secs()))?
108 .map_err(|e| e.to_string())?;
109 if out.exit.is_some_and(crate::process::Exit::success) {
110 Ok(out.stdout.join("\n"))
111 } else {
112 Err(format!("failed: {}", out.stderr.join(" ").trim()))
113 }
114}
115
116#[cfg(test)]
117mod tests;