isoloom_spec/syntax/
check.rs1mod messages;
8mod overrides;
9mod steps;
10mod values;
11
12pub use messages::{Unresolved, placeholder_problems};
13
14use super::Spec;
15use super::app::Recipe;
16use super::machine::{Kind, Machine};
17use super::problem::{Code, FieldPath, Problem, Problems};
18use super::process::{HealthProbe, PortRef};
19use super::step::has_extension;
20
21#[must_use]
23pub fn check(spec: &Spec) -> Problems {
24 let mut out = Problems::new();
25 for (name, m) in &spec.machines {
26 let at = FieldPath::new("machines").key(name);
27 what_it_is(&at, m, &mut out);
28 process(&at, m, &mut out);
29 values::machine(spec, &at, m, &mut out);
30 }
31 overrides::check(spec, &mut out);
32 messages::check(spec, &mut out);
33 let mut placed: Problems = out.into_iter().map(|p| spec.spans.place(p)).collect();
34 placed.sort_by_key(|p| p.span.map(|s| (s.line, s.column)));
35 placed
36}
37
38fn what_it_is(at: &FieldPath, m: &Machine, out: &mut Problems) {
40 let container_only = matches!(&m.kind, Kind::App(a) if a.container.is_some() && a.steps.is_empty());
41 let supplied = m.access && matches!(m.kind, Kind::Bare);
42 match &m.kind {
43 Kind::App(a) if !a.steps.is_empty() && m.os.is_none() => {
44 out.push(Problem::new(Code::MissingField, at.key("os"), "steps install onto an OS").fix("`os: debian@12`"));
45 }
46 _ if m.os.is_none() && !container_only && !supplied => {
47 out.push(Problem::new(Code::MissingField, at.key("os"), "a machine says what it is: its OS, plus a service or an app").fix("`os: debian@12`"));
48 }
49 _ => {}
50 }
51 if matches!(m.kind, Kind::Service(_)) && m.command.is_some() {
54 out.push(Problem::new(Code::Conflict, at.key("command"), "a catalogue service runs its own process").fix("leave `command` out, or use `app:`"));
55 }
56 if let Some(a) = m.app() {
57 for (i, step) in a.steps.iter().enumerate() {
58 steps::check(&at.key("app").key("steps").index(i), m, step, out);
59 }
60 if let Some(Recipe::Appliance(_)) = &a.container
61 && !a.steps.is_empty()
62 {
63 out.push(Problem::new(Code::Conflict, at.key("app").key("steps"), "an appliance has no VM form for steps to set up").fix("leave `steps` out"));
64 }
65 }
66}
67
68fn process(at: &FieldPath, m: &Machine, out: &mut Problems) {
70 let windows = m.os.is_some_and(super::os::OsRef::is_windows);
71 for (i, script) in m.setup.iter().enumerate() {
72 let ok = if windows { has_extension(script, "ps1") } else { has_extension(script, "sh") };
73 if !ok {
74 let want = if windows { "`.ps1` on Windows" } else { "a `.sh` script" };
75 out.push(Problem::new(
76 Code::InvalidValue,
77 at.key("setup").index(i),
78 format!("`{script}`: a setup script is {want}"),
79 ));
80 }
81 }
82 let service = matches!(m.kind, Kind::Service(_));
83 for (name, p) in &m.ports {
84 if p.port.is_some() && service {
85 out.push(
86 Problem::new(
87 Code::Conflict,
88 at.key("ports").key(name.as_str()).key("port"),
89 "a catalogue service's entry gives its ports' numbers",
90 )
91 .fix("leave `port` out (publish it with `publish`), and change the number through `with:`"),
92 );
93 }
94 if p.port.is_none() && !service {
95 out.push(
96 Problem::new(Code::MissingField, at.key("ports").key(name.as_str()).key("port"), "a port says its number")
97 .fix(format!("`{name}: {{ port: 8080 }}`")),
98 );
99 }
100 }
101 if let Some(HealthProbe::Http(page)) = m.healthcheck.as_ref().map(|h| &h.probe)
102 && let PortRef::Name(n) = &page.port
103 && !m.ports.contains_key(n)
104 {
105 out.push(port_reference(&at.key("healthcheck").key("http").key("port"), n.as_str(), m));
106 }
107}
108
109fn port_reference(at: &FieldPath, name: &str, m: &Machine) -> Problem {
111 let known: Vec<String> = m.ports.keys().map(|k| format!("`{k}`")).collect();
112 let p = Problem::new(Code::UnknownReference, at.clone(), format!("the machine has no port named `{name}`"));
113 if known.is_empty() {
114 p.fix("declare it under `ports:`")
115 } else {
116 p.fix(format!("its ports: {}", known.join(", ")))
117 }
118}
119
120#[cfg(test)]
121mod tests;