pub mod guest;
use std::collections::{BTreeMap, BTreeSet};
use std::net::IpAddr;
use std::num::{NonZeroU16, NonZeroU32};
use isoloom_manifest::{
Action, Arch, Argv, Check, CheckKind, Cidr, Delivery, EnvName, Error as ManifestError, Generator, GeneratorName, HostArgv, Lab, LabName, Machine, Name,
Network, NonEmpty, Output, OutputRole, OutputSource, Param, Part, Placeholder, Port, PortKind, PortName, Program, Protocol, Publish, PublishMode, Role,
Run, SetBy, SnakeName, Step, StepId, StepSelection, Template, Tool, ToolName, Version, When,
};
use isoloom_plan::checks::{Probe, Resolved};
use isoloom_plan::{Environment, MachinePlan, PublishedPort};
use isoloom_spec::model;
const PER_CHECK_S: u32 = 15;
const START_S: u32 = 60;
pub fn generator() -> Result<Generator, ManifestError> {
Ok(Generator {
name: GeneratorName::Isoloom,
version: Version::parse(env!("CARGO_PKG_VERSION"))?,
})
}
pub fn lab(env: &Environment) -> Result<Lab, ManifestError> {
Ok(Lab {
name: LabName::parse(&env.name)?,
instance: env.instance.map(isoloom_manifest::Instance::new).transpose()?,
message: None,
})
}
pub fn program(name: &str) -> Result<Program, ManifestError> {
Program::parse(name)
}
pub fn argv(name: &str, args: &[&str]) -> Result<HostArgv, ManifestError> {
Ok(HostArgv::Every(Argv::new(program(name)?, args.iter().map(|a| Template::text(a)).collect())))
}
pub fn run(id: &str, argv: HostArgv, timeout_s: u32) -> Result<Step, ManifestError> {
let mut s = Step::argv(StepId::parse(id)?, argv);
s.timeout_s = NonZeroU32::new(timeout_s);
Ok(s)
}
pub fn act(id: &str, action: Action) -> Result<Step, ManifestError> {
Ok(Step::action(StepId::parse(id)?, action))
}
pub fn action_with<'a>(action: Action, with: impl IntoIterator<Item = (&'a str, Template)>) -> Result<Run, ManifestError> {
Ok(Run::Action {
action,
with: with
.into_iter()
.map(|(k, v)| Ok((SnakeName::parse(k)?, v)))
.collect::<Result<BTreeMap<_, _>, ManifestError>>()?,
})
}
pub fn derived_switch(steps: Vec<StepId>) -> Result<Option<Param>, ManifestError> {
let Ok(steps) = NonEmpty::new(steps, "the check steps") else {
return Ok(None);
};
Ok(Some(Param {
name: EnvName::parse(DERIVED)?,
description: "0: the checks derived from the spec (reachability, no internet) are skipped.".into(),
default: Some("1".into()),
values: vec!["0".into(), "1".into()],
required: false,
hosts: BTreeSet::new(),
providers: BTreeSet::new(),
set_by: SetBy::Embedder,
deliver: NonEmpty::one(Delivery::Env {
var: EnvName::parse(DERIVED)?,
steps: StepSelection::Only(steps),
}),
}))
}
pub const DERIVED: &str = "ISOLOOM_DERIVED";
#[must_use]
pub fn cleanup(mut s: Step) -> Step {
s.when = When::Always;
s.failure = isoloom_manifest::Failure::Tolerate;
s
}
#[must_use]
pub fn tolerated(mut s: Step) -> Step {
s.failure = isoloom_manifest::Failure::Tolerate;
s
}
#[must_use]
pub fn noted(mut s: Step, note: &str) -> Step {
s.note = Some(note.to_owned());
s
}
pub fn tool(name: &str, probe: HostArgv) -> Result<Tool, ManifestError> {
Ok(Tool {
name: ToolName::parse(name)?,
probe,
min_version: None,
container: None,
container_hosts: BTreeSet::new(),
hosts: BTreeSet::new(),
providers: BTreeSet::new(),
})
}
pub fn networks(env: &Environment, native: impl Fn(&str) -> Template) -> Result<Vec<Network>, ManifestError> {
env.networks
.iter()
.map(|(name, n)| {
Ok(Network {
name: Name::parse(name)?,
cidr: Cidr::parse(&n.cidr)?,
internet: n.internet,
native: Some(native(name)),
})
})
.collect()
}
#[must_use]
pub fn arch(a: model::Arch) -> Arch {
match a {
model::Arch::Amd64 => Arch::Amd64,
model::Arch::Arm64 => Arch::Arm64,
}
}
fn protocol(p: model::Protocol) -> Protocol {
if p.is_tcp() { Protocol::Tcp } else { Protocol::Udp }
}
pub fn addresses(mp: &MachinePlan) -> Result<BTreeMap<Name, IpAddr>, ManifestError> {
let v4 = mp.addresses.iter().map(|(n, a)| (n, IpAddr::V4(*a)));
let v6 = mp
.addresses6
.iter()
.filter(|(n, _)| !mp.addresses.contains_key(*n))
.map(|(n, a)| (n, IpAddr::V6(*a)));
v4.chain(v6).map(|(n, a)| Ok((Name::parse(n)?, a))).collect()
}
pub fn port(label: &str, p: &model::Port, published: &[PublishedPort], address: &Template) -> Result<Port, ManifestError> {
let publish = published.iter().find(|x| x.name == label).map(|x| Publish {
mode: if x.fixed { PublishMode::Fixed } else { PublishMode::Ephemeral },
preferred: NonZeroU16::new(x.publish),
address: Some(address.clone()),
});
Ok(Port {
name: PortName::parse(label)?,
port: NonZeroU16::new(p.port).ok_or(ManifestError::OutOfRange {
what: "a port",
value: 0,
min: 1,
max: 65535,
})?,
kind: PortKind::from_parts(protocol(p.protocol), p.http, p.tls)?,
publish,
})
}
pub fn machine(name: &str, role: Role) -> Result<Machine, ManifestError> {
Ok(Machine {
name: Name::parse(name)?,
role,
of: None,
os: None,
windows: false,
arch: None,
resources: None,
addresses: std::collections::BTreeMap::new(),
keep_running: false,
native: None,
volumes: Vec::new(),
ports: Vec::new(),
})
}
pub fn terraform_output(name: &str, role: OutputRole, description: &str) -> Result<Output, ManifestError> {
Ok(Output {
name: SnakeName::parse(name)?,
description: Some(description.to_owned()),
role,
sensitive: false,
format: None,
from: OutputSource::TerraformOutput {
name: isoloom_manifest::Identifier::parse(name)?,
default: None,
},
})
}
pub fn outputs(machines: &[Machine]) -> Result<Vec<Output>, ManifestError> {
outputs_at(machines, &Template::text("127.0.0.1"))
}
pub fn outputs_at(machines: &[Machine], host: &Template) -> Result<Vec<Output>, ManifestError> {
outputs_with(machines, |_| host.clone())
}
pub fn outputs_on_their_hosts(machines: &[Machine]) -> Result<Vec<Output>, ManifestError> {
outputs_with(machines, |m| Template::from(Placeholder::Host(m.name.clone())))
}
pub fn outputs_with(machines: &[Machine], host: impl Fn(&Machine) -> Template) -> Result<Vec<Output>, ManifestError> {
let shown = machines.iter().filter(|m| matches!(m.role, Role::Target | Role::Access | Role::Tool));
shown
.flat_map(|m| m.ports.iter().filter(|p| p.publish.is_some()).map(move |p| (m, p)))
.map(|(m, p)| output(m, p, &host(m)))
.collect()
}
fn output(m: &Machine, p: &Port, host: &Template) -> Result<Output, ManifestError> {
let snake: String = format!("{}_{}", m.name, p.name)
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_lowercase() } else { '_' })
.collect();
let (role, scheme, after) = match p.kind {
PortKind::Https => (OutputRole::Entry, "https://", "/"),
PortKind::Http => (OutputRole::Entry, "http://", "/"),
PortKind::Tcp | PortKind::Udp | PortKind::Tls => (OutputRole::Info, "", ""),
};
let port = if p.kind == PortKind::Udp {
format!("{}/udp", p.port)
} else {
p.port.to_string()
};
let what = format!("{}'s {} (port {port})", m.name, p.name);
Ok(Output {
name: SnakeName::parse(&snake)?,
description: Some(format!("{what}.")),
role,
sensitive: false,
format: Some(Template::from_parts(
std::iter::once(Part::Text(scheme.into())).chain(host.parts().iter().cloned()).chain([
Part::Text(":".into()),
Part::Placeholder(Placeholder::Value),
Part::Text(after.into()),
]),
)),
from: OutputSource::PublishedPort {
machine: m.name.clone(),
port: p.name.clone(),
},
})
}
#[must_use]
pub fn check(c: &Resolved) -> Check {
let kind = match c.probe {
Probe::Http { .. } => CheckKind::Http,
Probe::Tcp { .. } => CheckKind::Tcp,
Probe::Exec { .. } => CheckKind::Exec,
Probe::Script { .. } => CheckKind::Script,
Probe::Playbook { .. } => CheckKind::Playbook,
};
Check {
name: c.name.clone(),
kind,
wait_s: c.wait,
derived: c.derived,
}
}
#[must_use]
pub fn timeout_s(checks: &[&Resolved]) -> u32 {
let waits: u32 = checks.iter().map(|c| c.wait).sum();
let count = u32::try_from(checks.len()).unwrap_or(u32::MAX);
waits.saturating_add(count.saturating_mul(PER_CHECK_S)).saturating_add(START_S)
}
#[cfg(test)]
mod tests;