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 sandbox capability is unavailable on the target platform.
203    #[error(
204        code = "SANDBOX_CAPABILITY_UNSUPPORTED",
205        message = "Sandbox capability '{capability}' is not supported on {platform}",
206        retryable = "false",
207        internal = "false",
208        http_status_code = 400
209    )]
210    SandboxCapabilityUnsupported {
211        /// The capability that was requested
212        capability: String,
213        /// The platform that does not offer it
214        platform: String,
215    },
216
217    /// The target platform has no sandbox backend at all.
218    #[error(
219        code = "SANDBOX_PLATFORM_UNSUPPORTED",
220        message = "Sandbox is not available on {platform}",
221        retryable = "false",
222        internal = "false",
223        http_status_code = 400
224    )]
225    SandboxPlatformUnsupported {
226        /// The platform with no sandbox backend
227        platform: String,
228    },
229
230    /// A bundle signature or signing key was rejected.
231    #[error(
232        code = "BUNDLE_SIGNATURE_INVALID",
233        message = "Bundle signature invalid: {reason}",
234        retryable = "false",
235        internal = "false",
236        http_status_code = 400
237    )]
238    BundleSignatureInvalid {
239        /// Why the signature or key was rejected
240        reason: String,
241    },
242
243    /// A sandbox capability was refused.
244    #[error(
245        code = "SANDBOX_CAPABILITY_REFUSED",
246        message = "Sandbox capability refused: {reason}",
247        retryable = "false",
248        internal = "false",
249        http_status_code = 403
250    )]
251    SandboxCapabilityRefused {
252        /// Why it was refused. Deliberately coarse — it must not reveal which sandboxes exist.
253        reason: String,
254    },
255
256    /// A sandbox limit is missing or not a valid quantity.
257    #[error(
258        code = "SANDBOX_LIMIT_INVALID",
259        message = "Sandbox '{resource_id}' has an invalid {field} limit '{value}': {reason}",
260        retryable = "false",
261        internal = "false",
262        http_status_code = 400
263    )]
264    SandboxLimitInvalid {
265        /// The sandbox whose limit failed validation
266        resource_id: String,
267        /// Which limit (cpu, memory, disk, maxProcesses)
268        field: String,
269        /// The rejected value
270        value: String,
271        /// Why it was rejected
272        reason: String,
273    },
274
275    // Commands protocol errors
276    /// Invalid Commands envelope.
277    #[error(
278        code = "INVALID_ENVELOPE",
279        message = "Invalid Commands envelope: {message}",
280        retryable = "false",
281        internal = "false"
282    )]
283    InvalidEnvelope {
284        message: String,
285        field: Option<String>,
286    },
287
288    /// A command-enabled resource exists but the deployment has no token to
289    /// authenticate its command transport.
290    #[error(
291        code = "COMMAND_TOKEN_MISSING",
292        message = "Deployment has command-enabled resource '{resource_id}' but no deployment token to authenticate it: {reason}",
293        retryable = "false",
294        internal = "false"
295    )]
296    CommandTokenMissing { resource_id: String, reason: String },
297
298    /// Public URL configuration is invalid.
299    #[error(
300        code = "PUBLIC_URL_INVALID",
301        message = "Invalid public URL for resource '{resource_id}': {reason}",
302        retryable = "false",
303        internal = "false",
304        http_status_code = 400
305    )]
306    PublicUrlInvalid { resource_id: String, reason: String },
307
308    /// JSON serialization failed.
309    #[error(
310        code = "JSON_SERIALIZATION_FAILED",
311        message = "Failed to serialize JSON: {reason}",
312        retryable = "false",
313        internal = "false"
314    )]
315    JsonSerializationFailed { reason: String },
316
317    /// JSON deserialization failed.
318    #[error(
319        code = "JSON_DESERIALIZATION_FAILED",
320        message = "Failed to deserialize JSON: {reason}",
321        retryable = "false",
322        internal = "false"
323    )]
324    JsonDeserializationFailed { reason: String },
325
326    /// Distribution template serialization failed.
327    #[error(
328        code = "TEMPLATE_SERIALIZATION_FAILED",
329        message = "Failed to serialize {format} template: {reason}",
330        retryable = "false",
331        internal = "true"
332    )]
333    TemplateSerializationFailed { format: String, reason: String },
334
335    /// ImportData did not match the registered typed payload.
336    #[error(
337        code = "IMPORT_DATA_INVALID",
338        message = "Invalid ImportData for resource '{resource_id}' ({resource_type}) on {platform}",
339        retryable = "false",
340        internal = "false",
341        http_status_code = 400
342    )]
343    ImportDataInvalid {
344        resource_id: String,
345        resource_type: ResourceType,
346        platform: Platform,
347    },
348
349    /// No registry implementation exists for a resource/platform pair.
350    #[error(
351        code = "IMPORT_REGISTRATION_MISSING",
352        message = "Missing {registration_kind} registration for resource type '{resource_type}' on {platform}",
353        retryable = "false",
354        internal = "true"
355    )]
356    ImportRegistrationMissing {
357        resource_type: ResourceType,
358        platform: Platform,
359        registration_kind: String,
360    },
361
362    /// ImportData schema serialization failed.
363    #[error(
364        code = "IMPORT_SCHEMA_SERIALIZATION_FAILED",
365        message = "Failed to serialize ImportData schema",
366        retryable = "false",
367        internal = "true"
368    )]
369    ImportSchemaSerializationFailed,
370}
371
372pub type Result<T> = alien_error::Result<T, ErrorData>;