1use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
10#[serde(rename_all = "snake_case")]
11#[non_exhaustive]
12pub enum ErrorCode {
13 ManifestInvalid,
15 ManifestVersion,
17 Unsupported,
19 Io,
21 Locked,
23 ToolMissing,
25 ToolFailed,
27 ToolTooOld,
29 Timeout,
31 AlreadyRunning,
33 RunningElsewhere,
35 PortInUse,
37 NotParkable,
39 NothingToResume,
41 NotRunning,
43 NotReady,
45 SecretMissing,
47 ParamInvalid,
49 RunName,
51 ProviderRefused,
54 ElevationDeclined,
56 MachineUnknown,
58 SlotsFull,
60 AdoptRefused,
62 Unreachable,
64 NotSealed,
66}
67
68impl ErrorCode {
69 #[must_use]
71 pub fn as_str(self) -> &'static str {
72 match self {
73 ErrorCode::ManifestInvalid => "manifest_invalid",
74 ErrorCode::ManifestVersion => "manifest_version",
75 ErrorCode::Unsupported => "unsupported",
76 ErrorCode::Io => "io",
77 ErrorCode::Locked => "locked",
78 ErrorCode::ToolMissing => "tool_missing",
79 ErrorCode::ToolFailed => "tool_failed",
80 ErrorCode::ToolTooOld => "tool_too_old",
81 ErrorCode::Timeout => "timeout",
82 ErrorCode::AlreadyRunning => "already_running",
83 ErrorCode::RunningElsewhere => "running_elsewhere",
84 ErrorCode::PortInUse => "port_in_use",
85 ErrorCode::NotParkable => "not_parkable",
86 ErrorCode::NothingToResume => "nothing_to_resume",
87 ErrorCode::NotRunning => "not_running",
88 ErrorCode::NotReady => "not_ready",
89 ErrorCode::SecretMissing => "secret_missing",
90 ErrorCode::ParamInvalid => "param_invalid",
91 ErrorCode::RunName => "run_name",
92 ErrorCode::ProviderRefused => "provider_refused",
93 ErrorCode::ElevationDeclined => "elevation_declined",
94 ErrorCode::MachineUnknown => "machine_unknown",
95 ErrorCode::SlotsFull => "slots_full",
96 ErrorCode::AdoptRefused => "adopt_refused",
97 ErrorCode::Unreachable => "unreachable",
98 ErrorCode::NotSealed => "not_sealed",
99 }
100 }
101}
102
103pub type Result<T, E = Error> = std::result::Result<T, E>;
105
106#[derive(Debug, thiserror::Error)]
108#[non_exhaustive]
109pub enum Error {
110 #[error("{}: {source}", path.display())]
112 Manifest {
113 path: PathBuf,
115 source: isoloom_manifest::Error,
117 },
118 #[error("{what} isn't supported by this runner yet")]
120 Unsupported {
121 what: String,
123 },
124 #[error("{} couldn't be {action}: {source}", path.display())]
126 Io {
127 path: PathBuf,
129 action: &'static str,
131 source: std::io::Error,
133 },
134 #[error("run `{run}` is busy: another process runs an operation on it")]
136 Locked {
137 run: String,
139 },
140 #[error("step `{step}`: `{tool}` isn't installed or isn't on PATH")]
142 ToolMissing {
143 step: String,
145 tool: String,
147 },
148 #[error("`{tool}` is version {found}; this run needs {required} or later")]
150 ToolTooOld {
151 tool: String,
153 found: String,
155 required: String,
157 },
158 #[error("step `{step}` failed: {reason}")]
160 ToolFailed {
161 step: String,
163 reason: String,
165 },
166 #[error("step `{step}` timed out after {after_s}s and was stopped")]
168 Timeout {
169 step: String,
171 after_s: u64,
173 },
174 #[error("run `{run}` is already running")]
176 AlreadyRunning {
177 run: String,
179 },
180 #[error("run `{run}` is recorded on `{recorded}`, not `{asked}`")]
182 RunningElsewhere {
183 run: String,
185 recorded: String,
187 asked: String,
189 },
190 #[error("host port {port}/{protocol} is in use")]
192 PortInUse {
193 port: u16,
195 protocol: &'static str,
197 },
198 #[error("run `{run}` can't be parked ({mode}): its manifest has no steps for it")]
200 NotParkable {
201 run: String,
203 mode: &'static str,
205 },
206 #[error("run `{run}` has nothing left to resume: start it again")]
208 NothingToResume {
209 run: String,
211 },
212 #[error("run `{run}` isn't running")]
214 NotRunning {
215 run: String,
217 },
218 #[error("not ready: {condition}: {detail}")]
220 NotReady {
221 condition: String,
223 detail: String,
225 },
226 #[error("secret `{name}` is required and has no value")]
228 SecretMissing {
229 name: String,
231 },
232 #[error("param `{name}`: {reason}")]
234 Param {
235 name: String,
237 reason: String,
239 },
240 #[error("{at}: `${{{placeholder}}}` has no value in this operation")]
242 Unbound {
243 at: String,
245 placeholder: String,
247 },
248 #[error("run `{run}` has no machine `{machine}`")]
250 MachineUnknown {
251 run: String,
253 machine: String,
255 },
256 #[error("elevation: {reason}")]
258 Elevation {
259 reason: String,
261 },
262 #[error("provider `{provider}`: {reason}")]
264 Provider {
265 provider: String,
267 reason: String,
269 },
270 #[error("no slot is free on `{place}`: {held} runs hold one; stop one first")]
272 SlotsFull {
273 place: String,
275 held: usize,
277 },
278 #[error("step `{step}`: {login} didn't answer over SSH within {after_s}s: {reason}")]
280 Unreachable {
281 step: String,
283 login: String,
285 after_s: u64,
287 reason: String,
289 },
290 #[error("run `{run}` couldn't be sealed, so it isn't ready (stop it to remove it): {reason}")]
292 Unsealed {
293 run: String,
295 reason: String,
297 },
298 #[error("run `{run}` can't take the deployment over: {reason}")]
300 Adopt {
301 run: String,
303 reason: String,
305 },
306 #[error("`{value}` can't name a run: use 1 to 63 lowercase letters, digits, `-` or `_`, starting with a letter or digit")]
308 RunName {
309 value: String,
311 },
312}
313
314impl Error {
315 #[must_use]
317 pub fn code(&self) -> ErrorCode {
318 match self {
319 Error::Manifest { source, .. } if source.code() == "version" => ErrorCode::ManifestVersion,
320 Error::Manifest { .. } => ErrorCode::ManifestInvalid,
321 Error::Unsupported { .. } | Error::Unbound { .. } => ErrorCode::Unsupported,
322 Error::Io { .. } => ErrorCode::Io,
323 Error::Locked { .. } => ErrorCode::Locked,
324 Error::ToolMissing { .. } => ErrorCode::ToolMissing,
325 Error::ToolTooOld { .. } => ErrorCode::ToolTooOld,
326 Error::ToolFailed { .. } => ErrorCode::ToolFailed,
327 Error::Timeout { .. } => ErrorCode::Timeout,
328 Error::AlreadyRunning { .. } => ErrorCode::AlreadyRunning,
329 Error::RunningElsewhere { .. } => ErrorCode::RunningElsewhere,
330 Error::PortInUse { .. } => ErrorCode::PortInUse,
331 Error::NotParkable { .. } => ErrorCode::NotParkable,
332 Error::NothingToResume { .. } => ErrorCode::NothingToResume,
333 Error::NotRunning { .. } => ErrorCode::NotRunning,
334 Error::NotReady { .. } => ErrorCode::NotReady,
335 Error::SecretMissing { .. } => ErrorCode::SecretMissing,
336 Error::Param { .. } => ErrorCode::ParamInvalid,
337 Error::RunName { .. } => ErrorCode::RunName,
338 Error::Provider { .. } => ErrorCode::ProviderRefused,
339 Error::Elevation { .. } => ErrorCode::ElevationDeclined,
340 Error::MachineUnknown { .. } => ErrorCode::MachineUnknown,
341 Error::SlotsFull { .. } => ErrorCode::SlotsFull,
342 Error::Adopt { .. } => ErrorCode::AdoptRefused,
343 Error::Unreachable { .. } => ErrorCode::Unreachable,
344 Error::Unsealed { .. } => ErrorCode::NotSealed,
345 }
346 }
347
348 pub(crate) fn io(path: impl Into<PathBuf>, action: &'static str) -> impl FnOnce(std::io::Error) -> Error {
350 let path = path.into();
351 move |source| Error::Io { path, action, source }
352 }
353}
354
355impl std::fmt::Display for ErrorCode {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 f.write_str(self.as_str())
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 const ALL: &[ErrorCode] = &[
369 ErrorCode::ManifestInvalid,
370 ErrorCode::ManifestVersion,
371 ErrorCode::Unsupported,
372 ErrorCode::Io,
373 ErrorCode::Locked,
374 ErrorCode::ToolMissing,
375 ErrorCode::ToolFailed,
376 ErrorCode::ToolTooOld,
377 ErrorCode::Timeout,
378 ErrorCode::AlreadyRunning,
379 ErrorCode::RunningElsewhere,
380 ErrorCode::PortInUse,
381 ErrorCode::NotParkable,
382 ErrorCode::NothingToResume,
383 ErrorCode::NotRunning,
384 ErrorCode::NotReady,
385 ErrorCode::SecretMissing,
386 ErrorCode::ParamInvalid,
387 ErrorCode::RunName,
388 ErrorCode::ProviderRefused,
389 ErrorCode::ElevationDeclined,
390 ErrorCode::MachineUnknown,
391 ErrorCode::SlotsFull,
392 ErrorCode::AdoptRefused,
393 ErrorCode::Unreachable,
394 ErrorCode::NotSealed,
395 ];
396
397 #[test]
398 fn all_lists_every_code_once() {
399 for &code in ALL {
402 match code {
403 ErrorCode::ManifestInvalid
404 | ErrorCode::ManifestVersion
405 | ErrorCode::Unsupported
406 | ErrorCode::Io
407 | ErrorCode::Locked
408 | ErrorCode::ToolMissing
409 | ErrorCode::ToolFailed
410 | ErrorCode::ToolTooOld
411 | ErrorCode::Timeout
412 | ErrorCode::AlreadyRunning
413 | ErrorCode::RunningElsewhere
414 | ErrorCode::PortInUse
415 | ErrorCode::NotParkable
416 | ErrorCode::NothingToResume
417 | ErrorCode::NotRunning
418 | ErrorCode::NotReady
419 | ErrorCode::SecretMissing
420 | ErrorCode::ParamInvalid
421 | ErrorCode::RunName
422 | ErrorCode::ProviderRefused
423 | ErrorCode::ElevationDeclined
424 | ErrorCode::MachineUnknown
425 | ErrorCode::SlotsFull
426 | ErrorCode::AdoptRefused
427 | ErrorCode::Unreachable
428 | ErrorCode::NotSealed => {}
429 }
430 }
431 let codes: Vec<&str> = ALL.iter().map(|c| c.as_str()).collect();
432 let mut unique = codes.clone();
433 unique.sort_unstable();
434 unique.dedup();
435 assert_eq!(unique.len(), codes.len(), "two variants share a code string: {codes:?}");
436 }
437
438 #[test]
439 fn every_code_round_trips_through_json_as_its_written_form() {
440 for &code in ALL {
441 let json = serde_json::to_value(code).unwrap();
442 assert_eq!(json, serde_json::json!(code.as_str()), "as_str() disagrees with serde for {code:?}");
443 let back: ErrorCode = serde_json::from_value(json).unwrap();
444 assert_eq!(back, code, "{code:?} didn't read back as itself");
445 }
446 }
447
448 #[test]
449 fn another_manifest_version_has_its_own_code() {
450 let err = isoloom_manifest::parse(r#"{"manifest_version": 1}"#).unwrap_err();
451 let err = Error::Manifest {
452 path: "run.json".into(),
453 source: err,
454 };
455 assert_eq!(err.code(), ErrorCode::ManifestVersion);
456 assert!(err.to_string().starts_with("run.json: manifest_version 1"), "{err}");
457 }
458}