Skip to main content

alien_core/
error.rs

1use alien_error::AlienErrorData;
2use serde::{Deserialize, Serialize};
3
4use crate::{Platform, ResourceType};
5
6/// Core error data exposed by the `alien-core` crate.
7#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub enum ErrorData {
10    /// A deployment selected resources outside the release's declared choices.
11    #[error(
12        code = "CONTAINER_RESOURCE_SELECTION_INVALID",
13        message = "Invalid {dimension} selection for container '{resource_id}': {reason}",
14        retryable = "false",
15        internal = "false",
16        http_status_code = 400
17    )]
18    ContainerResourceSelectionInvalid {
19        resource_id: String,
20        dimension: String,
21        reason: String,
22    },
23    /// A fallback error when nothing more specific matches.
24    #[error(
25        code = "GENERIC_ERROR",
26        message = "{message}",
27        retryable = "true",
28        internal = "false"
29    )]
30    GenericError {
31        /// Human-readable description of the error.
32        message: String,
33    },
34
35    /// Resource type mismatch found during stack operations.
36    #[error(
37        code = "UNEXPECTED_RESOURCE_TYPE",
38        message = "Unexpected resource type for resource '{resource_id}': expected {expected}, but got {actual}",
39        retryable = "false",
40        internal = "false"
41    )]
42    UnexpectedResourceType {
43        resource_id: String,
44        expected: ResourceType,
45        actual: ResourceType,
46    },
47
48    /// Attempt to update a resource that does not support updates.
49    #[error(
50        code = "INVALID_RESOURCE_UPDATE",
51        message = "Resource '{resource_id}' cannot be updated: {reason}",
52        retryable = "false",
53        internal = "false"
54    )]
55    InvalidResourceUpdate { resource_id: String, reason: String },
56
57    /// Worker execution timeout is outside the supported range.
58    #[error(
59        code = "WORKER_TIMEOUT_INVALID",
60        message = "Worker timeoutSeconds must be between 1 and {max_timeout_seconds} seconds, got {timeout_seconds}",
61        retryable = "false",
62        internal = "false",
63        http_status_code = 400
64    )]
65    WorkerTimeoutInvalid {
66        timeout_seconds: u32,
67        max_timeout_seconds: u32,
68    },
69
70    /// The resource exists but has not produced any outputs yet.
71    #[error(
72        code = "RESOURCE_HAS_NO_OUTPUTS",
73        message = "Resource '{resource_id}' has no outputs",
74        retryable = "true",
75        internal = "false"
76    )]
77    ResourceHasNoOutputs { resource_id: String },
78
79    /// Requested resource absent from the stack state.
80    #[error(
81        code = "RESOURCE_NOT_FOUND",
82        message = "Resource '{resource_id}' not found in stack state. Available resources: {available_resources:?}",
83        retryable = "false",
84        internal = "false",
85        http_status_code = 404
86    )]
87    ResourceNotFound {
88        resource_id: String,
89        available_resources: Vec<String>,
90    },
91
92    /// Binding configuration is invalid or missing required fields.
93    #[error(
94        code = "BINDING_CONFIG_INVALID",
95        message = "Invalid binding configuration for '{binding_name}': {reason}",
96        retryable = "false",
97        internal = "false"
98    )]
99    BindingConfigInvalid {
100        binding_name: String,
101        reason: String,
102    },
103
104    /// Environment variable for binding is missing.
105    #[error(
106        code = "BINDING_ENV_VAR_MISSING",
107        message = "Missing environment variable '{env_var}' for binding '{binding_name}'",
108        retryable = "false",
109        internal = "false"
110    )]
111    BindingEnvVarMissing {
112        binding_name: String,
113        env_var: String,
114    },
115
116    /// Failed to parse binding JSON from environment variable.
117    #[error(
118        code = "BINDING_JSON_PARSE_FAILED",
119        message = "Failed to parse binding JSON for '{binding_name}': {reason}",
120        retryable = "false",
121        internal = "false"
122    )]
123    BindingJsonParseFailed {
124        binding_name: String,
125        reason: String,
126    },
127
128    /// Unexpected combination of resource statuses when computing stack status.
129    #[error(
130        code = "UNEXPECTED_RESOURCE_STATUS_COMBINATION",
131        message = "Unexpected resource status combination during {operation}: {resource_statuses:?}",
132        retryable = "false",
133        internal = "true"
134    )]
135    UnexpectedResourceStatusCombination {
136        resource_statuses: Vec<String>,
137        operation: String,
138    },
139
140    /// External binding type does not match the resource type.
141    #[error(
142        code = "EXTERNAL_BINDING_TYPE_MISMATCH",
143        message = "External binding type mismatch for resource '{resource_id}': expected {expected}, got {actual}",
144        retryable = "false",
145        internal = "false"
146    )]
147    ExternalBindingTypeMismatch {
148        resource_id: String,
149        expected: String,
150        actual: String,
151    },
152
153    // Presigned request errors
154    /// Presigned request has expired.
155    #[error(
156        code = "PRESIGNED_REQUEST_EXPIRED",
157        message = "Presigned request for '{path}' expired at {expired_at}",
158        retryable = "false",
159        internal = "false"
160    )]
161    PresignedRequestExpired {
162        path: String,
163        expired_at: chrono::DateTime<chrono::Utc>,
164    },
165
166    /// HTTP request failed.
167    #[error(
168        code = "HTTP_REQUEST_FAILED",
169        message = "HTTP {method} request to '{url}' failed",
170        retryable = "true",
171        internal = "false"
172    )]
173    HttpRequestFailed { url: String, method: String },
174
175    /// Operation not supported.
176    #[error(
177        code = "OPERATION_NOT_SUPPORTED",
178        message = "Operation '{operation}' not supported: {reason}",
179        retryable = "false",
180        internal = "false"
181    )]
182    OperationNotSupported { operation: String, reason: String },
183
184    /// Local filesystem error.
185    #[error(
186        code = "LOCAL_FILESYSTEM_ERROR",
187        message = "Local filesystem error for '{path}' during {operation}",
188        retryable = "false",
189        internal = "false"
190    )]
191    LocalFilesystemError { path: String, operation: String },
192
193    /// Feature not enabled.
194    #[error(
195        code = "FEATURE_NOT_ENABLED",
196        message = "Feature '{feature}' is not enabled",
197        retryable = "false",
198        internal = "false"
199    )]
200    FeatureNotEnabled { feature: String },
201
202    /// A resource requires setup capabilities unavailable to automatic management.
203    #[error(
204        code = "AUTOMATIC_SETUP_RESOURCE_UNSUPPORTED",
205        message = "Automatic infrastructure management does not support {resource_type} '{resource_id}' on {platform}; use manual setup",
206        retryable = "false",
207        internal = "false",
208        http_status_code = 400
209    )]
210    AutomaticSetupResourceUnsupported {
211        resource_type: String,
212        resource_id: String,
213        platform: Platform,
214    },
215
216    /// A sandbox capability is unavailable on the target platform.
217    #[error(
218        code = "SANDBOX_CAPABILITY_UNSUPPORTED",
219        message = "Sandbox capability '{capability}' is not supported on {platform}",
220        retryable = "false",
221        internal = "false",
222        http_status_code = 400
223    )]
224    SandboxCapabilityUnsupported {
225        /// The capability that was requested
226        capability: String,
227        /// The platform that does not offer it
228        platform: String,
229    },
230
231    /// The target platform has no sandbox backend at all.
232    #[error(
233        code = "SANDBOX_PLATFORM_UNSUPPORTED",
234        message = "Sandbox is not available on {platform}",
235        retryable = "false",
236        internal = "false",
237        http_status_code = 400
238    )]
239    SandboxPlatformUnsupported {
240        /// The platform with no sandbox backend
241        platform: String,
242    },
243
244    /// A bundle signature or signing key was rejected.
245    #[error(
246        code = "BUNDLE_SIGNATURE_INVALID",
247        message = "Bundle signature invalid: {reason}",
248        retryable = "false",
249        internal = "false",
250        http_status_code = 400
251    )]
252    BundleSignatureInvalid {
253        /// Why the signature or key was rejected
254        reason: String,
255    },
256
257    /// A sandbox capability was refused.
258    #[error(
259        code = "SANDBOX_CAPABILITY_REFUSED",
260        message = "Sandbox capability refused: {reason}",
261        retryable = "false",
262        internal = "false",
263        http_status_code = 403
264    )]
265    SandboxCapabilityRefused {
266        /// Why it was refused. Deliberately coarse — it must not reveal which sandboxes exist.
267        reason: String,
268    },
269
270    /// A sandbox limit is missing or not a valid quantity.
271    #[error(
272        code = "SANDBOX_LIMIT_INVALID",
273        message = "Sandbox '{resource_id}' has an invalid {field} limit '{value}': {reason}",
274        retryable = "false",
275        internal = "false",
276        http_status_code = 400
277    )]
278    SandboxLimitInvalid {
279        /// The sandbox whose limit failed validation
280        resource_id: String,
281        /// Which limit (cpu, memory, disk, maxProcesses)
282        field: String,
283        /// The rejected value
284        value: String,
285        /// Why it was rejected
286        reason: String,
287    },
288
289    // Commands protocol errors
290    /// Invalid Commands envelope.
291    #[error(
292        code = "INVALID_ENVELOPE",
293        message = "Invalid Commands envelope: {message}",
294        retryable = "false",
295        internal = "false"
296    )]
297    InvalidEnvelope {
298        message: String,
299        field: Option<String>,
300    },
301
302    /// A command-enabled resource exists but the deployment has no token to
303    /// authenticate its command transport.
304    #[error(
305        code = "COMMAND_TOKEN_MISSING",
306        message = "Deployment has command-enabled resource '{resource_id}' but no deployment token to authenticate it: {reason}",
307        retryable = "false",
308        internal = "false"
309    )]
310    CommandTokenMissing { resource_id: String, reason: String },
311
312    /// Public URL configuration is invalid.
313    #[error(
314        code = "PUBLIC_URL_INVALID",
315        message = "Invalid public URL for resource '{resource_id}': {reason}",
316        retryable = "false",
317        internal = "false",
318        http_status_code = 400
319    )]
320    PublicUrlInvalid { resource_id: String, reason: String },
321
322    /// JSON serialization failed.
323    #[error(
324        code = "JSON_SERIALIZATION_FAILED",
325        message = "Failed to serialize JSON: {reason}",
326        retryable = "false",
327        internal = "false"
328    )]
329    JsonSerializationFailed { reason: String },
330
331    /// JSON deserialization failed.
332    #[error(
333        code = "JSON_DESERIALIZATION_FAILED",
334        message = "Failed to deserialize JSON: {reason}",
335        retryable = "false",
336        internal = "false"
337    )]
338    JsonDeserializationFailed { reason: String },
339
340    /// Distribution template serialization failed.
341    #[error(
342        code = "TEMPLATE_SERIALIZATION_FAILED",
343        message = "Failed to serialize {format} template: {reason}",
344        retryable = "false",
345        internal = "true"
346    )]
347    TemplateSerializationFailed { format: String, reason: String },
348
349    /// ImportData did not match the registered typed payload.
350    #[error(
351        code = "IMPORT_DATA_INVALID",
352        message = "Invalid ImportData for resource '{resource_id}' ({resource_type}) on {platform}",
353        retryable = "false",
354        internal = "false",
355        http_status_code = 400
356    )]
357    ImportDataInvalid {
358        resource_id: String,
359        resource_type: ResourceType,
360        platform: Platform,
361    },
362
363    /// No registry implementation exists for a resource/platform pair.
364    #[error(
365        code = "IMPORT_REGISTRATION_MISSING",
366        message = "Missing {registration_kind} registration for resource type '{resource_type}' on {platform}",
367        retryable = "false",
368        internal = "true"
369    )]
370    ImportRegistrationMissing {
371        resource_type: ResourceType,
372        platform: Platform,
373        registration_kind: String,
374    },
375
376    /// ImportData schema serialization failed.
377    #[error(
378        code = "IMPORT_SCHEMA_SERIALIZATION_FAILED",
379        message = "Failed to serialize ImportData schema",
380        retryable = "false",
381        internal = "true"
382    )]
383    ImportSchemaSerializationFailed,
384}
385
386pub type Result<T> = alien_error::Result<T, ErrorData>;