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, EnvironmentName, EnvironmentRef, Error as ManifestError, Generator, GeneratorName, HostArgv,
15 Machine, Name, Network, NonEmpty, Output, OutputRole, OutputSource, Param, Part, Placeholder, Port, PortKind, PortName, Program, Protocol, Publish,
16 PublishMode, Role, 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 environment(env: &Environment) -> Result<EnvironmentRef, ManifestError> {
46 Ok(EnvironmentRef {
47 name: EnvironmentName::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 on_demand: false,
264 native: None,
265 volumes: Vec::new(),
266 ports: Vec::new(),
267 })
268}
269
270pub fn terraform_output(name: &str, role: OutputRole, description: &str) -> Result<Output, ManifestError> {
276 Ok(Output {
277 name: SnakeName::parse(name)?,
278 description: Some(description.to_owned()),
279 role,
280 sensitive: false,
281 format: None,
282 from: OutputSource::TerraformOutput {
283 name: isoloom_manifest::Identifier::parse(name)?,
284 default: None,
285 },
286 })
287}
288
289pub fn outputs(machines: &[Machine]) -> Result<Vec<Output>, ManifestError> {
296 outputs_at(machines, &Template::text("127.0.0.1"))
297}
298
299pub fn outputs_at(machines: &[Machine], host: &Template) -> Result<Vec<Output>, ManifestError> {
305 outputs_with(machines, |_| host.clone())
306}
307
308pub fn outputs_on_their_hosts(machines: &[Machine]) -> Result<Vec<Output>, ManifestError> {
316 outputs_with(machines, |m| Template::from(Placeholder::Host(m.name.clone())))
317}
318
319pub fn outputs_with(machines: &[Machine], host: impl Fn(&Machine) -> Template) -> Result<Vec<Output>, ManifestError> {
325 let shown = machines.iter().filter(|m| matches!(m.role, Role::Target | Role::Access | Role::Tool));
326 shown
327 .flat_map(|m| m.ports.iter().filter(|p| p.publish.is_some()).map(move |p| (m, p)))
328 .map(|(m, p)| output(m, p, &host(m)))
329 .collect()
330}
331
332fn output(m: &Machine, p: &Port, host: &Template) -> Result<Output, ManifestError> {
333 let snake: String = format!("{}_{}", m.name, p.name)
334 .chars()
335 .map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_lowercase() } else { '_' })
336 .collect();
337 let (role, scheme, after) = match p.kind {
338 PortKind::Https => (OutputRole::Entry, "https://", "/"),
339 PortKind::Http => (OutputRole::Entry, "http://", "/"),
340 PortKind::Tcp | PortKind::Udp | PortKind::Tls => (OutputRole::Info, "", ""),
341 };
342 let port = if p.kind == PortKind::Udp {
343 format!("{}/udp", p.port)
344 } else {
345 p.port.to_string()
346 };
347 let what = format!("{}'s {} (port {port})", m.name, p.name);
348 Ok(Output {
349 name: SnakeName::parse(&snake)?,
350 description: Some(format!("{what}.")),
351 role,
352 sensitive: false,
353 format: Some(Template::from_parts(
354 std::iter::once(Part::Text(scheme.into())).chain(host.parts().iter().cloned()).chain([
355 Part::Text(":".into()),
356 Part::Placeholder(Placeholder::Value),
357 Part::Text(after.into()),
358 ]),
359 )),
360 from: OutputSource::PublishedPort {
361 machine: m.name.clone(),
362 port: p.name.clone(),
363 },
364 })
365}
366
367#[must_use]
369pub fn check(c: &Resolved) -> Check {
370 let kind = match c.probe {
371 Probe::Http { .. } => CheckKind::Http,
372 Probe::Tcp { .. } => CheckKind::Tcp,
373 Probe::Exec { .. } => CheckKind::Exec,
374 Probe::Script { .. } => CheckKind::Script,
375 Probe::Playbook { .. } => CheckKind::Playbook,
376 };
377 Check {
378 name: c.name.clone(),
379 kind,
380 wait_s: c.wait,
381 derived: c.derived,
382 }
383}
384
385#[must_use]
389pub fn timeout_s(checks: &[&Resolved]) -> u32 {
390 let waits: u32 = checks.iter().map(|c| c.wait).sum();
391 let count = u32::try_from(checks.len()).unwrap_or(u32::MAX);
392 waits.saturating_add(count.saturating_mul(PER_CHECK_S)).saturating_add(START_S)
393}
394
395#[cfg(test)]
396mod tests;