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