1pub use isoloom_spec::probe::{Host, Url, can_run_checks};
19
20mod derived;
21mod runner;
22
23pub use derived::{allowed, derived, from_networks};
24pub use runner::{Line, Render, parse_line, script};
25
26use isoloom_spec::model::{Check, Declared, Expect, Spec};
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub enum Position {
31 Machine(String),
33 Networks,
35}
36
37impl Position {
38 #[must_use]
40 pub fn id(&self) -> &str {
41 match self {
42 Position::Machine(m) => m,
43 Position::Networks => "networks",
44 }
45 }
46
47 #[must_use]
49 pub fn label(&self) -> String {
50 match self {
51 Position::Machine(m) => format!("from {m}"),
52 Position::Networks => "from the environment's networks".to_owned(),
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum HttpExpect {
60 Status(u16),
62 Any,
64 Blocked,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum TcpExpect {
71 Open,
73 Blocked,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Default)]
79pub struct HttpRequest {
80 pub method: Option<String>,
82 pub headers: Vec<(String, String)>,
84 pub body: Option<String>,
86 pub contains: Option<String>,
88 pub follow: bool,
90}
91
92impl HttpRequest {
93 fn is_plain(&self) -> bool {
95 self.method.is_none() && self.headers.is_empty() && self.body.is_none() && self.contains.is_none()
96 }
97}
98
99pub const HTTP_MAX_REDIRECTS: u32 = 5;
102
103#[derive(Debug, Clone, PartialEq, Eq, Default)]
105pub struct TcpExchange {
106 pub send: Option<String>,
108 pub contains: Option<String>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum Probe {
115 Http {
117 url: Url,
119 expect: HttpExpect,
121 request: HttpRequest,
123 },
124 Tcp {
126 host: Host,
128 port: u16,
130 expect: TcpExpect,
132 exchange: TcpExchange,
134 },
135 Exec {
137 command: String,
139 expect: Option<String>,
141 },
142 Script {
144 path: String,
146 },
147 Playbook {
149 path: String,
151 },
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct Resolved {
157 pub name: String,
159 pub position: Position,
161 pub probe: Probe,
163 pub wait: u32,
165 pub derived: bool,
167}
168
169#[must_use]
171pub fn default_position(spec: &Spec) -> Position {
172 match spec.machines.iter().find(|(_, m)| m.access) {
173 Some((name, _)) => Position::Machine(name.clone()),
174 None => Position::Networks,
175 }
176}
177
178#[must_use]
181pub fn plan(spec: &Spec) -> Vec<Resolved> {
182 let default = default_position(spec);
183 let mut out: Vec<Resolved> = spec
184 .checks
185 .iter()
186 .filter_map(|c| match c {
187 Check::Script(p) => Some(Resolved {
188 name: p.clone(),
189 position: default.clone(),
190 probe: if c.is_playbook() {
191 Probe::Playbook { path: p.clone() }
192 } else {
193 Probe::Script { path: p.clone() }
194 },
195 wait: 0,
196 derived: false,
197 }),
198 Check::Declared(d) => declared(d, &default),
199 })
200 .collect();
201 out.extend(derived(spec));
202 out
203}
204
205fn declared(d: &Declared, default: &Position) -> Option<Resolved> {
206 let position = d.from.as_ref().map_or_else(|| default.clone(), |f| Position::Machine(f.clone()));
207 let (probe, default_wait, what) = if let Some(u) = &d.http {
208 declared_http(d, u)?
209 } else if let Some(t) = &d.tcp {
210 declared_tcp(d, t)?
211 } else if let Some(c) = &d.exec {
212 let expect = match &d.expect {
213 None => None,
214 Some(Expect::Text(t)) => Some(t.clone()),
215 Some(Expect::Status(_)) => return None,
216 };
217 (Probe::Exec { command: c.clone(), expect }, 0, c.clone())
218 } else {
219 let p = d.script.as_ref()?;
220 (Probe::Script { path: p.clone() }, 0, p.clone())
221 };
222 Some(Resolved {
223 name: d.name.clone().unwrap_or_else(|| match &position {
224 Position::Machine(m) => format!("{what} from {m}"),
225 Position::Networks => what,
226 }),
227 position,
228 probe,
229 wait: d.wait.unwrap_or(default_wait),
230 derived: false,
231 })
232}
233
234type DeclaredProbe = (Probe, u32, String);
236
237fn declared_http(d: &Declared, u: &str) -> Option<DeclaredProbe> {
239 let url = Url::parse(u)?;
240 let expect = match &d.expect {
241 None => HttpExpect::Status(200),
242 Some(Expect::Status(s)) => HttpExpect::Status(*s),
243 Some(Expect::Text(t)) if t == "any" => HttpExpect::Any,
244 Some(Expect::Text(t)) if t == "blocked" => HttpExpect::Blocked,
245 Some(Expect::Text(_)) => return None,
246 };
247 let wait = if expect == HttpExpect::Blocked { 0 } else { 30 };
248 let request = HttpRequest {
249 method: d.method.clone(),
250 headers: d.headers.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
251 body: d.body.clone(),
252 contains: d.contains.clone(),
253 follow: d.follow,
254 };
255 let what = match (&expect, &request.method) {
256 (HttpExpect::Blocked, _) => format!("{u} blocked"),
257 (_, Some(m)) => format!("{} {u}", m.to_uppercase()),
258 _ => u.to_owned(),
259 };
260 Some((Probe::Http { url, expect, request }, wait, what))
261}
262
263fn declared_tcp(d: &Declared, target: &str) -> Option<DeclaredProbe> {
265 let (host, port) = target.rsplit_once(':')?;
266 let port: u16 = port.parse().ok().filter(|p| *p > 0)?;
267 let expect = match d.expect.as_ref() {
268 None => TcpExpect::Open,
269 Some(Expect::Text(t)) if t == "open" => TcpExpect::Open,
270 Some(Expect::Text(t)) if t == "blocked" => TcpExpect::Blocked,
271 _ => return None,
272 };
273 let wait = if expect == TcpExpect::Blocked { 0 } else { 30 };
274 let what = match expect {
275 TcpExpect::Blocked => format!("{target} blocked"),
276 TcpExpect::Open => target.to_owned(),
277 };
278 let exchange = TcpExchange {
279 send: d.send.clone(),
280 contains: d.contains.clone(),
281 };
282 let probe = Probe::Tcp {
283 host: Host::Literal(host.to_owned()),
284 port,
285 expect,
286 exchange,
287 };
288 Some((probe, wait, what))
289}
290
291#[must_use]
293pub fn by_position<'a>(spec: &Spec, checks: &'a [Resolved]) -> Vec<(Position, Vec<&'a Resolved>)> {
294 let mut order: Vec<Position> = vec![default_position(spec)];
295 for m in spec.machines.keys() {
296 let p = Position::Machine(m.clone());
297 if !order.contains(&p) {
298 order.push(p);
299 }
300 }
301 if !order.contains(&Position::Networks) {
302 order.push(Position::Networks);
303 }
304 order
305 .into_iter()
306 .filter_map(|p| {
307 let group: Vec<&Resolved> = checks.iter().filter(|c| c.position == p).collect();
308 (!group.is_empty()).then_some((p, group))
309 })
310 .collect()
311}
312
313#[cfg(test)]
314mod tests;