isoloom_plan/lab/
steps.rs1use std::path::Path;
6
7use isoloom_spec::model::Seal;
8use isoloom_spec::syntax::placeholder::Place;
9use isoloom_spec::syntax::{self, Code, FieldPath, Problem, Step, Warning};
10
11use super::Lab;
12use crate::configure::{DaemonTable, unstarted};
13
14impl Lab {
15 #[must_use]
19 pub fn warnings(&self) -> Vec<Warning> {
20 let daemons = DaemonTable::common();
21 let mut out: Vec<Warning> = Vec::new();
22 for (name, m) in &self.source.machines {
23 let Some(os) = m.os else { continue };
24 let base = self
25 .source
26 .clones
27 .iter()
28 .find(|(_, c)| c.contains(name))
29 .map_or(name.as_str(), |(b, _)| b.as_str());
30 for u in unstarted(os, m, &daemons) {
31 let path = FieldPath::new("machines").key(base).key("app").key("steps").index(u.step);
32 if out.iter().any(|w| w.path == path && w.reason.starts_with(&format!("`{}`", u.package))) {
33 continue;
34 }
35 let mut w = Warning::new(
36 path,
37 format!(
38 "`{}` installs a daemon that starts on Debian VMs only (their packages start it): nothing starts it in a container or on another family's VM",
39 u.package
40 ),
41 )
42 .fix(format!("`service: {unit}`, or `{{ service: {{ name: {unit}, enabled: false }} }}`", unit = u.unit));
43 w.span = self.source.spans.find(&w.path);
44 out.push(w);
45 }
46 }
47 for (name, m) in &self.source.machines {
48 if m.seal != Seal::BestEffort {
49 continue;
50 }
51 let mut w = Warning::new(
52 FieldPath::new("machines").key(name).key("seal"),
53 "`seal: best-effort` makes this machine's `internet: false` seal advisory: it is applied, but a privileged container can lift it from inside, so treat the machine as able to reach the internet",
54 );
55 w.span = self.source.spans.find(&w.path);
56 out.push(w);
57 }
58 out.sort_by_key(|w| w.span.map(|s| (s.line, s.column)));
59 out
60 }
61}
62
63pub(super) fn step_files(source: &syntax::Spec, dir: &Path) -> Vec<Problem> {
66 let mut out = Vec::new();
67 for (name, m) in &source.machines {
68 let Some(app) = m.app() else { continue };
69 for (i, step) in app.steps.iter().enumerate() {
70 let at = FieldPath::new("machines").key(name).key("app").key("steps").index(i);
71 match step {
72 Step::Copy(c) if !dir.join(c.from.as_str()).exists() => out.push(missing(at.key("copy").key("from"), c.from.as_str())),
73 Step::Template(t) => out.extend(template(
74 source,
75 name,
76 &at.key("template").key("from"),
77 &dir.join(t.from.as_str()),
78 t.from.as_str(),
79 )),
80 _ => {}
81 }
82 }
83 }
84 out
85}
86
87pub(super) fn setting_files(source: &syntax::Spec, catalogue: &isoloom_catalogue::Catalogue, dir: &Path) -> Vec<Problem> {
90 let mut out = Vec::new();
91 for name in source.machines.keys() {
92 let Ok(resolved) = catalogue.resolve(source, name) else { continue };
93 for (setting, value) in resolved.iter().flat_map(|r| r.settings.iter()) {
94 let (isoloom_catalogue::SettingValue::Folder(path) | isoloom_catalogue::SettingValue::File(path)) = value else {
95 continue;
96 };
97 if !dir.join(path.as_str()).exists() {
98 out.push(missing(FieldPath::new("machines").key(name).key("with").key(setting.as_str()), path.as_str()));
99 }
100 }
101 }
102 out
103}
104
105fn missing(at: FieldPath, path: &str) -> Problem {
107 Problem::new(Code::InvalidValue, at, format!("`{path}` doesn't exist in the project folder"))
108}
109
110fn template(source: &syntax::Spec, machine: &str, at: &FieldPath, file: &Path, from: &str) -> Vec<Problem> {
113 if !file.exists() {
114 return vec![missing(at.clone(), from)];
115 }
116 let Ok(text) = std::fs::read_to_string(file) else {
117 return vec![Problem::new(
118 Code::InvalidValue,
119 at.clone(),
120 format!("`{from}` isn't a text file: a template is filled in as text"),
121 )];
122 };
123 let found = syntax::placeholder_problems(source, Some(machine), &text, Place::Template);
125 let Some(first) = found.first() else { return Vec::new() };
126 let code = if first.unclosed { Code::InvalidValue } else { Code::UnknownReference };
127 let reasons: Vec<&str> = found.iter().map(|u| u.reason.as_str()).collect();
128 vec![
129 Problem::new(code, at.clone(), format!("`{from}`: {}", reasons.join("; ")))
130 .fix("a template names `machine.name`, `machine.addresses.<network>`, `machines.<m>.addresses.<network>` and `machines.<m>.ports.<name>.port`"),
131 ]
132}
133
134#[cfg(test)]
135mod tests;