Skip to main content

a3s_box_core/
error.rs

1use thiserror::Error;
2
3/// A3S Box error types
4#[derive(Error, Debug)]
5pub enum BoxError {
6    /// VM failed to start
7    #[error("VM boot failed: {message}{}", .hint.as_ref().map(|h| format!(" (hint: {h})")).unwrap_or_default())]
8    BoxBootError {
9        message: String,
10        hint: Option<String>,
11    },
12
13    /// Timeout error
14    #[error("Timeout: {0}")]
15    TimeoutError(String),
16
17    /// I/O error
18    #[error("I/O error: {0}")]
19    IoError(#[from] std::io::Error),
20
21    /// Serialization error
22    #[error("Serialization error: {0}")]
23    SerializationError(String),
24
25    /// Configuration error
26    #[error("Configuration error: {0}")]
27    ConfigError(String),
28
29    /// TEE configuration error
30    #[error("TEE configuration error: {0}")]
31    TeeConfig(String),
32
33    /// TEE hardware not available
34    #[error("TEE hardware not available: {0}")]
35    TeeNotSupported(String),
36
37    /// Attestation error
38    #[error("Attestation error: {0}")]
39    AttestationError(String),
40
41    /// OCI image error
42    #[error("OCI image error: {0}")]
43    OciImageError(String),
44
45    /// Container registry error
46    #[error("Registry error: {registry} - {message}")]
47    RegistryError { registry: String, message: String },
48
49    /// Cache error
50    #[error("Cache error: {0}")]
51    CacheError(String),
52
53    /// Pool error
54    #[error("Pool error: {0}")]
55    PoolError(String),
56
57    /// Exec error
58    #[error("Exec error: {0}")]
59    ExecError(String),
60
61    /// Build error
62    #[error("Build error: {0}")]
63    BuildError(String),
64
65    /// Network error
66    #[error("Network error: {0}")]
67    NetworkError(String),
68
69    /// VM state machine error (invalid state transition or precondition)
70    #[error("VM state error: {0}")]
71    StateError(String),
72
73    /// Audit log error
74    #[error("Audit error: {0}")]
75    AuditError(String),
76
77    /// Resource resize error (hot-resize not supported or failed)
78    #[error("Resize error: {0}")]
79    ResizeError(String),
80
81    /// Generic error
82    #[error("{0}")]
83    Other(String),
84}
85
86impl From<serde_json::Error> for BoxError {
87    fn from(err: serde_json::Error) -> Self {
88        BoxError::SerializationError(err.to_string())
89    }
90}
91
92impl From<serde_yaml::Error> for BoxError {
93    fn from(err: serde_yaml::Error) -> Self {
94        BoxError::SerializationError(err.to_string())
95    }
96}
97
98/// Result type alias for A3S Box operations
99pub type Result<T> = std::result::Result<T, BoxError>;
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_box_boot_error_display() {
107        let error = BoxError::BoxBootError {
108            message: "Failed to start VM".to_string(),
109            hint: Some("Check virtualization support".to_string()),
110        };
111        // The actionable hint is rendered (matching the CRI path) so an operator
112        // who hits a boot failure on the CLI gets the remediation, not just the
113        // diagnosis.
114        assert_eq!(
115            error.to_string(),
116            "VM boot failed: Failed to start VM (hint: Check virtualization support)"
117        );
118    }
119
120    #[test]
121    fn test_box_boot_error_without_hint() {
122        let error = BoxError::BoxBootError {
123            message: "No kernel found".to_string(),
124            hint: None,
125        };
126        assert_eq!(error.to_string(), "VM boot failed: No kernel found");
127    }
128
129    #[test]
130    fn test_timeout_error_display() {
131        let error = BoxError::TimeoutError("Operation timed out after 30s".to_string());
132        assert_eq!(error.to_string(), "Timeout: Operation timed out after 30s");
133    }
134
135    #[test]
136    fn test_io_error_conversion() {
137        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
138        let box_error: BoxError = io_error.into();
139        assert!(matches!(box_error, BoxError::IoError(_)));
140        assert!(box_error.to_string().contains("file not found"));
141    }
142
143    #[test]
144    fn test_serialization_error_display() {
145        let error = BoxError::SerializationError("Invalid JSON".to_string());
146        assert_eq!(error.to_string(), "Serialization error: Invalid JSON");
147    }
148
149    #[test]
150    fn test_config_error_display() {
151        let error = BoxError::ConfigError("Missing required field".to_string());
152        assert_eq!(
153            error.to_string(),
154            "Configuration error: Missing required field"
155        );
156    }
157
158    #[test]
159    fn test_other_error_display() {
160        let error = BoxError::Other("Unknown error occurred".to_string());
161        assert_eq!(error.to_string(), "Unknown error occurred");
162    }
163
164    #[test]
165    fn test_tee_config_error_display() {
166        let error = BoxError::TeeConfig("Failed to set TEE config file".to_string());
167        assert_eq!(
168            error.to_string(),
169            "TEE configuration error: Failed to set TEE config file"
170        );
171    }
172
173    #[test]
174    fn test_tee_not_supported_error_display() {
175        let error = BoxError::TeeNotSupported("AMD SEV-SNP not available".to_string());
176        assert_eq!(
177            error.to_string(),
178            "TEE hardware not available: AMD SEV-SNP not available"
179        );
180    }
181
182    #[test]
183    fn test_attestation_error_display() {
184        let error = BoxError::AttestationError("Failed to get SNP report".to_string());
185        assert_eq!(
186            error.to_string(),
187            "Attestation error: Failed to get SNP report"
188        );
189    }
190
191    #[test]
192    fn test_oci_image_error_display() {
193        let error = BoxError::OciImageError("Invalid manifest".to_string());
194        assert_eq!(error.to_string(), "OCI image error: Invalid manifest");
195    }
196
197    #[test]
198    fn test_registry_error_display() {
199        let error = BoxError::RegistryError {
200            registry: "ghcr.io".to_string(),
201            message: "Authentication failed".to_string(),
202        };
203        assert_eq!(
204            error.to_string(),
205            "Registry error: ghcr.io - Authentication failed"
206        );
207    }
208
209    #[test]
210    fn test_serde_json_error_conversion() {
211        let json_str = "{ invalid json }";
212        let result: std::result::Result<serde_json::Value, _> = serde_json::from_str(json_str);
213        let json_error = result.unwrap_err();
214        let box_error: BoxError = json_error.into();
215        assert!(matches!(box_error, BoxError::SerializationError(_)));
216    }
217
218    #[test]
219    fn test_serde_yaml_error_conversion() {
220        let yaml_str = "invalid: yaml: content:";
221        let result: std::result::Result<serde_yaml::Value, _> = serde_yaml::from_str(yaml_str);
222        let yaml_error = result.unwrap_err();
223        let box_error: BoxError = yaml_error.into();
224        assert!(matches!(box_error, BoxError::SerializationError(_)));
225    }
226
227    #[test]
228    fn test_result_type_alias() {
229        fn returns_ok() -> Result<i32> {
230            Ok(42)
231        }
232
233        fn returns_err() -> Result<i32> {
234            Err(BoxError::Other("test error".to_string()))
235        }
236
237        assert_eq!(returns_ok().unwrap(), 42);
238        assert!(returns_err().is_err());
239    }
240
241    #[test]
242    fn test_resize_error_display() {
243        let error = BoxError::ResizeError("Cannot change vCPU count".to_string());
244        assert_eq!(error.to_string(), "Resize error: Cannot change vCPU count");
245    }
246
247    #[test]
248    fn test_error_is_debug() {
249        let error = BoxError::Other("test".to_string());
250        let debug_str = format!("{:?}", error);
251        assert!(debug_str.contains("Other"));
252    }
253
254    #[test]
255    fn test_cache_error_display() {
256        let error = BoxError::CacheError("Rootfs cache corrupted".to_string());
257        assert_eq!(error.to_string(), "Cache error: Rootfs cache corrupted");
258    }
259
260    #[test]
261    fn test_pool_error_display() {
262        let error = BoxError::PoolError("No idle VMs available".to_string());
263        assert_eq!(error.to_string(), "Pool error: No idle VMs available");
264    }
265
266    #[test]
267    fn test_exec_error_display() {
268        let error = BoxError::ExecError("Command not found".to_string());
269        assert_eq!(error.to_string(), "Exec error: Command not found");
270    }
271
272    #[test]
273    fn test_build_error_display() {
274        let error = BoxError::BuildError("Dockerfile parse failed".to_string());
275        assert_eq!(error.to_string(), "Build error: Dockerfile parse failed");
276    }
277}