mod delivery;
pub(crate) mod plan;
mod reconnect;
pub(crate) mod state_files;
mod step;
pub(crate) use delivery::{env_deliveries, every_delivery, step_env};
use std::collections::BTreeSet;
use std::sync::{Mutex, PoisonError};
use isoloom_manifest::{Action, Failure, Run, Step, When};
use tracing::Instrument;
pub(crate) use step::{Echo, call, call_ok, capture_step, env_of, run_argv, stdin_of};
use crate::Result;
use crate::error::Error;
use crate::events::WarningCode;
use crate::events::{Emitter, StepState};
use crate::run::Running;
use crate::scope::Scope;
use crate::secrets::Secrets;
use crate::vars::Vars;
pub(crate) struct Cx<'a> {
pub(crate) run: &'a Running<'a>,
pub(crate) vars: Vars,
pub(crate) secrets: &'a Secrets,
pub(crate) events: &'a Emitter<'a>,
pub(crate) scope: Scope,
pub(crate) processes: std::sync::Arc<dyn crate::process::ProcessRunner>,
pub(crate) stale: Stale,
pub(crate) ssh_jump: Option<String>,
pub(crate) expires_at: Option<u64>,
pub(crate) relayed: Option<crate::relay::Relayed>,
}
#[derive(Debug, Default)]
pub(crate) struct Stale {
markers: Vec<String>,
seen: Mutex<Option<String>>,
}
impl Stale {
pub(crate) fn new(markers: &[String]) -> Stale {
Stale {
markers: markers.iter().map(|m| m.to_lowercase()).filter(|m| !m.is_empty()).collect(),
seen: Mutex::new(None),
}
}
fn notice(&self, line: &str) {
let lower = line.to_lowercase();
if let Some(m) = self.markers.iter().find(|m| lower.contains(m.as_str())) {
*self.seen.lock().unwrap_or_else(PoisonError::into_inner) = Some(m.clone());
}
}
pub(crate) fn take(&self) -> Option<String> {
self.seen.lock().unwrap_or_else(PoisonError::into_inner).take()
}
}
#[derive(Debug, Default)]
pub(crate) struct Ran {
pub(crate) ok: BTreeSet<String>,
}
pub(crate) const UP: &str = "lifecycle.up";
pub(crate) async fn run_steps(cx: &mut Cx<'_>, list: &str, steps: &[Step]) -> Result<Ran> {
let mut ran = Ran::default();
let mut failure: Option<Error> = None;
for step in steps {
let id = step.id.as_str();
if !plan::applies(&cx.scope, step) {
ran.ok.insert(id.to_owned());
continue;
}
if failure.is_some() && step.when == When::IfOk {
cx.events.step(id, StepState::Skipped, None);
continue;
}
let span = tracing::info_span!("step", list, step = id);
let done = match run_step(cx, list, step).instrument(span).await {
Ok(()) if list == UP => crate::guest::deliver(cx, Some(id)).await,
other => other,
};
match done {
Ok(()) => {
ran.ok.insert(id.to_owned());
}
Err(e) if step.failure == Failure::Tolerate => {
cx.events.warning(WarningCode::StepTolerated, &e.to_string());
}
Err(e) => {
failure.get_or_insert(e);
}
}
}
failure.map_or(Ok(ran), Err)
}
async fn run_step(cx: &mut Cx<'_>, list: &str, step: &Step) -> Result<()> {
let id = step.id.as_str();
cx.events.step(id, StepState::Running, step.note.as_deref());
let mut done = Ok(());
if awaits_state(&cx.vars, step) {
let run = cx.run;
done = match run.observe(cx).await {
Ok(observed) => {
run.bind_observed(cx, observed);
Ok(())
}
Err(e) => Err(e),
};
}
if done.is_ok() {
let cx: &Cx<'_> = cx;
done = match &step.run {
Run::Argv(_) | Run::Action { action: Action::SshRun, .. } => run_argv(cx, list, step, &mut |line| cx.stale.notice(line)).await,
Run::Action {
action: Action::ProjectArchive,
with,
} => crate::archive::project(cx, step, with).await,
Run::Action { action, with } => cx.run.driver.action(cx, step, *action, with).await,
};
}
match &done {
Ok(()) => tracing::debug!("step done"),
Err(e) => tracing::debug!(error = %e, "step failed"),
}
cx.events.step(id, if done.is_ok() { StepState::Done } else { StepState::Failed }, None);
done
}
fn awaits_state(vars: &Vars, step: &Step) -> bool {
let argvs: Vec<&isoloom_manifest::Argv> = match &step.run {
Run::Argv(isoloom_manifest::HostArgv::Every(a)) => vec![a],
Run::Argv(isoloom_manifest::HostArgv::PerHost(p)) => [&p.default, &p.linux, &p.macos, &p.windows].into_iter().flatten().collect(),
Run::Action { .. } => Vec::new(),
};
let with = match &step.run {
Run::Action { with, .. } => with.values().collect(),
Run::Argv(_) => Vec::new(),
};
argvs
.into_iter()
.flat_map(|a| a.args().iter())
.chain(with)
.chain(step.env.values())
.chain(step.stdin.iter().map(isoloom_manifest::PathTemplate::template))
.flat_map(isoloom_manifest::Template::placeholders)
.any(|p| vars.awaits_state(p))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_step_naming_an_output_or_a_host_without_a_value_awaits_the_drivers_state() {
let step = |json: &str| serde_json::from_str::<Step>(json).unwrap();
let ssh = step(r#"{"id":"copy","action":"ssh.run","with":{"host":"${outputs.ip}","command":"true"}}"#);
let piped = step(r#"{"id":"open","argv":["curl","http://${host.web}/"]}"#);
let plain = step(r#"{"id":"apply","argv":["terraform","apply"],"env":{"TF_DATA_DIR":"${run.state_dir}/.terraform"}}"#);
let mut vars = Vars::default();
assert!(awaits_state(&vars, &ssh) && awaits_state(&vars, &piped));
assert!(!awaits_state(&vars, &plain));
vars.set_output("ip", "10.0.0.7".into());
vars.set_hosts(std::collections::BTreeMap::from([("web".to_owned(), "127.0.0.1".to_owned())]));
assert!(!awaits_state(&vars, &ssh) && !awaits_state(&vars, &piped));
}
#[test]
fn a_stale_marker_is_seen_in_any_line_whatever_its_case() {
let stale = Stale::new(&["verr_already_exists".into(), "is locked".into()]);
stale.notice(" web: Progress state: VBOX_E_FILE_ERROR");
assert_eq!(stale.take(), None);
stale.notice("VBoxManage: error: Details: code VERR_ALREADY_EXISTS (0x80004005)");
stale.notice("ok");
assert_eq!(stale.take().as_deref(), Some("verr_already_exists"));
assert_eq!(stale.take(), None, "taken once");
let none = Stale::new(&[]);
none.notice("anything");
assert_eq!(none.take(), None);
}
}