Skip to main content

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}
248
249impl fmt::Display for NormalizeError {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        match self {
252            Self::MissingName => f.write_str("app config is missing a name"),
253            Self::InvalidName(n) => {
254                write!(
255                    f,
256                    "sheep name `{n}` may not contain a path separator or a colon, or be `.` or `..`; use `-` in place of a colon"
257                )
258            }
259            Self::InvalidEnvironment { name, value } => write!(
260                f,
261                "sheep `{name}` has environment = `{value}`: must be 1-128 bytes of \
262                 `[A-Za-z0-9._-]` not starting with `.`, and not `{}` (the secrets \
263                 store's every-environment slot)",
264                secrets::ALL_ENVIRONMENTS
265            ),
266            Self::ReservedEnvVar { name, var } => {
267                // The two identity variables have a template to borrow
268                // instead; the environment is not a template token, so its
269                // advice points at the field that selects one (#284).
270                let advice = match *var {
271                    "SHEP_INSTANCE" => "or `{{instance}}` in your own variable",
272                    "SHEP_NAME" => "or `{{name}}` in your own variable",
273                    _ => "or set `environment` on the app",
274                };
275                write!(
276                    f,
277                    "sheep `{name}` sets `{var}` in env, but shep injects it: use a different name, {advice}"
278                )
279            }
280            Self::MissingScript => f.write_str("app config is missing a script"),
281            Self::ZeroInstances => f.write_str("instances must be at least 1"),
282            Self::InvalidCron { pattern, reason } => {
283                write!(f, "invalid cron_restart pattern `{pattern}`: {reason}")
284            }
285            Self::InvalidTimezone { name } => {
286                write!(f, "`{name}` is not a recognized IANA timezone")
287            }
288            Self::DuplicateName(n) => write!(f, "duplicate sheep name `{n}`"),
289            Self::DependencyCycle(cycle) => write!(
290                f,
291                "dependency cycle: {}",
292                crate::config::graph::render_cycle(cycle)
293            ),
294            Self::InvalidProbe { probe, reason } => write!(f, "{probe}: {reason}"),
295            Self::ZeroFailureThreshold { probe } => {
296                write!(f, "{probe}.failure_threshold must be at least 1")
297            }
298            Self::IntervalBelowMinimum { probe, value, min } => {
299                write!(f, "{probe}.interval is `{value}`: must be at least {min}")
300            }
301            Self::ZeroMaxMemory { name } => {
302                write!(
303                    f,
304                    "sheep `{name}` has max_memory = 0, a limit nothing can stay under"
305                )
306            }
307            Self::ActionTimeoutTooLong { name, value, max } => {
308                write!(
309                    f,
310                    "sheep `{name}` has action_timeout = {value}: must be at most {max}, \
311                     the longest wait any caller's deadline could ever cover"
312                )
313            }
314            Self::InvalidKillSignal { name, value } => {
315                write!(
316                    f,
317                    "`{name}`: kill_signal `{value}` is not one shep can send (accepted: {})",
318                    KillSignal::ACCEPTED.join(", ")
319                )
320            }
321            Self::TildeUser { name, field, value } => write!(
322                f,
323                "`{name}`: {field} is `{value}`, and shep expands only `~/` (your own home). \
324                 Another user's home needs a passwd lookup whose answer depends on who the \
325                 daemon runs as, so write the path out in full instead."
326            ),
327            Self::NoHomeForTilde { name, field } => write!(
328                f,
329                "`{name}`: {field} begins with `~/` but no home directory could be found. \
330                 Set {HOME_DIR_VAR}, or write the path out in full."
331            ),
332            Self::NoShepHome { name, field } => write!(
333                f,
334                "`{name}`: {field} carries `{{{{SHEP_HOME}}}}` but no shep home could be \
335                 found. Set {SHEP_HOME_VAR}, or write the path out in full."
336            ),
337            Self::WatchWithoutCwd { name } => {
338                write!(f, "sheep `{name}` has watch = true but no cwd to watch")
339            }
340            Self::ZeroWatchDelay { name } => {
341                write!(
342                    f,
343                    "sheep `{name}` has watch_delay = 0: must be greater than 0"
344                )
345            }
346            Self::InvalidWatchGlob {
347                name,
348                field,
349                pattern,
350                reason,
351            } => write!(
352                f,
353                "sheep `{name}` has an invalid {field} pattern `{pattern}`: {reason}"
354            ),
355            Self::InvalidLevelRule { name, reason } => {
356                write!(f, "sheep `{name}` has an invalid level rule: {reason}")
357            }
358            Self::BadTemplate {
359                name,
360                field,
361                reason,
362            } => write!(f, "sheep `{name}`, {field}: {reason}"),
363            Self::SecretInLogPath { name, field } => write!(
364                f,
365                "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`"
366            ),
367            Self::SharedLogPath { name, field } => write!(
368                f,
369                "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"
370            ),
371            Self::SelfDependency(n) => {
372                write!(f, "`{n}` names itself in depends_on")
373            }
374            Self::InstanceDependency { sheep, target } => {
375                let app = target.split(':').next().unwrap_or(target);
376                write!(
377                    f,
378                    "`{sheep}` depends on `{target}`, which names one instance. \
379                     Depend on `{app}` instead: a dependency waits for every \
380                     instance of an app"
381                )
382            }
383        }
384    }
385}
386
387impl core::error::Error for NormalizeError {}