1pub mod guest;
8
9use std::collections::{BTreeMap, BTreeSet};
10use std::net::IpAddr;
11use std::num::{NonZeroU16, NonZeroU32};
12
13use isoloom_manifest::{
14 Action, Arch, Argv, Check, CheckKind, Cidr, Delivery, EnvName, Error as ManifestError, Generator, GeneratorName, HostArgv, Lab, LabName, Machine, Name,
15 Network, NonEmpty, Output, OutputRole, OutputSource, Param, Part, Placeholder, Port, PortKind, PortName, Program, Protocol, Publish, PublishMode, Role,
16 Run, SetBy, SnakeName, Step, StepId, StepSelection, Template, Tool, ToolName, Version, When,
17};
18use isoloom_plan::checks::{Probe, Resolved};
19use isoloom_plan::{Environment, MachinePlan, PublishedPort};
20use isoloom_spec::model;
21
22const PER_CHECK_S: u32 = 15;
25const START_S: u32 = 60;
26
27pub fn generator() -> Result<Generator, ManifestError> {
33 Ok(Generator {
34 name: GeneratorName::Isoloom,
35 version: Version::parse(env!("CARGO_PKG_VERSION"))?,
36 })
37}
38
39pub fn lab(env: &Environment) -> Result<Lab, ManifestError> {
46 Ok(Lab {
47 name: LabName::parse(&env.name)?,
48 instance: env.instance.map(isoloom_manifest::Instance::new).transpose()?,
49 message: None,
50 })
51}
52
53pub fn program(name: &str) -> Result<Program, ManifestError> {
59 Program::parse(name)
60}
61
62pub fn argv(name: &str, args: &[&str]) -> Result<HostArgv, ManifestError> {
68 Ok(HostArgv::Every(Argv::new(program(name)?, args.iter().map(|a| Template::text(a)).collect())))
69}
70
71pub fn run(id: &str, argv: HostArgv, timeout_s: u32) -> Result<Step, ManifestError> {
77 let mut s = Step::argv(StepId::parse(id)?, argv);
78 s.timeout_s = NonZeroU32::new(timeout_s);
79 Ok(s)
80}
81
82pub fn act(id: &str, action: Action) -> Result<Step, ManifestError> {
88 Ok(Step::action(StepId::parse(id)?, action))
89}
90
91pub fn action_with<'a>(action: Action, with: impl IntoIterator<Item = (&'a str, Template)>) -> Result<Run, ManifestError> {
97 Ok(Run::Action {
98 action,
99 with: with
100 .into_iter()
101 .map(|(k, v)| Ok((SnakeName::parse(k)?, v)))
102 .collect::<Result<BTreeMap<_, _>, ManifestError>>()?,
103 })
104}
105
106pub fn derived_switch(steps: Vec<StepId>) -> Result<Option<Param>, ManifestError> {
113 let Ok(steps) = NonEmpty::new(steps, "the check steps") else {
114 return Ok(None);
115 };
116 Ok(Some(Param {
117 name: EnvName::parse(DERIVED)?,
118 description: "0: the checks derived from the spec (reachability, no internet) are skipped.".into(),
119 default: Some("1".into()),
120 values: vec!["0".into(), "1".into()],
121 required: false,
122 hosts: BTreeSet::new(),
123 providers: BTreeSet::new(),
124 set_by: SetBy::Embedder,
125 deliver: NonEmpty::one(Delivery::Env {
126 var: EnvName::parse(DERIVED)?,
127 steps: StepSelection::Only(steps),
128 }),
129 }))
130}
131
132pub const DERIVED: &str = "ISOLOOM_DERIVED";
134
135#[must_use]
137pub fn cleanup(mut s: Step) -> Step {
138 s.when = When::Always;
139 s.failure = isoloom_manifest::Failure::Tolerate;
140 s
141}
142
143#[must_use]
145pub fn tolerated(mut s: Step) -> Step {
146 s.failure = isoloom_manifest::Failure::Tolerate;
147 s
148}
149
150#[must_use]
152pub fn noted(mut s: Step, note: &str) -> Step {
153 s.note = Some(note.to_owned());
154 s
155}
156
157pub fn tool(name: &str, probe: HostArgv) -> Result<Tool, ManifestError> {
163 Ok(Tool {
164 name: ToolName::parse(name)?,
165 probe,
166 min_version: None,
167 container: None,
168 container_hosts: BTreeSet::new(),
169 hosts: BTreeSet::new(),
170 providers: BTreeSet::new(),
171 })
172}
173
174pub fn networks(env: &Environment, native: impl Fn(&str) -> Template) -> Result<Vec<Network>, ManifestError> {
180 env.networks
181 .iter()
182 .map(|(name, n)| {
183 Ok(Network {
184 name: Name::parse(name)?,
185 cidr: Cidr::parse(&n.cidr)?,
186 internet: n.internet,
187 native: Some(native(name)),
188 })
189 })
190 .collect()
191}
192
193#[must_use]
195pub fn arch(a: model::Arch) -> Arch {
196 match a {
197 model::Arch::Amd64 => Arch::Amd64,
198 model::Arch::Arm64 => Arch::Arm64,
199 }
200}
201
202fn protocol(p: model::Protocol) -> Protocol {
203 if p.is_tcp() { Protocol::Tcp } else { Protocol::Udp }
204}
205
206pub fn addresses(mp: &MachinePlan) -> Result<BTreeMap<Name, IpAddr>, ManifestError> {
212 let v4 = mp.addresses.iter().map(|(n, a)| (n, IpAddr::V4(*a)));
213 let v6 = mp
214 .addresses6
215 .iter()
216 .filter(|(n, _)| !mp.addresses.contains_key(*n))
217 .map(|(n, a)| (n, IpAddr::V6(*a)));
218 v4.chain(v6).map(|(n, a)| Ok((Name::parse(n)?, a))).collect()
219}
220
221pub fn port(label: &str, p: &model::Port, published: &[PublishedPort], address: &Template) -> Result<Port, ManifestError> {
228 let publish = published.iter().find(|x| x.name == label).map(|x| Publish {
229 mode: if x.fixed { PublishMode::Fixed } else { PublishMode::Ephemeral },
230 preferred: NonZeroU16::new(x.publish),
231 address: Some(address.clone()),
232 });
233 Ok(Port {
234 name: PortName::parse(label)?,
235 port: NonZeroU16::new(p.port).ok_or(ManifestError::OutOfRange {
236 what: "a port",
237 value: 0,
238 min: 1,
239 max: 65535,
240 })?,
241 kind: PortKind::from_parts(protocol(p.protocol), p.http, p.tls)?,
242 publish,
243 })
244}
245
246pub fn machine(name: &str, role: Role) -> Result<Machine, ManifestError> {
253 Ok(Machine {
254 name: Name::parse(name)?,
255 role,
256 of: None,
257 os: None,
258 windows: false,
259 arch: None,
260 resources: None,
261 addresses: std::collections::BTreeMap::new(),
262 keep_running: false,
263 native: None,
264 volumes: Vec::new(),
265 ports: Vec::new(),
266 })
267}
268
269pub fn terraform_output(name: &str, role: OutputRole, description: &str) -> Result<Output, ManifestError> {
275 Ok(Output {
276 name: SnakeName::parse(name)?,
277 description: Some(description.to_owned()),
278 role,
279 sensitive: false,
280 format: None,
281 from: OutputSource::TerraformOutput {
282 name: isoloom_manifest::Identifier::parse(name)?,
283 default: None,
284 },
285 })
286}
287
288pub fn outputs(machines: &[Machine]) -> Result<Vec<Output>, ManifestError> {
295 outputs_at(machines, &Template::text("127.0.0.1"))
296}
297
298pub fn outputs_at(machines: &[Machine], host: &Template) -> Result<Vec<Output>, ManifestError> {
304 outputs_with(machines, |_| host.clone())
305}
306
307pub fn outputs_on_their_hosts(machines: &[Machine]) -> Result<Vec<Output>, ManifestError> {
315 outputs_with(machines, |m| Template::from(Placeholder::Host(m.name.clone())))
316}
317
318pub fn outputs_with(machines: &[Machine], host: impl Fn(&Machine) -> Template) -> Result<Vec<Output>, ManifestError> {
324 let shown = machines.iter().filter(|m| matches!(m.role, Role::Target | Role::Access | Role::Tool));
325 shown
326 .flat_map(|m| m.ports.iter().filter(|p| p.publish.is_some()).map(move |p| (m, p)))
327 .map(|(m, p)| output(m, p, &host(m)))
328 .collect()
329}
330
331fn output(m: &Machine, p: &Port, host: &Template) -> Result<Output, ManifestError> {
332 let snake: String = format!("{}_{}", m.name, p.name)
333 .chars()
334 .map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_lowercase() } else { '_' })
335 .collect();
336 let (role, scheme, after) = match p.kind {
337 PortKind::Https => (OutputRole::Entry, "https://", "/"),
338 PortKind::Http => (OutputRole::Entry, "http://", "/"),
339 PortKind::Tcp | PortKind::Udp | PortKind::Tls => (OutputRole::Info, "", ""),
340 };
341 let port = if p.kind == PortKind::Udp {
342 format!("{}/udp", p.port)
343 } else {
344 p.port.to_string()
345 };
346 let what = format!("{}'s {} (port {port})", m.name, p.name);
347 Ok(Output {
348 name: SnakeName::parse(&snake)?,
349 description: Some(format!("{what}.")),
350 role,
351 sensitive: false,
352 format: Some(Template::from_parts(
353 std::iter::once(Part::Text(scheme.into())).chain(host.parts().iter().cloned()).chain([
354 Part::Text(":".into()),
355 Part::Placeholder(Placeholder::Value),
356 Part::Text(after.into()),
357 ]),
358 )),
359 from: OutputSource::PublishedPort {
360 machine: m.name.clone(),
361 port: p.name.clone(),
362 },
363 })
364}
365
366#[must_use]
368pub fn check(c: &Resolved) -> Check {
369 let kind = match c.probe {
370 Probe::Http { .. } => CheckKind::Http,
371 Probe::Tcp { .. } => CheckKind::Tcp,
372 Probe::Exec { .. } => CheckKind::Exec,
373 Probe::Script { .. } => CheckKind::Script,
374 Probe::Playbook { .. } => CheckKind::Playbook,
375 };
376 Check {
377 name: c.name.clone(),
378 kind,
379 wait_s: c.wait,
380 derived: c.derived,
381 }
382}
383
384#[must_use]
388pub fn timeout_s(checks: &[&Resolved]) -> u32 {
389 let waits: u32 = checks.iter().map(|c| c.wait).sum();
390 let count = u32::try_from(checks.len()).unwrap_or(u32::MAX);
391 waits.saturating_add(count.saturating_mul(PER_CHECK_S)).saturating_add(START_S)
392}
393
394#[cfg(test)]
395mod tests;