1mod 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#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Problem {
58 pub code: Code,
61 pub at: String,
63 pub message: String,
65}
66
67impl Problem {
68 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 #[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#[derive(Debug, Default)]
93struct Problems(Vec<Problem>);
94
95impl Problems {
96 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 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
111type Blocks = Vec<(String, Cidr)>;
113
114fn block(blocks: &Blocks, name: &str) -> Option<Cidr> {
116 blocks.iter().find(|(n, _)| n == name).map(|(_, c)| *c)
117}
118
119#[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 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
175fn dns_label(s: &str) -> bool {
177 kebab(s) && !s.starts_with(|c: char| c.is_ascii_digit())
178}
179
180fn 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
186pub 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 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
210fn 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}