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 lab 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 #[test]
366 fn every_code_reads_as_written_in_json() {
367 for code in [
368 ErrorCode::PortInUse,
369 ErrorCode::ManifestVersion,
370 ErrorCode::NothingToResume,
371 ErrorCode::RunName,
372 ErrorCode::ProviderRefused,
373 ] {
374 assert_eq!(serde_json::to_value(code).unwrap(), serde_json::json!(code.as_str()));
375 }
376 }
377
378 #[test]
379 fn another_manifest_version_has_its_own_code() {
380 let err = isoloom_manifest::parse(r#"{"manifest_version": 1}"#).unwrap_err();
381 let err = Error::Manifest {
382 path: "run.json".into(),
383 source: err,
384 };
385 assert_eq!(err.code(), ErrorCode::ManifestVersion);
386 assert!(err.to_string().starts_with("run.json: manifest_version 1"), "{err}");
387 }
388}