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