mod messages;
mod overrides;
mod steps;
mod values;
pub use messages::{Unresolved, placeholder_problems};
use super::Spec;
use super::app::Recipe;
use super::machine::{Kind, Machine};
use super::problem::{Code, FieldPath, Problem, Problems};
use super::process::{HealthProbe, PortRef};
use super::step::has_extension;
#[must_use]
pub fn check(spec: &Spec) -> Problems {
let mut out = Problems::new();
for (name, m) in &spec.machines {
let at = FieldPath::new("machines").key(name);
what_it_is(&at, m, &mut out);
process(&at, m, &mut out);
values::machine(spec, &at, m, &mut out);
}
overrides::check(spec, &mut out);
messages::check(spec, &mut out);
let mut placed: Problems = out.into_iter().map(|p| spec.spans.place(p)).collect();
placed.sort_by_key(|p| p.span.map(|s| (s.line, s.column)));
placed
}
fn what_it_is(at: &FieldPath, m: &Machine, out: &mut Problems) {
let container_only = matches!(&m.kind, Kind::App(a) if a.container.is_some() && a.steps.is_empty());
let supplied = m.access && matches!(m.kind, Kind::Bare);
match &m.kind {
Kind::App(a) if !a.steps.is_empty() && m.os.is_none() => {
out.push(Problem::new(Code::MissingField, at.key("os"), "steps install onto an OS").fix("`os: debian@12`"));
}
_ if m.os.is_none() && !container_only && !supplied => {
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`"));
}
_ => {}
}
if matches!(m.kind, Kind::Service(_)) && m.command.is_some() {
out.push(Problem::new(Code::Conflict, at.key("command"), "a catalogue service runs its own process").fix("leave `command` out, or use `app:`"));
}
if let Some(a) = m.app() {
for (i, step) in a.steps.iter().enumerate() {
steps::check(&at.key("app").key("steps").index(i), m, step, out);
}
if let Some(Recipe::Appliance(_)) = &a.container
&& !a.steps.is_empty()
{
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"));
}
}
}
fn process(at: &FieldPath, m: &Machine, out: &mut Problems) {
let windows = m.os.is_some_and(super::os::OsRef::is_windows);
for (i, script) in m.setup.iter().enumerate() {
let ok = if windows { has_extension(script, "ps1") } else { has_extension(script, "sh") };
if !ok {
let want = if windows { "`.ps1` on Windows" } else { "a `.sh` script" };
out.push(Problem::new(
Code::InvalidValue,
at.key("setup").index(i),
format!("`{script}`: a setup script is {want}"),
));
}
}
let service = matches!(m.kind, Kind::Service(_));
for (name, p) in &m.ports {
if p.port.is_some() && service {
out.push(
Problem::new(
Code::Conflict,
at.key("ports").key(name.as_str()).key("port"),
"a catalogue service's entry gives its ports' numbers",
)
.fix("leave `port` out (publish it with `publish`), and change the number through `with:`"),
);
}
if p.port.is_none() && !service {
out.push(
Problem::new(Code::MissingField, at.key("ports").key(name.as_str()).key("port"), "a port says its number")
.fix(format!("`{name}: {{ port: 8080 }}`")),
);
}
}
if let Some(HealthProbe::Http(page)) = m.healthcheck.as_ref().map(|h| &h.probe)
&& let PortRef::Name(n) = &page.port
&& !m.ports.contains_key(n)
{
out.push(port_reference(&at.key("healthcheck").key("http").key("port"), n.as_str(), m));
}
}
fn port_reference(at: &FieldPath, name: &str, m: &Machine) -> Problem {
let known: Vec<String> = m.ports.keys().map(|k| format!("`{k}`")).collect();
let p = Problem::new(Code::UnknownReference, at.clone(), format!("the machine has no port named `{name}`"));
if known.is_empty() {
p.fix("declare it under `ports:`")
} else {
p.fix(format!("its ports: {}", known.join(", ")))
}
}
#[cfg(test)]
mod tests;