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