Skip to main content

alien_core/events/
event.rs

1use crate::events::{EventBus, EventHandle, EventState};
2use crate::{Result, StackState};
3use alien_error::{AlienError, AlienErrorData};
4use serde::{Deserialize, Serialize};
5#[cfg(feature = "openapi")]
6use utoipa::ToSchema;
7
8/// Progress information for image push operations
9#[derive(Serialize, Deserialize, Debug, Clone)]
10#[cfg_attr(feature = "openapi", derive(ToSchema))]
11#[serde(rename_all = "camelCase")]
12pub struct PushProgress {
13    /// Current operation being performed
14    pub operation: String,
15    /// Number of layers uploaded so far
16    pub layers_uploaded: usize,
17    /// Total number of layers to upload
18    pub total_layers: usize,
19    /// Bytes uploaded so far
20    pub bytes_uploaded: u64,
21    /// Total bytes to upload
22    pub total_bytes: u64,
23}
24
25/// Represents all possible events in the Alien system
26#[derive(Serialize, Deserialize, Debug, Clone)]
27#[cfg_attr(feature = "openapi", derive(ToSchema))]
28#[serde(tag = "type")]
29pub enum AlienEvent {
30    // ============================================================================
31    // General Events
32    // ============================================================================
33    /// Loading configuration file
34    #[serde(rename_all = "camelCase")]
35    LoadingConfiguration,
36
37    /// Operation finished successfully
38    #[serde(rename_all = "camelCase")]
39    Finished,
40
41    // ============================================================================
42    // Alien Build Events
43    // ============================================================================
44    /// Stack packaging event
45    #[serde(rename_all = "camelCase")]
46    BuildingStack {
47        /// Name of the stack being built
48        stack: String,
49    },
50
51    /// Running build-time preflight checks and mutations
52    #[serde(rename_all = "camelCase")]
53    RunningPreflights {
54        /// Name of the stack being checked
55        stack: String,
56        /// Platform being targeted
57        platform: String,
58        /// Non-blocking diagnostics reported by the completed checks.
59        #[serde(default, skip_serializing_if = "Vec::is_empty")]
60        warnings: Vec<String>,
61    },
62
63    /// Downloading alien runtime event
64    #[serde(rename_all = "camelCase")]
65    DownloadingAlienRuntime {
66        /// Target triple for the runtime
67        target_triple: String,
68        /// URL being downloaded from
69        url: String,
70    },
71
72    /// Resource build event (function, container, or worker)
73    #[serde(rename_all = "camelCase")]
74    BuildingResource {
75        /// Name of the resource being built
76        resource_name: String,
77        /// Type of the resource: "worker", "container"
78        resource_type: String,
79        /// All resource names sharing this build (for deduped container groups)
80        #[serde(default, skip_serializing_if = "Vec::is_empty")]
81        related_resources: Vec<String>,
82    },
83
84    /// Image build event
85    #[serde(rename_all = "camelCase")]
86    BuildingImage {
87        /// Name of the image being built
88        image: String,
89    },
90
91    /// Image push event
92    #[serde(rename_all = "camelCase")]
93    PushingImage {
94        /// Name of the image being pushed
95        image: String,
96        /// Progress information for the push operation
97        progress: Option<PushProgress>,
98    },
99
100    /// Pushing stack images to registry
101    #[serde(rename_all = "camelCase")]
102    PushingStack {
103        /// Name of the stack being pushed
104        stack: String,
105        /// Target platform
106        platform: String,
107        /// Human-readable destination for pushed images
108        #[serde(default, skip_serializing_if = "Option::is_none")]
109        destination: Option<String>,
110    },
111
112    /// Pushing resource images to registry
113    #[serde(rename_all = "camelCase")]
114    PushingResource {
115        /// Name of the resource being pushed
116        resource_name: String,
117        /// Type of the resource: "worker", "container"
118        resource_type: String,
119    },
120
121    /// Creating a release on the platform
122    #[serde(rename_all = "camelCase")]
123    CreatingRelease {
124        /// Project name
125        project: String,
126    },
127
128    /// Code compilation event (rust, typescript, etc.)
129    #[serde(rename_all = "camelCase")]
130    CompilingCode {
131        /// Language being compiled (rust, typescript, etc.)
132        language: String,
133        /// Current progress/status line from the build output
134        progress: Option<String>,
135    },
136
137    // ============================================================================
138    // Alien Infra Events
139    // ============================================================================
140    #[serde(rename_all = "camelCase")]
141    StackStep {
142        /// The resulting state of the stack after the step.
143        next_state: StackState,
144        /// An suggested duration to wait before executing the next step.
145        suggested_delay_ms: Option<u64>,
146    },
147
148    /// Generating CloudFormation template
149    #[serde(rename_all = "camelCase")]
150    GeneratingCloudFormationTemplate,
151
152    /// Generating infrastructure template
153    #[serde(rename_all = "camelCase")]
154    GeneratingTemplate {
155        /// Platform for which the template is being generated
156        platform: String,
157    },
158
159    // ============================================================================
160    // Agent Events
161    // ============================================================================
162    /// Provisioning a new agent
163    #[serde(rename_all = "camelCase")]
164    ProvisioningAgent {
165        /// ID of the agent being provisioned
166        agent_id: String,
167        /// ID of the release being deployed to the agent
168        release_id: String,
169    },
170
171    /// Updating an existing agent
172    #[serde(rename_all = "camelCase")]
173    UpdatingAgent {
174        /// ID of the agent being updated
175        agent_id: String,
176        /// ID of the new release being deployed to the agent
177        release_id: String,
178    },
179
180    /// Deleting an agent
181    #[serde(rename_all = "camelCase")]
182    DeletingAgent {
183        /// ID of the agent being deleted
184        agent_id: String,
185        /// ID of the release that was running on the agent
186        release_id: String,
187    },
188
189    /// Starting a debug session for an agent
190    #[serde(rename_all = "camelCase")]
191    DebuggingAgent {
192        /// ID of the agent being debugged
193        agent_id: String,
194        /// ID of the debug session
195        debug_session_id: String,
196    },
197
198    // ============================================================================
199    // Alien Test Events (General)
200    // ============================================================================
201    /// Preparing environment for deployment
202    #[serde(rename_all = "camelCase")]
203    PreparingEnvironment {
204        /// Name of the deployment strategy being used
205        strategy_name: String,
206    },
207
208    /// Deploying stack with alien-infra
209    #[serde(rename_all = "camelCase")]
210    DeployingStack {
211        /// Name of the stack being deployed
212        stack_name: String,
213    },
214
215    /// Running test function after deployment
216    #[serde(rename_all = "camelCase")]
217    RunningTestWorker {
218        /// Name of the stack being tested
219        stack_name: String,
220    },
221
222    /// Cleaning up deployed stack resources
223    #[serde(rename_all = "camelCase")]
224    CleaningUpStack {
225        /// Name of the stack being cleaned up
226        stack_name: String,
227        /// Name of the deployment strategy being used for cleanup
228        strategy_name: String,
229    },
230
231    /// Cleaning up deployment environment
232    #[serde(rename_all = "camelCase")]
233    CleaningUpEnvironment {
234        /// Name of the stack being cleaned up
235        stack_name: String,
236        /// Name of the deployment strategy being used for cleanup
237        strategy_name: String,
238    },
239
240    /// Setting up platform context
241    #[serde(rename_all = "camelCase")]
242    SettingUpPlatformContext {
243        /// Name of the platform (e.g., "AWS", "GCP")
244        platform_name: String,
245    },
246
247    // ============================================================================
248    // Alien Test Events (AWS-specific)
249    // ============================================================================
250    /// Ensuring docker repository exists
251    #[serde(rename_all = "camelCase")]
252    EnsuringDockerRepository {
253        /// Name of the docker repository
254        repository_name: String,
255    },
256
257    /// Deploying CloudFormation stack
258    #[serde(rename_all = "camelCase")]
259    DeployingCloudFormationStack {
260        /// Name of the CloudFormation stack
261        cfn_stack_name: String,
262        /// Current stack status
263        current_status: String,
264    },
265
266    /// Assuming AWS IAM role
267    #[serde(rename_all = "camelCase")]
268    AssumingRole {
269        /// ARN of the role to assume
270        role_arn: String,
271    },
272
273    /// Importing stack state from CloudFormation
274    #[serde(rename_all = "camelCase")]
275    ImportingStackStateFromCloudFormation {
276        /// Name of the CloudFormation stack
277        cfn_stack_name: String,
278    },
279
280    /// Deleting CloudFormation stack
281    #[serde(rename_all = "camelCase")]
282    DeletingCloudFormationStack {
283        /// Name of the CloudFormation stack
284        cfn_stack_name: String,
285        /// Current stack status
286        current_status: String,
287    },
288
289    /// Emptying S3 buckets before stack deletion
290    #[serde(rename_all = "camelCase")]
291    EmptyingBuckets {
292        /// Names of the S3 buckets being emptied
293        bucket_names: Vec<String>,
294    },
295
296    // ============================================================================
297    // Events just for testing this module
298    // ============================================================================
299    #[cfg(test)]
300    #[serde(rename_all = "camelCase")]
301    TestBuildingStack { stack: String },
302
303    #[cfg(test)]
304    #[serde(rename_all = "camelCase")]
305    TestBuildingImage { image: String },
306
307    #[cfg(test)]
308    #[serde(rename_all = "camelCase")]
309    TestBuildImage { image: String, stage: String },
310
311    #[cfg(test)]
312    #[serde(rename_all = "camelCase")]
313    TestPushImage { image: String },
314
315    #[cfg(test)]
316    #[serde(rename_all = "camelCase")]
317    TestCreatingResource {
318        resource_type: String,
319        resource_name: String,
320        details: Option<String>,
321    },
322
323    #[cfg(test)]
324    #[serde(rename_all = "camelCase")]
325    TestDeployingStack { stack: String },
326
327    #[cfg(test)]
328    #[serde(rename_all = "camelCase")]
329    TestPerformingHealthCheck { target: String, check_type: String },
330}
331
332impl AlienEvent {
333    /// Emit this event and wait for it to be handled
334    pub async fn emit(self) -> Result<EventHandle> {
335        EventBus::emit(self, None, EventState::None).await
336    }
337
338    /// Emit this event with a specific initial state
339    pub async fn emit_with_state(self, state: EventState) -> Result<EventHandle> {
340        EventBus::emit(self, None, state).await
341    }
342
343    /// Emit this event as a child of another event
344    pub async fn emit_with_parent(self, parent_id: &str) -> Result<EventHandle> {
345        EventBus::emit(self, Some(parent_id.to_string()), EventState::None).await
346    }
347
348    /// Start a scoped event that will track success/failure
349    /// All events emitted within the scope will automatically be children of this event
350    pub async fn in_scope<F, Fut, T, E>(self, f: F) -> std::result::Result<T, AlienError<E>>
351    where
352        F: FnOnce(EventHandle) -> Fut,
353        Fut: std::future::Future<Output = std::result::Result<T, AlienError<E>>>,
354        E: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
355    {
356        let handle = match EventBus::emit(self, None, EventState::Started).await {
357            Ok(handle) => handle,
358            Err(e) => {
359                // If we can't emit the event, we still want to run the function
360                // but without event tracking. The error from emit is logged here.
361                // This behavior is expected by `test_handler_failure_in_scoped_event`.
362                eprintln!("Failed to emit event, continuing with no-op handle: {}", e);
363                EventHandle::noop()
364            }
365        };
366
367        // Establish parent context so all events emitted within the scope become children
368        let result = handle.as_parent(|_| f(handle.clone())).await;
369
370        match result {
371            Ok(result) => {
372                let _ = handle.complete().await; // Ignore errors in completion
373                Ok(result)
374            }
375            Err(err) => {
376                let _ = handle.fail(err.clone()).await; // Ignore errors in failure
377
378                // Return the original error
379                Err(err)
380            }
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_event_serialization() {
391        let event = AlienEvent::BuildingStack {
392            stack: "test-stack".to_string(),
393        };
394
395        let json = serde_json::to_string(&event).unwrap();
396        assert!(json.contains("\"type\":\"BuildingStack\""));
397        assert!(json.contains("\"stack\":\"test-stack\""));
398
399        let deserialized: AlienEvent = serde_json::from_str(&json).unwrap();
400        match deserialized {
401            AlienEvent::BuildingStack { stack } => assert_eq!(stack, "test-stack"),
402            _ => panic!("Wrong event type"),
403        }
404
405        // Test that field names are camelCase
406        let event_with_snake_case = AlienEvent::DownloadingAlienRuntime {
407            target_triple: "x86_64-unknown-linux-gnu".to_string(),
408            url: "https://example.com".to_string(),
409        };
410
411        let json = serde_json::to_string(&event_with_snake_case).unwrap();
412        assert!(json.contains("\"type\":\"DownloadingAlienRuntime\""));
413        assert!(json.contains("\"targetTriple\":\"x86_64-unknown-linux-gnu\""));
414        assert!(json.contains("\"url\":\"https://example.com\""));
415
416        let deserialized: AlienEvent = serde_json::from_str(&json).unwrap();
417        match deserialized {
418            AlienEvent::DownloadingAlienRuntime { target_triple, url } => {
419                assert_eq!(target_triple, "x86_64-unknown-linux-gnu");
420                assert_eq!(url, "https://example.com");
421            }
422            _ => panic!("Wrong event type"),
423        }
424    }
425    #[test]
426    fn preflight_events_accept_legacy_payloads_and_preserve_warnings() {
427        let legacy = serde_json::json!({
428            "type": "RunningPreflights", "stack": "example", "platform": "aws"
429        });
430        let event: AlienEvent = serde_json::from_value(legacy.clone()).unwrap();
431        assert!(
432            matches!(&event, AlienEvent::RunningPreflights { warnings, .. } if warnings.is_empty())
433        );
434        assert_eq!(serde_json::to_value(event).unwrap(), legacy);
435
436        let mut completed = legacy;
437        completed["warnings"] = serde_json::json!(["Check the optional binding"]);
438        let event: AlienEvent = serde_json::from_value(completed.clone()).unwrap();
439        assert!(
440            matches!(&event, AlienEvent::RunningPreflights { warnings, .. }
441            if warnings == &["Check the optional binding"])
442        );
443        assert_eq!(serde_json::to_value(event).unwrap(), completed);
444    }
445}