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