shep_core/config/normalize/error.rs
1//! [`NormalizeError`], the one refusal every step of this module speaks.
2
3use core::fmt;
4
5use crate::config::KillSignal;
6use crate::paths::{HOME_DIR_VAR, SHEP_HOME_VAR};
7use crate::secrets;
8use crate::values::UpDuration;
9
10// Named by intra-doc links and by nothing rustc compiles, so the
11// import is behind `cfg(doc)` rather than flagged unused.
12#[cfg(doc)]
13use super::{normalize, normalize_all, normalize_with_home};
14#[cfg(doc)]
15use crate::config::ProbeTarget;
16
17/// Error type returned from [`normalize`] and [`normalize_all`]
18///
19/// `#[non_exhaustive]`: every config surface this crate learns to validate
20/// brings its own rejection reasons with it.
21#[non_exhaustive]
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum NormalizeError {
24 /// `name` is empty
25 MissingName,
26 /// `name` contains `/`, `\` or `:`, or is `.`/`..`. A path separator
27 /// would escape the shep home, since the name becomes a filesystem path
28 /// stem; a colon is the `name:slot` separator, and is also illegal in a
29 /// Windows filename, which a sheep name becomes part of. Carries the
30 /// name.
31 InvalidName(String),
32 /// `environment` is [`crate::secrets::ALL_ENVIRONMENTS`], the secrets
33 /// store's every-environment slot, or falls outside the grammar
34 /// [`crate::secrets`] keys and environment names share. Carries the
35 /// sheep name and the value as written.
36 InvalidEnvironment {
37 /// The sheep name, so the error names which Flockfile entry to edit.
38 name: String,
39 /// The value as the user wrote it.
40 value: String,
41 },
42 /// An app's `env` sets a variable shep injects itself. Carries the sheep
43 /// name and the variable, so the error names the entry to edit.
44 ReservedEnvVar {
45 /// The sheep name
46 name: String,
47 /// The variable the app tried to set
48 var: &'static str,
49 },
50 /// `script` is empty
51 MissingScript,
52 /// `instances` is zero
53 ZeroInstances,
54 /// `cron_restart` is not valid in croner's dialect. Carries the pattern
55 /// and the rejection reason.
56 InvalidCron {
57 /// The pattern as the user wrote it
58 pattern: String,
59 /// Why it was rejected
60 reason: String,
61 },
62 /// `cron_timezone` is not a name in the IANA time-zone database
63 InvalidTimezone {
64 /// The value as the user wrote it
65 name: String,
66 },
67 /// Two apps in one flock share this name
68 DuplicateName(String),
69 /// Two or more apps in one document depend on each other. Carries the
70 /// cycle as a path, so the refusal names it rather than only reporting
71 /// that one exists.
72 DependencyCycle(Vec<String>),
73 /// A `readiness_probe` or `liveness_probe` target is malformed. Carries
74 /// which probe and the rendered reason.
75 InvalidProbe {
76 /// `"readiness_probe"` or `"liveness_probe"`, so the error names the
77 /// line the user has to edit.
78 probe: &'static str,
79 /// [`ProbeTarget::parse`]'s rendered rejection reason.
80 reason: String,
81 },
82 /// A `readiness_probe` or `liveness_probe` has `failure_threshold == 0`.
83 ZeroFailureThreshold {
84 /// `"readiness_probe"` or `"liveness_probe"`, so the error names the
85 /// line the user has to edit.
86 probe: &'static str,
87 },
88 /// A `readiness_probe` or `liveness_probe` has an `interval` under the
89 /// floor its own loop in the daemon honours. At `0` that would spin the
90 /// loop as fast as the runtime allows; a `liveness_probe` under a full
91 /// second would instead be silently polled at that second.
92 IntervalBelowMinimum {
93 /// `"readiness_probe"` or `"liveness_probe"`, so the error names the
94 /// line the user has to edit.
95 probe: &'static str,
96 /// The value as the user wrote it.
97 value: UpDuration,
98 /// The floor it failed.
99 min: UpDuration,
100 },
101 /// `max_memory` is `0`, a ceiling every live process is already over, so
102 /// the enforcer would restart the sheep on every poll forever. Carries
103 /// the app name.
104 ZeroMaxMemory {
105 /// The sheep name, so the error names which Flockfile entry to edit.
106 name: String,
107 },
108 /// `action_timeout` is at or above `normalize`'s own ceiling: a wait no
109 /// RPC caller could ever be given enough deadline to outlast, since the
110 /// daemon clamps every deadline a caller can ask for. Carries the app
111 /// name, the value as written, and the ceiling it failed.
112 ActionTimeoutTooLong {
113 /// The sheep name, so the error names which Flockfile entry to edit.
114 name: String,
115 /// The value as the user wrote it.
116 value: UpDuration,
117 /// The ceiling it failed.
118 max: UpDuration,
119 },
120 /// `kill_signal` names a signal the daemon's stop ladder cannot send.
121 /// Carries the app name and the value as written.
122 InvalidKillSignal {
123 /// The sheep name, so the error names which Flockfile entry to edit.
124 name: String,
125 /// The value as the user wrote it.
126 value: String,
127 },
128 /// `watch` is enabled but the app sets no `cwd`, so there is no
129 /// directory to watch. Carries the app name.
130 WatchWithoutCwd {
131 /// The sheep name, so the error names which Flockfile entry to edit.
132 name: String,
133 },
134 /// A path begins `~user/`, naming another user's home.
135 ///
136 /// Refused rather than resolved: answering it means a passwd lookup, and
137 /// under a systemd unit the answer is not obviously the one anyone meant.
138 /// `~/` is supported; this is not.
139 TildeUser {
140 /// The sheep name, so the error names which Flockfile entry to edit.
141 name: String,
142 /// Which field carried it.
143 field: &'static str,
144 /// The path as written.
145 value: String,
146 },
147 /// A path begins `~/` and no home directory could be determined.
148 NoHomeForTilde {
149 /// The sheep name, so the error names which Flockfile entry to edit.
150 name: String,
151 /// Which field carried it.
152 field: &'static str,
153 },
154 /// A value carries `{{SHEP_HOME}}` and no shep home could be determined.
155 ///
156 /// The tilde's condition, one token over: nothing named a `$SHEP_HOME`
157 /// and there was no home directory to put the default under.
158 ///
159 /// No operator reaches this. The CLI refuses when neither `--home`,
160 /// `$SHEP_HOME` nor `$HOME` resolves a root, before any config is
161 /// normalised, so this guards a library caller of
162 /// [`normalize_with_home`] that supplies its own directories.
163 NoShepHome {
164 /// The sheep name, so the error names which Flockfile entry to edit.
165 name: String,
166 /// Which field carried it. Owned rather than `&'static str`, since
167 /// an `env` key and an `args` index are both spelled at runtime.
168 field: String,
169 },
170 /// `watch_delay` is `0`, which would spin the debouncer's own OS thread.
171 /// Carries the app name.
172 ZeroWatchDelay {
173 /// The sheep name, so the error names which Flockfile entry to edit.
174 name: String,
175 },
176 /// A `watch_options` or `ignore_watch` pattern is one globset will not
177 /// compile, so the watch it describes could never be armed.
178 InvalidWatchGlob {
179 /// The sheep name, so the error names which Flockfile entry to edit.
180 name: String,
181 /// `"watch_options"` or `"ignore_watch"`, so the error names which
182 /// of the two lists to edit.
183 field: &'static str,
184 /// The pattern as the user wrote it.
185 pattern: String,
186 /// globset's own rendered reason.
187 reason: String,
188 },
189 /// A `level_rules` entry shep cannot use: an empty pattern, which would
190 /// claim every line, or one the regex engine refuses. Carries the sheep
191 /// name and the rejection rendered.
192 InvalidLevelRule {
193 /// The sheep name, so the error names which Flockfile entry to edit.
194 name: String,
195 /// [`crate::config::LevelRuleError`]'s own rendering, so this variant
196 /// does not restate a grammar it does not own.
197 reason: String,
198 },
199 /// A value carries a `{{...}}` that is not a template token, or a `{{`
200 /// it never closes. Carries the sheep name, which field held it, and the
201 /// rejection rendered.
202 BadTemplate {
203 /// The sheep name
204 name: String,
205 /// Which field, for example `env.WORKER` or `args[1]`
206 field: String,
207 /// The template grammar's own error, rendered, so this
208 /// variant does not have to restate the grammar's own copy
209 reason: String,
210 },
211 /// An explicit `out_file` or `err_file` carries a `{{secret:...}}`.
212 ///
213 /// A resolved log path is not contained the way a resolved `env` value
214 /// is: it reaches `ProcessInfo`, every bus event carrying one,
215 /// `shep flock`, `shep describe`, `shep lookout`, and a filename on disk
216 /// under `$SHEP_HOME/logs`. `{{instance}}` and `{{name}}` still render
217 /// there; only a secret is refused.
218 SecretInLogPath {
219 /// The sheep name
220 name: String,
221 /// `out_file` or `err_file`
222 field: &'static str,
223 },
224 /// An explicit log path renders to the same string for two different
225 /// slots, the app runs more than one instance, and `merge_logs` is off,
226 /// so every instance would write to one file without having asked to.
227 /// A path with no `{{instance}}` is the ordinary case; a `{{name}}`-only
228 /// path and an escaped `{{{{instance}}}}` collide for the same reason.
229 SharedLogPath {
230 /// The sheep name
231 name: String,
232 /// `out_file` or `err_file`
233 field: &'static str,
234 },
235 /// An app names itself in `depends_on`. Carries the sheep name. A
236 /// one-node cycle, caught here rather than in the graph because it is
237 /// visible in a single `AppConfig`.
238 SelfDependency(String),
239 /// A `depends_on` entry names one instance rather than an app. Carries
240 /// the sheep and the offending target, so the refusal can name both.
241 InstanceDependency {
242 /// The sheep whose list holds the entry
243 sheep: String,
244 /// The entry as written
245 target: String,
246 },
247 /// A `dogs` key is the empty string. Carries the sheep name, so the
248 /// error names which Flockfile entry to edit.
249 EmptyDogName {
250 /// The sheep name
251 name: String,
252 },
253}
254
255impl fmt::Display for NormalizeError {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 match self {
258 Self::MissingName => f.write_str("app config is missing a name"),
259 Self::InvalidName(n) => {
260 write!(
261 f,
262 "sheep name `{n}` may not contain a path separator or a colon, or be `.` or `..`; use `-` in place of a colon"
263 )
264 }
265 Self::InvalidEnvironment { name, value } => write!(
266 f,
267 "sheep `{name}` has environment = `{value}`: must be 1-128 bytes of \
268 `[A-Za-z0-9._-]` not starting with `.`, and not `{}` (the secrets \
269 store's every-environment slot)",
270 secrets::ALL_ENVIRONMENTS
271 ),
272 Self::ReservedEnvVar { name, var } => {
273 // The two identity variables have a template to borrow
274 // instead; the environment is not a template token, so its
275 // advice points at the field that selects one (#284).
276 let advice = match *var {
277 "SHEP_INSTANCE" => "or `{{instance}}` in your own variable",
278 "SHEP_NAME" => "or `{{name}}` in your own variable",
279 _ => "or set `environment` on the app",
280 };
281 write!(
282 f,
283 "sheep `{name}` sets `{var}` in env, but shep injects it: use a different name, {advice}"
284 )
285 }
286 Self::MissingScript => f.write_str("app config is missing a script"),
287 Self::ZeroInstances => f.write_str("instances must be at least 1"),
288 Self::InvalidCron { pattern, reason } => {
289 write!(f, "invalid cron_restart pattern `{pattern}`: {reason}")
290 }
291 Self::InvalidTimezone { name } => {
292 write!(f, "`{name}` is not a recognized IANA timezone")
293 }
294 Self::DuplicateName(n) => write!(f, "duplicate sheep name `{n}`"),
295 Self::DependencyCycle(cycle) => write!(
296 f,
297 "dependency cycle: {}",
298 crate::config::graph::render_cycle(cycle)
299 ),
300 Self::InvalidProbe { probe, reason } => write!(f, "{probe}: {reason}"),
301 Self::ZeroFailureThreshold { probe } => {
302 write!(f, "{probe}.failure_threshold must be at least 1")
303 }
304 Self::IntervalBelowMinimum { probe, value, min } => {
305 write!(f, "{probe}.interval is `{value}`: must be at least {min}")
306 }
307 Self::ZeroMaxMemory { name } => {
308 write!(
309 f,
310 "sheep `{name}` has max_memory = 0, a limit nothing can stay under"
311 )
312 }
313 Self::ActionTimeoutTooLong { name, value, max } => {
314 write!(
315 f,
316 "sheep `{name}` has action_timeout = {value}: must be at most {max}, \
317 the longest wait any caller's deadline could ever cover"
318 )
319 }
320 Self::InvalidKillSignal { name, value } => {
321 write!(
322 f,
323 "`{name}`: kill_signal `{value}` is not one shep can send (accepted: {})",
324 KillSignal::ACCEPTED.join(", ")
325 )
326 }
327 Self::TildeUser { name, field, value } => write!(
328 f,
329 "`{name}`: {field} is `{value}`, and shep expands only `~/` (your own home). \
330 Another user's home needs a passwd lookup whose answer depends on who the \
331 daemon runs as, so write the path out in full instead."
332 ),
333 Self::NoHomeForTilde { name, field } => write!(
334 f,
335 "`{name}`: {field} begins with `~/` but no home directory could be found. \
336 Set {HOME_DIR_VAR}, or write the path out in full."
337 ),
338 Self::NoShepHome { name, field } => write!(
339 f,
340 "`{name}`: {field} carries `{{{{SHEP_HOME}}}}` but no shep home could be \
341 found. Set {SHEP_HOME_VAR}, or write the path out in full."
342 ),
343 Self::WatchWithoutCwd { name } => {
344 write!(f, "sheep `{name}` has watch = true but no cwd to watch")
345 }
346 Self::ZeroWatchDelay { name } => {
347 write!(
348 f,
349 "sheep `{name}` has watch_delay = 0: must be greater than 0"
350 )
351 }
352 Self::InvalidWatchGlob {
353 name,
354 field,
355 pattern,
356 reason,
357 } => write!(
358 f,
359 "sheep `{name}` has an invalid {field} pattern `{pattern}`: {reason}"
360 ),
361 Self::InvalidLevelRule { name, reason } => {
362 write!(f, "sheep `{name}` has an invalid level rule: {reason}")
363 }
364 Self::BadTemplate {
365 name,
366 field,
367 reason,
368 } => write!(f, "sheep `{name}`, {field}: {reason}"),
369 Self::SecretInLogPath { name, field } => write!(
370 f,
371 "sheep `{name}` puts a `{{{{secret:...}}}}` in `{field}`: a log path may not hold a secret, since it becomes a filename on disk and is reported by `shep flock` and `shep describe`"
372 ),
373 Self::SharedLogPath { name, field } => write!(
374 f,
375 "sheep `{name}` runs several instances and sets `{field}` to one path: put `{{{{instance}}}}` in it, or set `merge_logs = true` to share it on purpose"
376 ),
377 Self::SelfDependency(n) => {
378 write!(f, "`{n}` names itself in depends_on")
379 }
380 Self::InstanceDependency { sheep, target } => {
381 let app = target.split(':').next().unwrap_or(target);
382 write!(
383 f,
384 "`{sheep}` depends on `{target}`, which names one instance. \
385 Depend on `{app}` instead: a dependency waits for every \
386 instance of an app"
387 )
388 }
389 Self::EmptyDogName { name } => {
390 write!(f, "sheep `{name}` has a dogs entry with an empty name")
391 }
392 }
393 }
394}
395
396impl core::error::Error for NormalizeError {}