1use std::sync::Arc;
2
3use anyhow::bail;
4use async_trait::async_trait;
5use k8s_openapi::api::batch::v1 as batchv1;
6use kube::Resource;
7use kube::runtime::events;
8use kube::runtime::events::{
9 Event,
10 EventType,
11};
12#[cfg(feature = "mock")]
13use mockall::automock;
14
15use crate::prelude::*;
16
17#[cfg_attr(feature = "mock", automock)]
18#[async_trait]
19trait EventSendable {
20 async fn send_event(&self, event: Event, object_ref: &corev1::ObjectReference) -> EmptyResult;
21}
22
23#[derive(Clone)]
24struct EventSender {
25 recorder: events::Recorder,
26}
27
28#[async_trait]
29impl EventSendable for EventSender {
30 async fn send_event(&self, event: Event, object_ref: &corev1::ObjectReference) -> EmptyResult {
31 Ok(self.recorder.publish(&event, object_ref).await?)
32 }
33}
34
35#[derive(Clone)]
36pub struct SkEventRecorder {
37 sender: Arc<dyn EventSendable + Send + Sync>,
38 sim_object_ref: Option<corev1::ObjectReference>,
39}
40
41
42impl SkEventRecorder {
43 pub fn new(client: kube::Client, controller: String) -> SkEventRecorder {
44 let reporter = events::Reporter { controller, instance: None };
45 let recorder = events::Recorder::new(client, reporter);
46 SkEventRecorder {
47 sender: Arc::new(EventSender { recorder }),
48 sim_object_ref: None,
49 }
50 }
51
52 pub fn with_sim(&self, sim: &Simulation) -> Self {
53 let mut rec = self.clone();
54 rec.sim_object_ref = Some(sim.object_ref(&()).clone());
55 rec
56 }
57
58 pub async fn send_driver_created_event(&self, job: &batchv1::Job) -> EmptyResult {
59 self.send_event(Event {
60 type_: EventType::Normal,
61 reason: "SuccessfulDriverCreate".into(),
62 note: Some(format!("Created driver job: {}", job.namespaced_name())),
63 action: "SimulationPrerequisitesCreated".into(),
64 secondary: None,
65 })
66 .await
67 }
68
69 pub async fn send_driver_pod_failed_event(&self) -> EmptyResult {
70 self.send_event(Event {
71 type_: EventType::Normal,
72 reason: "SimulationDriverFailed".into(),
73 note: Some("Simulation driver pod failed".into()),
74 action: "SimulationDriverRunning".into(),
75 secondary: None,
76 })
77 .await
78 }
79
80 pub async fn send_hook_failed_event(
82 &self,
83 hook_type: &str,
84 hook_cmd_str: &str,
85 exit_code: Option<i32>,
86 ) -> EmptyResult {
87 self.send_event(Event {
88 type_: EventType::Warning,
89 reason: format!("{hook_type}HookFailed"),
90 note: Some(format!(
91 "Simulation hook `{hook_cmd_str}` failed with exit code: {}",
92 exit_code.map(|c| format!("{c}")).unwrap_or("<unknown>".into())
93 )),
94 action: format!("{hook_type}HookExecuted"),
95 secondary: None,
96 })
97 .await
98 }
99
100 pub async fn send_hooks_succeeded_event(&self, hook_type: &str) -> EmptyResult {
101 self.send_event(Event {
102 type_: EventType::Normal,
103 reason: format!("{hook_type}HooksSucceeded"),
104 note: Some("All hooks succeeded".into()),
105 action: format!("{hook_type}HookExecuted"),
106 secondary: None,
107 })
108 .await
109 }
110
111 pub async fn send_sim_blocked_event(&self) -> EmptyResult {
112 self.send_event(Event {
113 type_: EventType::Normal,
114 reason: "Blocked".into(),
115 note: Some("Simulation is blocked from running".into()),
116 action: "OtherSimulationRunning".into(),
117 secondary: None,
118 })
119 .await
120 }
121
122 pub async fn send_sim_cleanup_failed_event(&self) -> EmptyResult {
123 self.send_event(Event {
124 type_: EventType::Warning,
125 reason: "CleanupFailed".into(),
126 note: Some("Simulation cleanup failed; there may be dangling resources".into()),
127 action: "SimulationCleanupInitiated".into(),
128 secondary: None,
129 })
130 .await
131 }
132
133 pub async fn send_sim_started_event(&self, sim_name: &str) -> EmptyResult {
134 self.send_event(Event {
135 type_: EventType::Normal,
136 reason: "SimulationStarted".into(),
137 note: Some(format!("Started running simulation: {}", sim_name)),
138 action: "NewSimulationResourceCreated".into(),
139 secondary: None,
140 })
141 .await
142 }
143
144 pub async fn send_waiting_for_metrics_event(&self) -> EmptyResult {
145 self.send_event(Event {
146 type_: EventType::Normal,
147 reason: "WaitingForMetrics".into(),
148 note: Some("Monitoring pod(s) are not Ready".into()),
149 action: "PrometheusResourceCreated".into(),
150 secondary: None,
151 })
152 .await
153 }
154
155 pub async fn send_waiting_for_secret_event(&self) -> EmptyResult {
156 self.send_event(Event {
157 type_: EventType::Normal,
158 reason: "WaitingForDriverSecret".into(),
159 note: Some("The driver certificate secret is not Created".into()),
160 action: "CertificateRequestCreated".into(),
161 secondary: None,
162 })
163 .await
164 }
165
166 pub async fn send_waiting_for_webhook_cert_event(&self) -> EmptyResult {
167 self.send_event(Event {
168 type_: EventType::Normal,
169 reason: "WaitingForWebhookCertBundle".into(),
170 note: Some("The driver webhook certificate bundle is not injected".into()),
171 action: "CertificateRequestCreated".into(),
172 secondary: None,
173 })
174 .await
175 }
176
177 async fn send_event(&self, event: Event) -> EmptyResult {
178 let Some(object_ref) = &self.sim_object_ref else {
179 bail!("Simulation object reference empty; cannot send event");
180 };
181 self.sender.send_event(event, object_ref).await
182 }
183}
184
185#[cfg(feature = "mock")]
186impl SkEventRecorder {
187 pub fn mock() -> SkEventRecorder {
188 let mut sender = MockEventSendable::new();
189 sender.expect_send_event().returning(|_, _| Ok(()));
190 SkEventRecorder {
191 sender: Arc::new(sender),
192 sim_object_ref: Some(corev1::ObjectReference::default()),
193 }
194 }
195}