use std::collections::BTreeMap;
use std::time::Duration;
use isoloom_manifest::{Delivery, Transport};
use crate::Result;
use crate::error::Error;
use crate::events::StepState;
use crate::executor::{Cx, Echo, call};
use crate::secrets::SecretValue;
use crate::ssh::{self, Login};
pub(crate) const RECEIVE: &str = "set -e; umask 077; d=/var/lib/isoloom/secrets; mkdir -p \"$d\"; chmod 700 \"$d\"; \
while read -r n v; do case \"$n\" in ''|*[!A-Za-z0-9_-]*) echo \"isothrum: a secret's name is refused\" >&2; exit 2;; esac; \
printf %s \"$v\" | base64 -d > \"$d/$n.new\"; mv -f \"$d/$n.new\" \"$d/$n\"; done; touch \"$d/.delivered\"";
const PATIENCE: Duration = Duration::from_secs(900);
const RETRY: Duration = Duration::from_secs(5);
const TRY_LIMIT: Duration = Duration::from_secs(120);
const SSH_UNREACHABLE: i32 = 255;
pub(crate) async fn deliver(cx: &mut Cx<'_>, after: Option<&str>) -> Result<()> {
let by_machine = due(cx, after)?;
if by_machine.is_empty() {
return Ok(());
}
let outputs = cx.run.read_outputs(cx).await?;
let output = |name: &str| outputs.iter().find(|o| o.name == name).and_then(|o| o.value.clone());
for (machine, lines) in by_machine {
let key = machine.replace('-', "_");
let host = output(&format!("address_{key}")).ok_or_else(|| Error::Unsupported {
what: format!("a guest delivery to `{machine}` without its `address_{key}` output"),
})?;
let user = output(&format!("ssh_user_{key}"))
.or_else(|| output("ssh_user"))
.unwrap_or_else(|| "root".into());
to_machine(cx, &machine, &Login::new(&user, &host, cx.ssh_jump.as_deref(), "")?, &lines).await?;
}
Ok(())
}
fn due(cx: &Cx<'_>, after: Option<&str>) -> Result<BTreeMap<String, String>> {
let mut out: BTreeMap<String, String> = BTreeMap::new();
for slot in &cx.run.manifest().secrets {
let value = cx.secrets.get(slot.name.as_str()).map(SecretValue::expose).or(slot.default.as_deref());
for d in &slot.deliver {
let Delivery::Guest {
machine,
transport,
during_step,
} = d
else {
continue;
};
if during_step.as_ref().map(isoloom_manifest::StepId::as_str) != after {
continue;
}
if *transport != Transport::Ssh {
return Err(Error::Unsupported {
what: format!("a guest delivery to `{machine}` over {transport:?}"),
});
}
let Some(v) = value else { continue };
let encoded = crate::encoding::base64(v.as_bytes());
let lines = out.entry(machine.to_string()).or_default();
lines.push_str(slot.name.as_str());
lines.push(' ');
lines.push_str(&encoded);
lines.push('\n');
}
}
Ok(out)
}
async fn to_machine(cx: &Cx<'_>, machine: &str, login: &Login, lines: &str) -> Result<()> {
let id = format!("guest-{machine}");
cx.events.step(&id, StepState::Running, Some(&format!("Delivering {machine}'s secrets…")));
let dir = cx.run.runner.store.run_dir(cx.run.name())?;
let input = dir.join(format!("{id}.in"));
crate::store::write_private(&input, lines.as_bytes()).map_err(Error::io(&input, "written"))?;
let done = tries(cx, &id, login, &input).await;
if let Err(e) = std::fs::remove_file(&input) {
tracing::warn!(error = %e, "a delivery's input file was left in the run's state folder");
}
cx.events.step(&id, if done.is_ok() { StepState::Done } else { StepState::Failed }, None);
done
}
async fn tries(cx: &Cx<'_>, id: &str, login: &Login, input: &std::path::Path) -> Result<()> {
let deadline = tokio::time::Instant::now() + PATIENCE;
loop {
let mut spec = ssh::spec(cx, login, &format!("sudo sh -c {}", ssh::sh_quote(RECEIVE)), Vec::new())?;
spec.stdin = Some(input.to_path_buf());
let out = call(cx, id, &spec, TRY_LIMIT, Echo::Quiet).await?;
match out.exit.and_then(|e| e.code) {
Some(0) => return Ok(()),
Some(SSH_UNREACHABLE) if tokio::time::Instant::now() < deadline => tokio::time::sleep(RETRY).await,
_ => {
return Err(Error::ToolFailed {
step: id.to_owned(),
reason: cx.events.mask(&out.stderr.join("\n")),
});
}
}
}
}