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    /// 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 {}