Skip to main content

isoloom_spec/
validate.rs

1//! Checks a spec for mistakes the generators would turn into broken files. Every problem
2//! names the field it's about (`machines.web.networks.dmz`) and says what to do, so an
3//! author (human or AI) can fix it without reading the generators.
4//!
5//! [`validate`] runs one validator per section of the spec, in a fixed order (the order of
6//! the problems is part of the output): the top level, constraints, cloud services, networks,
7//! reach rules, inputs and secrets, machines (with their settings, volumes and ports, then
8//! their own process), provisioning and the controller, tools, link impairment, the message,
9//! groups, checks and targets.
10
11mod boot;
12mod checks;
13mod cidr;
14mod cidr6;
15mod cloud;
16mod constraints;
17mod dns;
18mod files;
19mod groups;
20mod hardware;
21mod ipv6;
22mod links;
23mod machine_files;
24mod machines;
25mod networks;
26mod os;
27mod outside;
28mod ports;
29mod process;
30mod provision;
31mod reach;
32mod secrets;
33mod settings;
34mod storage;
35mod targets;
36mod tools;
37mod top;
38mod volumes;
39mod windows;
40
41#[cfg(test)]
42mod tests;
43
44use std::collections::BTreeSet;
45
46use crate::model::{Machine, Spec};
47use crate::syntax::Code;
48
49pub use cidr::{Cidr, DOCKER_BLOCK, PRIVATE, docker_cidrs};
50pub use cidr6::{Cidr6, PREFIX as IPV6_PREFIX};
51pub use files::validate_files;
52pub use settings::CONTAINER_SYSCTLS;
53
54/// One thing wrong with a lowered spec: the field's path and what to do about it. The plan
55/// places it at the field the author wrote, as a [`crate::syntax::Problem`].
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Problem {
58    /// What kind of problem: `unknown-reference` for a name that names nothing, else
59    /// `invalid-value`.
60    pub code: Code,
61    /// Where in the file, e.g. `machines.web.depends_on`.
62    pub at: String,
63    /// The reason, and the fix when there is one.
64    pub message: String,
65}
66
67impl Problem {
68    /// A problem of kind `code` at the field path `at`.
69    pub fn new(code: Code, at: impl Into<String>, message: impl Into<String>) -> Self {
70        Self {
71            code,
72            at: at.into(),
73            message: message.into(),
74        }
75    }
76
77    /// The top-level section of the spec the problem is in: `machines` for
78    /// `machines.web.depends_on[0]`, `checks` for `checks[2].http`.
79    #[must_use]
80    pub fn section(&self) -> &str {
81        self.at.split(['.', '[']).next().unwrap_or_default()
82    }
83}
84
85impl std::fmt::Display for Problem {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        write!(f, "{}: {}", self.at, self.message)
88    }
89}
90
91/// The problems found so far, in the order the validators report them.
92#[derive(Debug, Default)]
93struct Problems(Vec<Problem>);
94
95impl Problems {
96    /// A value that's wrong where it is.
97    fn add(&mut self, at: impl Into<String>, message: impl Into<String>) {
98        self.0.push(Problem::new(Code::InvalidValue, at, message));
99    }
100
101    /// A name that names nothing the spec declares.
102    fn unknown(&mut self, at: impl Into<String>, message: impl Into<String>) {
103        self.0.push(Problem::new(Code::UnknownReference, at, message));
104    }
105
106    fn is_empty(&self) -> bool {
107        self.0.is_empty()
108    }
109}
110
111/// The networks whose `cidr` parses, in declaration order, with their block.
112type Blocks = Vec<(String, Cidr)>;
113
114/// The block of network `name`, when its `cidr` parses.
115fn block(blocks: &Blocks, name: &str) -> Option<Cidr> {
116    blocks.iter().find(|(n, _)| n == name).map(|(_, c)| *c)
117}
118
119/// Checks the spec itself (no file system access). See [`validate_files`] for paths.
120#[must_use]
121pub fn validate(spec: &Spec) -> Vec<Problem> {
122    let mut p = Problems::default();
123    top::version_and_name(spec, &mut p);
124    networks::declared(spec, &mut p);
125    constraints::check(spec, &mut p);
126    cloud::check(spec, &mut p);
127    let blocks = networks::blocks(spec, &mut p);
128    networks::gateways(spec, &blocks, &mut p);
129    outside::networks(spec, &mut p);
130    reach::check(spec, &mut p);
131    outside::reach(spec, &mut p);
132    top::inputs(spec, &mut p);
133    secrets::check(spec, &mut p);
134    machines::declared(spec, &mut p);
135    machines::appliances(spec, &mut p);
136    networks::switches(spec, &mut p);
137    networks::management(spec, &mut p);
138    volumes::shared(spec, &mut p);
139    machines::each(spec, &blocks, &mut p);
140    dns::options(spec, &mut p);
141    outside::first_networks(spec, &mut p);
142    machine_files::check(spec, &mut p);
143    windows::check(spec, &mut p);
144    process::check(spec, &mut p);
145    dns::private(spec, &mut p);
146    machines::cycle(spec, &mut p);
147    boot::check(spec, &mut p);
148    provision::controller(spec, &mut p);
149    provision::steps(spec, &mut p);
150    tools::reserved(spec, &blocks, &mut p);
151    tools::each(spec, &mut p);
152    machines::external(spec, &mut p);
153    networks::tc(spec, &mut p);
154    links::check(spec, &mut p);
155    ipv6::check(spec, &mut p);
156    top::message(spec, &mut p);
157    groups::check(spec, &mut p);
158    checks::check(spec, &mut p);
159    targets::check(spec, &mut p);
160    // On Docker, the networks (moved or not) mustn't overlap either: only once the blocks are
161    // known to be valid.
162    if p.is_empty() {
163        networks::docker_overlaps(spec, &mut p);
164    }
165    p.0
166}
167
168fn kebab(s: &str) -> bool {
169    !s.is_empty()
170        && s.len() <= 63
171        && s.split('-')
172            .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()))
173}
174
175/// Machine and network names become DNS names (machines resolve each other by name).
176fn dns_label(s: &str) -> bool {
177    kebab(s) && !s.starts_with(|c: char| c.is_ascii_digit())
178}
179
180/// An environment variable's (or Ansible variable's) name: letters, digits and `_`, not
181/// starting with a digit.
182fn variable_name(s: &str) -> bool {
183    s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
184}
185
186/// The machines a machine starts after: its `depends_on`, then the gateways of its networks.
187pub fn starts_after<'a>(spec: &'a Spec, name: &str, m: &'a Machine) -> Vec<&'a str> {
188    let mut after: Vec<&str> = m.depends_on.iter().map(String::as_str).collect();
189    for net in m.networks.keys() {
190        if let Some(gw) = spec.networks.get(net).and_then(|n| n.gateway.as_deref())
191            && gw != name
192            && !after.contains(&gw)
193        {
194            after.push(gw);
195        }
196    }
197    // A shared volume's users start once its owner holds it (serves it, on the VM targets).
198    for v in m.volumes.keys() {
199        if let Some(owner) = spec.volume_owner(v)
200            && owner != name
201            && spec.machines.contains_key(owner)
202            && !after.contains(&owner)
203        {
204            after.push(owner);
205        }
206    }
207    after
208}
209
210/// The first cycle in the machines' start order (`depends_on`, gateways, shared volumes), as
211/// the names along it, the first repeated at the end.
212fn dependency_cycle(spec: &Spec) -> Option<Vec<String>> {
213    fn visit<'a>(spec: &'a Spec, n: &'a str, stack: &mut Vec<&'a str>, done: &mut BTreeSet<&'a str>) -> Option<Vec<String>> {
214        if let Some(pos) = stack.iter().position(|s| *s == n) {
215            let mut c: Vec<String> = stack.iter().skip(pos).map(|s| (*s).to_owned()).collect();
216            c.push(n.to_owned());
217            return Some(c);
218        }
219        if done.contains(n) {
220            return None;
221        }
222        stack.push(n);
223        for dep in spec.machines.get(n).map(|m| starts_after(spec, n, m)).unwrap_or_default() {
224            if spec.machines.contains_key(dep)
225                && let Some(c) = visit(spec, dep, stack, done)
226            {
227                return Some(c);
228            }
229        }
230        stack.pop();
231        done.insert(n);
232        None
233    }
234    let mut done = BTreeSet::new();
235    spec.machines.keys().find_map(|n| visit(spec, n, &mut Vec::new(), &mut done))
236}