mod file;
use std::time::Duration;
use isoloom_manifest::{Condition, Role};
use crate::Result;
use crate::driver::Observed;
use crate::error::Error;
use crate::events::ReadyState;
use crate::executor::{Cx, Ran};
use crate::status::MachineState;
const TCP_RETRY: Duration = Duration::from_secs(2);
pub(crate) async fn check(cx: &mut Cx<'_>, ran: Option<&Ran>) -> Result<()> {
let lab = cx.lab;
for condition in &lab.manifest().ready {
match condition {
Condition::StepOk { step, means } => {
if let Some(ran) = ran {
step_ok(cx, ran, step.as_str(), means.as_deref())?;
}
}
Condition::MachinesRunning { machines } => {
let names: Vec<&str> = machines.iter().map(isoloom_manifest::Name::as_str).collect();
machines_running(cx, &names).await?;
}
Condition::Tcp { address, port, timeout_s } => {
let address = cx.vars.render(address, "ready.tcp.address")?;
let port = cx.vars.render(port, "ready.tcp.port")?;
tcp(cx, &address, &port, Duration::from_secs(u64::from(timeout_s.get()))).await?;
}
Condition::ReadyFile { .. } => file::wait(cx, &ready_file(cx, condition)?).await?,
}
}
Ok(())
}
fn ready_file(cx: &Cx<'_>, condition: &Condition) -> Result<file::Wait> {
let Condition::ReadyFile {
host,
user,
path,
log,
via,
timeout_s,
unreachable_timeout_s,
interval_s,
..
} = condition
else {
return Err(Error::Unsupported {
what: "a condition that isn't `ready_file` here".into(),
});
};
let render = |t: &isoloom_manifest::Template, what: &str| cx.vars.render(t, &format!("ready.ready_file.{what}"));
let host = render(host, "host")?;
let via = via.as_ref().map(|v| render(v, "via")).transpose()?.unwrap_or_default();
let secs = |n: std::num::NonZeroU32| Duration::from_secs(u64::from(n.get()));
Ok(file::Wait {
condition: format!("ready_file {host}"),
login: crate::ssh::Login::new(&render(user, "user")?, &host, cx.ssh_jump.as_deref(), &via)?,
path: render(path, "path")?,
log: log.as_ref().map(|l| render(l, "log")).transpose()?,
within: secs(*timeout_s),
unreachable: unreachable_timeout_s.map(secs),
every: secs(*interval_s),
})
}
fn step_ok(cx: &Cx<'_>, ran: &Ran, step: &str, means: Option<&str>) -> Result<()> {
let condition = format!("step_ok {step}");
let detail = means.unwrap_or_default();
if ran.ok.contains(step) {
cx.events.ready(&condition, ReadyState::Ok, detail);
return Ok(());
}
cx.events.ready(&condition, ReadyState::Failed, detail);
Err(Error::NotReady {
condition,
detail: "the step didn't succeed".into(),
})
}
async fn machines_running(cx: &mut Cx<'_>, names: &[&str]) -> Result<()> {
let lab = cx.lab;
let observed: Observed = lab.observe(cx).await?;
let down: Vec<String> = observed
.machines
.iter()
.filter(|m| names.contains(&m.name.as_str()) || (names.is_empty() && m.role == Role::Target))
.filter(|m| m.state != MachineState::Running)
.map(|m| m.name.clone())
.collect();
let condition = "machines_running";
if down.is_empty() {
cx.events.ready(condition, ReadyState::Ok, "");
return Ok(());
}
let detail = format!("not running: {}", down.join(", "));
cx.events.ready(condition, ReadyState::Failed, &detail);
Err(Error::NotReady {
condition: condition.into(),
detail,
})
}
async fn tcp(cx: &Cx<'_>, address: &str, port: &str, within: Duration) -> Result<()> {
let condition = format!("tcp {address}:{port}");
let number: u16 = port.parse().map_err(|_| Error::NotReady {
condition: condition.clone(),
detail: format!("`{port}` isn't a port"),
})?;
let deadline = tokio::time::Instant::now() + within;
loop {
let tried = tokio::time::Instant::now();
let left = deadline.saturating_duration_since(tried);
if cx.lab.runner.config.ports.reachable(address, number, left.min(TCP_RETRY)).await {
cx.events.ready(&condition, ReadyState::Ok, "");
return Ok(());
}
if tokio::time::Instant::now() >= deadline {
let detail = format!("no answer within {}s", within.as_secs());
cx.events.ready(&condition, ReadyState::Failed, &detail);
return Err(Error::NotReady { condition, detail });
}
cx.events.ready(&condition, ReadyState::Waiting, "");
tokio::time::sleep_until((tried + TCP_RETRY).min(deadline)).await;
}
}