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    /// An applicable stack input is missing or violates its declared constraints.
11    #[error(
12        code = "STACK_INPUT_INVALID",
13        message = "Stack input '{input_id}': {reason}",
14        retryable = "false",
15        internal = "false",
16        http_status_code = 400
17    )]
18    StackInputInvalid { input_id: String, reason: String },
19
20    /// A stack definition field written in authoring shorthand cannot be resolved.
21    #[error(
22        code = "STACK_DEFINITION_INVALID",
23        message = "Invalid {field}: {reason}",
24        retryable = "false",
25        internal = "false",
26        http_status_code = 400
27    )]
28    StackDefinitionInvalid {
29        /// Authoring path of the field, as the TypeScript SDK names it (for example `dataDisk.size.min`).
30        field: String,
31        /// Why the value cannot be used.
32        reason: String,
33    },
34
35    /// Federation metadata cannot safely identify the intended trust or target.
36    #[error(
37        code = "GCP_FEDERATION_INVALID",
38        message = "Invalid GCP federation field '{field}': {reason}",
39        retryable = "false",
40        internal = "false",
41        http_status_code = 400
42    )]
43    GcpFederationInvalid { field: String, reason: String },
44
45    /// The image's command identity cannot be resolved safely.
46    #[error(
47        code = "SANDBOX_IMAGE_USER_INVALID",
48        message = "Invalid sandbox image USER '{user}': {reason}",
49        retryable = "false",
50        internal = "false",
51        http_status_code = 400
52    )]
53    SandboxImageUserInvalid { user: String, reason: String },
54    /// Policy exceeds the portable target-count or canonical JSON size limit.
55    #[error(
56        code = "SANDBOX_EGRESS_POLICY_TOO_LARGE",
57        message = "Sandbox egress policy exceeds {limit}",
58        retryable = "false",
59        internal = "false",
60        http_status_code = 400
61    )]
62    SandboxEgressPolicyTooLarge { limit: String },
63
64    /// An egress target cannot be enforced safely.
65    #[error(
66        code = "SANDBOX_EGRESS_INVALID",
67        message = "Invalid sandbox egress target '{target}': {reason}",
68        retryable = "false",
69        internal = "false",
70        http_status_code = 400
71    )]
72    SandboxEgressInvalid { target: String, reason: String },
73
74    /// An AWS internet sandbox reached template rendering without the egress gateway.
75    #[error(
76        code = "SANDBOX_INTERNET_GATEWAY_MISSING",
77        message = "Sandbox '{sandbox_id}' declares egress 'internet', which routes through the managed sandbox egress gateway, but this stack has no gateway. Prepare the stack with the deployment preflights before rendering its setup template",
78        retryable = "false",
79        internal = "false"
80    )]
81    SandboxInternetGatewayMissing { sandbox_id: String },
82
83    /// A deployment selected resources outside the release's declared choices.
84    #[error(
85        code = "CONTAINER_RESOURCE_SELECTION_INVALID",
86        message = "Invalid {dimension} selection for container '{resource_id}': {reason}",
87        retryable = "false",
88        internal = "false",
89        http_status_code = 400
90    )]
91    ContainerResourceSelectionInvalid {
92        resource_id: String,
93        dimension: String,
94        reason: String,
95    },
96    /// A fallback error when nothing more specific matches.
97    #[error(
98        code = "GENERIC_ERROR",
99        message = "{message}",
100        retryable = "true",
101        internal = "false"
102    )]
103    GenericError {
104        /// Human-readable description of the error.
105        message: String,
106    },
107
108    /// Resource type mismatch found during stack operations.
109    #[error(
110        code = "UNEXPECTED_RESOURCE_TYPE",
111        message = "Unexpected resource type for resource '{resource_id}': expected {expected}, but got {actual}",
112        retryable = "false",
113        internal = "false"
114    )]
115    UnexpectedResourceType {
116        resource_id: String,
117        expected: ResourceType,
118        actual: ResourceType,
119    },
120
121    /// Attempt to update a resource that does not support updates.
122    #[error(
123        code = "INVALID_RESOURCE_UPDATE",
124        message = "Resource '{resource_id}' cannot be updated: {reason}",
125        retryable = "false",
126        internal = "false"
127    )]
128    InvalidResourceUpdate { resource_id: String, reason: String },
129
130    /// Worker execution timeout is outside the supported range.
131    #[error(
132        code = "WORKER_TIMEOUT_INVALID",
133        message = "Worker timeoutSeconds must be between 1 and {max_timeout_seconds} seconds, got {timeout_seconds}",
134        retryable = "false",
135        internal = "false",
136        http_status_code = 400
137    )]
138    WorkerTimeoutInvalid {
139        timeout_seconds: u32,
140        max_timeout_seconds: u32,
141    },
142
143    /// The resource exists but has not produced any outputs yet.
144    #[error(
145        code = "RESOURCE_HAS_NO_OUTPUTS",
146        message = "Resource '{resource_id}' has no outputs",
147        retryable = "true",
148        internal = "false"
149    )]
150    ResourceHasNoOutputs { resource_id: String },
151
152    /// Requested resource absent from the stack state.
153    #[error(
154        code = "RESOURCE_NOT_FOUND",
155        message = "Resource '{resource_id}' not found in stack state. Available resources: {available_resources:?}",
156        retryable = "false",
157        internal = "false",
158        http_status_code = 404
159    )]
160    ResourceNotFound {
161        resource_id: String,
162        available_resources: Vec<String>,
163    },
164
165    /// Binding configuration is invalid or missing required fields.
166    #[error(
167        code = "BINDING_CONFIG_INVALID",
168        message = "Invalid binding configuration for '{binding_name}': {reason}",
169        retryable = "false",
170        internal = "false"
171    )]
172    BindingConfigInvalid {
173        binding_name: String,
174        reason: String,
175    },
176
177    /// Environment variable for binding is missing.
178    #[error(
179        code = "BINDING_ENV_VAR_MISSING",
180        message = "Missing environment variable '{env_var}' for binding '{binding_name}'",
181        retryable = "false",
182        internal = "false"
183    )]
184    BindingEnvVarMissing {
185        binding_name: String,
186        env_var: String,
187    },
188
189    /// Failed to parse binding JSON from environment variable.
190    #[error(
191        code = "BINDING_JSON_PARSE_FAILED",
192        message = "Failed to parse binding JSON for '{binding_name}': {reason}",
193        retryable = "false",
194        internal = "false"
195    )]
196    BindingJsonParseFailed {
197        binding_name: String,
198        reason: String,
199    },
200
201    /// Unexpected combination of resource statuses when computing stack status.
202    #[error(
203        code = "UNEXPECTED_RESOURCE_STATUS_COMBINATION",
204        message = "Unexpected resource status combination during {operation}: {resource_statuses:?}",
205        retryable = "false",
206        internal = "true"
207    )]
208    UnexpectedResourceStatusCombination {
209        resource_statuses: Vec<String>,
210        operation: String,
211    },
212
213    /// External binding type does not match the resource type.
214    #[error(
215        code = "EXTERNAL_BINDING_TYPE_MISMATCH",
216        message = "External binding type mismatch for resource '{resource_id}': expected {expected}, got {actual}",
217        retryable = "false",
218        internal = "false"
219    )]
220    ExternalBindingTypeMismatch {
221        resource_id: String,
222        expected: String,
223        actual: String,
224    },
225
226    // Presigned request errors
227    /// Presigned request has expired.
228    #[error(
229        code = "PRESIGNED_REQUEST_EXPIRED",
230        message = "Presigned request for '{path}' expired at {expired_at}",
231        retryable = "false",
232        internal = "false"
233    )]
234    PresignedRequestExpired {
235        path: String,
236        expired_at: chrono::DateTime<chrono::Utc>,
237    },
238
239    /// HTTP request failed.
240    #[error(
241        code = "HTTP_REQUEST_FAILED",
242        message = "HTTP {method} request to '{url}' failed",
243        retryable = "true",
244        internal = "false"
245    )]
246    HttpRequestFailed { url: String, method: String },
247
248    /// Operation not supported.
249    #[error(
250        code = "OPERATION_NOT_SUPPORTED",
251        message = "Operation '{operation}' not supported: {reason}",
252        retryable = "false",
253        internal = "false"
254    )]
255    OperationNotSupported { operation: String, reason: String },
256
257    /// Local filesystem error.
258    #[error(
259        code = "LOCAL_FILESYSTEM_ERROR",
260        message = "Local filesystem error for '{path}' during {operation}",
261        retryable = "false",
262        internal = "false"
263    )]
264    LocalFilesystemError { path: String, operation: String },
265
266    /// Feature not enabled.
267    #[error(
268        code = "FEATURE_NOT_ENABLED",
269        message = "Feature '{feature}' is not enabled",
270        retryable = "false",
271        internal = "false"
272    )]
273    FeatureNotEnabled { feature: String },
274
275    /// A resource requires setup capabilities unavailable to automatic management.
276    #[error(
277        code = "AUTOMATIC_SETUP_RESOURCE_UNSUPPORTED",
278        message = "Automatic infrastructure management does not support {resource_type} '{resource_id}' on {platform}; use manual setup",
279        retryable = "false",
280        internal = "false",
281        http_status_code = 400
282    )]
283    AutomaticSetupResourceUnsupported {
284        resource_type: String,
285        resource_id: String,
286        platform: Platform,
287    },
288
289    /// A sandbox capability is unavailable on the target platform.
290    #[error(
291        code = "SANDBOX_CAPABILITY_UNSUPPORTED",
292        message = "Sandbox capability '{capability}' is not supported on {platform}",
293        retryable = "false",
294        internal = "false",
295        http_status_code = 400
296    )]
297    SandboxCapabilityUnsupported {
298        /// The capability that was requested
299        capability: String,
300        /// The platform that does not offer it
301        platform: String,
302    },
303
304    /// The target platform has no sandbox backend at all.
305    #[error(
306        code = "SANDBOX_PLATFORM_UNSUPPORTED",
307        message = "Sandbox is not available on {platform}",
308        retryable = "false",
309        internal = "false",
310        http_status_code = 400
311    )]
312    SandboxPlatformUnsupported {
313        /// The platform with no sandbox backend
314        platform: String,
315    },
316
317    /// A bundle signature or signing key was rejected.
318    #[error(
319        code = "BUNDLE_SIGNATURE_INVALID",
320        message = "Bundle signature invalid: {reason}",
321        retryable = "false",
322        internal = "false",
323        http_status_code = 400
324    )]
325    BundleSignatureInvalid {
326        /// Why the signature or key was rejected
327        reason: String,
328    },
329
330    /// A sandbox capability was refused.
331    #[error(
332        code = "SANDBOX_CAPABILITY_REFUSED",
333        message = "Sandbox capability refused: {reason}",
334        retryable = "false",
335        internal = "false",
336        http_status_code = 403
337    )]
338    SandboxCapabilityRefused {
339        /// Why it was refused. Deliberately coarse — it must not reveal which sandboxes exist.
340        reason: String,
341    },
342
343    /// A sandbox limit is missing or not a valid quantity.
344    #[error(
345        code = "SANDBOX_LIMIT_INVALID",
346        message = "Sandbox '{resource_id}' has an invalid {field} limit '{value}': {reason}",
347        retryable = "false",
348        internal = "false",
349        http_status_code = 400
350    )]
351    SandboxLimitInvalid {
352        /// The sandbox whose limit failed validation
353        resource_id: String,
354        /// Which limit (cpu, memory, disk, maxProcesses)
355        field: String,
356        /// The rejected value
357        value: String,
358        /// Why it was rejected
359        reason: String,
360    },
361
362    // Commands protocol errors
363    /// Invalid Commands envelope.
364    #[error(
365        code = "INVALID_ENVELOPE",
366        message = "Invalid Commands envelope: {message}",
367        retryable = "false",
368        internal = "false"
369    )]
370    InvalidEnvelope {
371        message: String,
372        field: Option<String>,
373    },
374
375    /// A command-enabled resource exists but the deployment has no token to
376    /// authenticate its command transport.
377    #[error(
378        code = "COMMAND_TOKEN_MISSING",
379        message = "Deployment has command-enabled resource '{resource_id}' but no deployment token to authenticate it: {reason}",
380        retryable = "false",
381        internal = "false"
382    )]
383    CommandTokenMissing { resource_id: String, reason: String },
384
385    /// Public URL configuration is invalid.
386    #[error(
387        code = "PUBLIC_URL_INVALID",
388        message = "Invalid public URL for resource '{resource_id}': {reason}",
389        retryable = "false",
390        internal = "false",
391        http_status_code = 400
392    )]
393    PublicUrlInvalid { resource_id: String, reason: String },
394
395    /// JSON serialization failed.
396    #[error(
397        code = "JSON_SERIALIZATION_FAILED",
398        message = "Failed to serialize JSON: {reason}",
399        retryable = "false",
400        internal = "false"
401    )]
402    JsonSerializationFailed { reason: String },
403
404    /// JSON deserialization failed.
405    #[error(
406        code = "JSON_DESERIALIZATION_FAILED",
407        message = "Failed to deserialize JSON: {reason}",
408        retryable = "false",
409        internal = "false"
410    )]
411    JsonDeserializationFailed { reason: String },
412
413    /// Distribution template serialization failed.
414    #[error(
415        code = "TEMPLATE_SERIALIZATION_FAILED",
416        message = "Failed to serialize {format} template: {reason}",
417        retryable = "false",
418        internal = "true"
419    )]
420    TemplateSerializationFailed { format: String, reason: String },
421
422    /// A management ExternalId is not 32 to 128 ASCII letters and digits.
423    #[error(
424        code = "MANAGEMENT_EXTERNAL_ID_INVALID",
425        message = "Management ExternalId must be 32 to 128 ASCII letters and digits (got {length} characters)",
426        retryable = "false",
427        internal = "false",
428        http_status_code = 400
429    )]
430    ManagementExternalIdInvalid {
431        /// Length of the rejected value. The value itself is never echoed.
432        length: usize,
433    },
434
435    /// A role ARN a setup template is told to accept as its managing role is
436    /// not an IAM role ARN.
437    #[error(
438        code = "MANAGING_ROLE_ARN_INVALID",
439        message = "Managing role ARN '{arn}' is not an IAM role ARN (arn:<partition>:iam::<account>:role/<name>)",
440        retryable = "false",
441        internal = "false",
442        http_status_code = 400
443    )]
444    ManagingRoleArnInvalid {
445        /// The rejected ARN.
446        arn: String,
447    },
448
449    /// ImportData did not match the registered typed payload.
450    #[error(
451        code = "IMPORT_DATA_INVALID",
452        message = "Invalid ImportData for resource '{resource_id}' ({resource_type}) on {platform}",
453        retryable = "false",
454        internal = "false",
455        http_status_code = 400
456    )]
457    ImportDataInvalid {
458        resource_id: String,
459        resource_type: ResourceType,
460        platform: Platform,
461    },
462
463    /// No registry implementation exists for a resource/platform pair.
464    #[error(
465        code = "IMPORT_REGISTRATION_MISSING",
466        message = "Missing {registration_kind} registration for resource type '{resource_type}' on {platform}",
467        retryable = "false",
468        internal = "true"
469    )]
470    ImportRegistrationMissing {
471        resource_type: ResourceType,
472        platform: Platform,
473        registration_kind: String,
474    },
475
476    /// ImportData schema serialization failed.
477    #[error(
478        code = "IMPORT_SCHEMA_SERIALIZATION_FAILED",
479        message = "Failed to serialize ImportData schema",
480        retryable = "false",
481        internal = "true"
482    )]
483    ImportSchemaSerializationFailed,
484}
485
486pub type Result<T> = alien_error::Result<T, ErrorData>;