Skip to main content

isoloom_plan/lab/
steps.rs

1//! What a lab's steps say beyond the spec's own checks: the project files they name (each
2//! `copy`'s source, each `template` and its placeholders), and the daemons a recipe installs
3//! that no step says where to run (a warning).
4
5use 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    /// What the spec says that is likely not what its author means, in file order: a daemon a
16    /// recipe installs with no `service` step for it (it would start on Debian VMs only). Never
17    /// a problem: validation passes with warnings.
18    #[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
63/// The project files the steps name, each present in `dir`, and each template's placeholders
64/// resolving against the spec.
65pub(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
87/// The project files and folders the catalogue services' settings name (`with: { root: site/
88/// }`), each present in `dir`.
89pub(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
105/// A project file the spec names that isn't there.
106fn missing(at: FieldPath, path: &str) -> Problem {
107    Problem::new(Code::InvalidValue, at, format!("`{path}` doesn't exist in the project folder"))
108}
109
110/// Template `file` (`from`, as written) of machine `machine`: present, text, and each of its
111/// placeholders one a template may name and the spec has.
112fn 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    // One problem per template, every placeholder in it: a field holds one problem.
124    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;