bindy/reconcilers/records/
status_helpers.rs1#[allow(clippy::wildcard_imports)]
7use super::types::*;
8
9pub(super) async fn create_event<T>(
10 client: &Client,
11 record: &T,
12 event_type: &str,
13 reason: &str,
14 message: &str,
15) -> Result<()>
16where
17 T: Resource<DynamicType = ()> + ResourceExt,
18{
19 let namespace = record.namespace().unwrap_or_default();
20 let name = record.name_any();
21 let event_api: Api<Event> = Api::namespaced(client.clone(), &namespace);
22
23 let now = Time(k8s_openapi::jiff::Timestamp::now());
24 let event = Event {
25 metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
26 generate_name: Some(format!("{name}-")),
27 namespace: Some(namespace.clone()),
28 ..Default::default()
29 },
30 involved_object: ObjectReference {
31 api_version: Some(T::api_version(&()).to_string()),
32 kind: Some(T::kind(&()).to_string()),
33 name: Some(name.clone()),
34 namespace: Some(namespace),
35 uid: record.meta().uid.clone(),
36 ..Default::default()
37 },
38 reason: Some(reason.to_string()),
39 message: Some(message.to_string()),
40 type_: Some(event_type.to_string()),
41 first_timestamp: Some(now.clone()),
42 last_timestamp: Some(now),
43 count: Some(1),
44 ..Default::default()
45 };
46
47 match event_api.create(&PostParams::default(), &event).await {
48 Ok(_) => Ok(()),
49 Err(e) => {
50 warn!("Failed to create event for {}: {}", name, e);
51 Ok(()) }
53 }
54}
55
56#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
80pub(super) async fn update_record_status<T>(
81 client: &Client,
82 record: &T,
83 condition_type: &str,
84 status: &str,
85 reason: &str,
86 message: &str,
87 observed_generation: Option<i64>,
88 record_hash: Option<String>,
89 last_updated: Option<String>,
90 addresses: Option<String>,
91 published_name: Option<String>,
92) -> Result<()>
93where
94 T: Resource<DynamicType = (), Scope = k8s_openapi::NamespaceResourceScope>
95 + ResourceExt
96 + Clone
97 + std::fmt::Debug
98 + serde::Serialize
99 + for<'de> serde::Deserialize<'de>,
100{
101 let namespace = record.namespace().unwrap_or_default();
102 let name = record.name_any();
103 let api: Api<T> = Api::namespaced(client.clone(), &namespace);
104
105 let current = api
107 .get(&name)
108 .await
109 .context("Failed to fetch current resource")?;
110
111 let current_json = serde_json::to_value(¤t)?;
114 let needs_update = if let Some(current_status) = current_json.get("status") {
115 if let Some(observed_gen) = current_status.get("observedGeneration") {
116 if observed_gen == &json!(record.meta().generation) {
118 if let Some(conditions) =
119 current_status.get("conditions").and_then(|c| c.as_array())
120 {
121 let matching_condition = conditions.iter().find(|cond| {
123 cond.get("type").and_then(|t| t.as_str()) == Some(condition_type)
124 });
125
126 if let Some(cond) = matching_condition {
127 let status_matches =
128 cond.get("status").and_then(|s| s.as_str()) == Some(status);
129 let reason_matches =
130 cond.get("reason").and_then(|r| r.as_str()) == Some(reason);
131 let message_matches =
132 cond.get("message").and_then(|m| m.as_str()) == Some(message);
133 !(status_matches && reason_matches && message_matches)
135 } else {
136 true }
138 } else {
139 true }
141 } else {
142 true }
144 } else {
145 true }
147 } else {
148 true };
150
151 if !needs_update {
152 return Ok(());
154 }
155
156 let last_transition_time = if let Some(current_status) = current_json.get("status") {
158 if let Some(conditions) = current_status.get("conditions").and_then(|c| c.as_array()) {
159 let matching_condition = conditions
161 .iter()
162 .find(|cond| cond.get("type").and_then(|t| t.as_str()) == Some(condition_type));
163
164 if let Some(cond) = matching_condition {
165 let status_changed = cond.get("status").and_then(|s| s.as_str()) != Some(status);
166 if status_changed {
167 Utc::now().to_rfc3339()
169 } else {
170 cond.get("lastTransitionTime")
172 .and_then(|t| t.as_str())
173 .unwrap_or(&Utc::now().to_rfc3339())
174 .to_string()
175 }
176 } else {
177 Utc::now().to_rfc3339()
179 }
180 } else {
181 Utc::now().to_rfc3339()
182 }
183 } else {
184 Utc::now().to_rfc3339()
185 };
186
187 let condition = Condition {
188 r#type: condition_type.to_string(),
189 status: status.to_string(),
190 reason: Some(reason.to_string()),
191 message: Some(message.to_string()),
192 last_transition_time: Some(last_transition_time),
193 };
194
195 let zone = current_json
197 .get("status")
198 .and_then(|s| s.get("zone"))
199 .and_then(|z| z.as_str())
200 .map(ToString::to_string);
201
202 let zone_ref = current_json
204 .get("status")
205 .and_then(|s| s.get("zoneRef"))
206 .and_then(|z| serde_json::from_value::<crate::crd::ZoneReference>(z.clone()).ok());
207
208 let status_addresses = addresses.or_else(|| {
210 current_json
211 .get("status")
212 .and_then(|s| s.get("addresses"))
213 .and_then(|a| a.as_str())
214 .map(ToString::to_string)
215 });
216
217 let status_published_name = published_name.or_else(|| {
219 current_json
220 .get("status")
221 .and_then(|s| s.get("publishedName"))
222 .and_then(|p| p.as_str())
223 .map(ToString::to_string)
224 });
225
226 #[allow(deprecated)] let record_status = RecordStatus {
228 conditions: vec![condition],
229 observed_generation: observed_generation.or(record.meta().generation),
230 zone,
231 zone_ref, record_hash,
233 last_updated,
234 addresses: status_addresses, published_name: status_published_name, };
237
238 let status_patch = json!({
239 "status": record_status
240 });
241
242 api.patch_status(&name, &PatchParams::default(), &Patch::Merge(&status_patch))
243 .await
244 .context("Failed to update record status")?;
245
246 info!(
247 "Updated status for {}/{}: {} = {}",
248 namespace, name, condition_type, status
249 );
250
251 let event_type = if status == "True" {
253 "Normal"
254 } else {
255 "Warning"
256 };
257 create_event(client, record, event_type, reason, message).await?;
258
259 Ok(())
260}