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#[derive(Serialize, Deserialize, Debug, Clone)]
10#[cfg_attr(feature = "openapi", derive(ToSchema))]
11#[serde(rename_all = "camelCase")]
12pub struct PushProgress {
13 pub operation: String,
15 pub layers_uploaded: usize,
17 pub total_layers: usize,
19 pub bytes_uploaded: u64,
21 pub total_bytes: u64,
23}
24
25#[derive(Serialize, Deserialize, Debug, Clone)]
27#[cfg_attr(feature = "openapi", derive(ToSchema))]
28#[serde(tag = "type")]
29pub enum AlienEvent {
30 #[serde(rename_all = "camelCase")]
35 LoadingConfiguration,
36
37 #[serde(rename_all = "camelCase")]
39 Finished,
40
41 #[serde(rename_all = "camelCase")]
46 BuildingStack {
47 stack: String,
49 },
50
51 #[serde(rename_all = "camelCase")]
53 RunningPreflights {
54 stack: String,
56 platform: String,
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 warnings: Vec<String>,
61 },
62
63 #[serde(rename_all = "camelCase")]
65 DownloadingAlienRuntime {
66 target_triple: String,
68 url: String,
70 },
71
72 #[serde(rename_all = "camelCase")]
74 BuildingResource {
75 resource_name: String,
77 resource_type: String,
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 related_resources: Vec<String>,
82 },
83
84 #[serde(rename_all = "camelCase")]
86 BuildingImage {
87 image: String,
89 },
90
91 #[serde(rename_all = "camelCase")]
93 PushingImage {
94 image: String,
96 progress: Option<PushProgress>,
98 },
99
100 #[serde(rename_all = "camelCase")]
102 PushingStack {
103 stack: String,
105 platform: String,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 destination: Option<String>,
110 },
111
112 #[serde(rename_all = "camelCase")]
114 PushingResource {
115 resource_name: String,
117 resource_type: String,
119 },
120
121 #[serde(rename_all = "camelCase")]
123 CreatingRelease {
124 project: String,
126 },
127
128 #[serde(rename_all = "camelCase")]
130 CompilingCode {
131 language: String,
133 progress: Option<String>,
135 },
136
137 #[serde(rename_all = "camelCase")]
141 StackStep {
142 next_state: StackState,
144 suggested_delay_ms: Option<u64>,
146 },
147
148 #[serde(rename_all = "camelCase")]
150 GeneratingCloudFormationTemplate,
151
152 #[serde(rename_all = "camelCase")]
154 GeneratingTemplate {
155 platform: String,
157 },
158
159 #[serde(rename_all = "camelCase")]
164 ProvisioningAgent {
165 agent_id: String,
167 release_id: String,
169 },
170
171 #[serde(rename_all = "camelCase")]
173 UpdatingAgent {
174 agent_id: String,
176 release_id: String,
178 },
179
180 #[serde(rename_all = "camelCase")]
182 DeletingAgent {
183 agent_id: String,
185 release_id: String,
187 },
188
189 #[serde(rename_all = "camelCase")]
191 DebuggingAgent {
192 agent_id: String,
194 debug_session_id: String,
196 },
197
198 #[serde(rename_all = "camelCase")]
203 PreparingEnvironment {
204 strategy_name: String,
206 },
207
208 #[serde(rename_all = "camelCase")]
210 DeployingStack {
211 stack_name: String,
213 },
214
215 #[serde(rename_all = "camelCase")]
217 RunningTestWorker {
218 stack_name: String,
220 },
221
222 #[serde(rename_all = "camelCase")]
224 CleaningUpStack {
225 stack_name: String,
227 strategy_name: String,
229 },
230
231 #[serde(rename_all = "camelCase")]
233 CleaningUpEnvironment {
234 stack_name: String,
236 strategy_name: String,
238 },
239
240 #[serde(rename_all = "camelCase")]
242 SettingUpPlatformContext {
243 platform_name: String,
245 },
246
247 #[serde(rename_all = "camelCase")]
252 EnsuringDockerRepository {
253 repository_name: String,
255 },
256
257 #[serde(rename_all = "camelCase")]
259 DeployingCloudFormationStack {
260 cfn_stack_name: String,
262 current_status: String,
264 },
265
266 #[serde(rename_all = "camelCase")]
268 AssumingRole {
269 role_arn: String,
271 },
272
273 #[serde(rename_all = "camelCase")]
275 ImportingStackStateFromCloudFormation {
276 cfn_stack_name: String,
278 },
279
280 #[serde(rename_all = "camelCase")]
282 DeletingCloudFormationStack {
283 cfn_stack_name: String,
285 current_status: String,
287 },
288
289 #[serde(rename_all = "camelCase")]
291 EmptyingBuckets {
292 bucket_names: Vec<String>,
294 },
295
296 #[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 pub async fn emit(self) -> Result<EventHandle> {
335 EventBus::emit(self, None, EventState::None).await
336 }
337
338 pub async fn emit_with_state(self, state: EventState) -> Result<EventHandle> {
340 EventBus::emit(self, None, state).await
341 }
342
343 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 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 eprintln!("Failed to emit event, continuing with no-op handle: {}", e);
363 EventHandle::noop()
364 }
365 };
366
367 let result = handle.as_parent(|_| f(handle.clone())).await;
369
370 match result {
371 Ok(result) => {
372 let _ = handle.complete().await; Ok(result)
374 }
375 Err(err) => {
376 let _ = handle.fail(err.clone()).await; 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 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}