Skip to main content

isb_core/
error.rs

1use std::time::Duration;
2
3/// Everything isb can fail with.
4#[derive(Debug, thiserror::Error)]
5pub enum Error {
6    /// Could not reach incusd at all (socket missing, permission denied, refused).
7    #[error("cannot connect to incusd at {socket}: {source}")]
8    Connect {
9        socket: String,
10        #[source]
11        source: std::io::Error,
12    },
13
14    /// A single HTTP request to incusd did not complete within its socket timeout.
15    #[error("incusd did not answer {method} {path} within {timeout:?}")]
16    RequestTimeout {
17        method: String,
18        path: String,
19        timeout: Duration,
20    },
21
22    /// incusd answered with an error response.
23    #[error("incusd: {method} {path}: {message} (HTTP {status})")]
24    Api {
25        method: String,
26        path: String,
27        status: u16,
28        message: String,
29    },
30
31    /// A background operation ran past its deadline. `step` names what isb was
32    /// doing ("create instance", "start instance", ...), so a stall is reported
33    /// as the step that stalled rather than as a bare hang.
34    #[error(
35        "{step} stalled: operation {operation} still {status} after {waited:?}{}",
36        if *.cancelled { " (cancelled)" } else { " (not cancellable; left running on the server)" }
37    )]
38    OperationTimeout {
39        step: String,
40        operation: String,
41        status: String,
42        waited: Duration,
43        cancelled: bool,
44    },
45
46    /// A background operation finished unsuccessfully.
47    #[error("{step} failed: {message}")]
48    OperationFailed { step: String, message: String },
49
50    /// A readiness check did not pass before its deadline.
51    #[error("sandbox {sandbox} not ready after {waited:?}: {check} ({detail})")]
52    NotReady {
53        sandbox: String,
54        check: String,
55        detail: String,
56        waited: Duration,
57    },
58
59    /// An exec with an explicit timeout ran past it; the process was sent SIGKILL.
60    #[error("exec {argv} timed out after {timeout:?} (killed)")]
61    ExecTimeout { argv: String, timeout: Duration },
62
63    #[error("{0} not found")]
64    NotFound(String),
65
66    #[error("sandbox {0} already exists")]
67    AlreadyExists(String),
68
69    /// The spec, the compose file or an argument is invalid.
70    #[error("{0}")]
71    Invalid(String),
72
73    /// The caller may not do this (another org's resources, a platform-only
74    /// tool, or no isb account).
75    #[error("forbidden: {0}")]
76    Forbidden(String),
77
78    /// Variable interpolation failed (unset variable, `${VAR:?message}`, bad syntax).
79    #[error("interpolation: {0}")]
80    Interpolation(String),
81
82    #[error("{path}: {message}")]
83    Parse { path: String, message: String },
84
85    #[error("websocket: {0}")]
86    WebSocket(String),
87
88    #[error("protocol: {0}")]
89    Protocol(String),
90
91    /// A tool called through `isb serve` failed. `code` and `data` are the
92    /// fields of the server-side error's [`crate::rpc::error_json`].
93    #[error("{message}")]
94    Remote {
95        code: String,
96        message: String,
97        data: serde_json::Value,
98    },
99
100    #[error(transparent)]
101    Io(#[from] std::io::Error),
102
103    #[error(transparent)]
104    Json(#[from] serde_json::Error),
105}
106
107impl Error {
108    #[doc(hidden)]
109    pub fn invalid(msg: impl Into<String>) -> Self {
110        Error::Invalid(msg.into())
111    }
112
113    /// True for an incusd "not found" answer.
114    pub fn is_not_found(&self) -> bool {
115        matches!(self, Error::Api { status: 404, .. } | Error::NotFound(_))
116            || matches!(self, Error::Remote { code, .. } if code == "not_found")
117    }
118
119    /// True for an incusd conflict (the object already exists).
120    pub fn is_conflict(&self) -> bool {
121        matches!(
122            self,
123            Error::Api { status: 409, .. } | Error::AlreadyExists(_)
124        ) || matches!(self, Error::Api { message, .. } if message.contains("already exists"))
125            || matches!(self, Error::Remote { code, .. } if code == "already_exists")
126    }
127
128    /// True when the error is a deadline, either on a request or an operation.
129    pub fn is_timeout(&self) -> bool {
130        matches!(
131            self,
132            Error::RequestTimeout { .. } | Error::OperationTimeout { .. }
133        ) || matches!(self, Error::Remote { code, .. } if code == "request_timeout" || code == "operation_timeout")
134    }
135}
136
137pub type Result<T, E = Error> = std::result::Result<T, E>;