bindy/reconcilers/bind9cluster/
status_helpers.rs1#[allow(clippy::wildcard_imports)]
10use super::types::*;
11
12#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
32pub fn calculate_cluster_status(
33 instances: &[Bind9Instance],
34 namespace: &str,
35 name: &str,
36) -> (i32, i32, Vec<String>, Vec<Condition>) {
37 let instance_count = instances.len() as i32;
39 let instance_names: Vec<String> = instances.iter().map(ResourceExt::name_any).collect();
40
41 let ready_instances = instances
42 .iter()
43 .filter(|instance| {
44 instance
45 .status
46 .as_ref()
47 .and_then(|status| status.conditions.first())
48 .is_some_and(|condition| condition.r#type == "Ready" && condition.status == "True")
49 })
50 .count() as i32;
51
52 info!(
53 "Bind9Cluster {}/{} has {} instances, {} ready",
54 namespace, name, instance_count, ready_instances
55 );
56
57 let mut instance_conditions = Vec::new();
59 for (index, instance) in instances.iter().enumerate() {
60 let instance_name = instance.name_any();
61 let is_instance_ready = instance
62 .status
63 .as_ref()
64 .and_then(|status| status.conditions.first())
65 .is_some_and(|condition| condition.r#type == "Ready" && condition.status == "True");
66
67 let (status, reason, message) = if is_instance_ready {
68 (
69 "True",
70 REASON_READY,
71 format!("Instance {instance_name} is ready"),
72 )
73 } else {
74 (
75 "False",
76 REASON_NOT_READY,
77 format!("Instance {instance_name} is not ready"),
78 )
79 };
80
81 instance_conditions.push(Condition {
82 r#type: bind9_instance_condition_type(index),
83 status: status.to_string(),
84 reason: Some(reason.to_string()),
85 message: Some(message),
86 last_transition_time: Some(Utc::now().to_rfc3339()),
87 });
88 }
89
90 let (encompassing_status, encompassing_reason, encompassing_message) = if instance_count == 0 {
92 debug!("No instances found for cluster");
93 (
94 "False",
95 REASON_NO_CHILDREN,
96 "No instances found for this cluster".to_string(),
97 )
98 } else if ready_instances == instance_count {
99 debug!("All instances ready");
100 (
101 "True",
102 REASON_ALL_READY,
103 format!("All {instance_count} instances are ready"),
104 )
105 } else if ready_instances > 0 {
106 debug!(ready_instances, instance_count, "Cluster progressing");
107 (
108 "False",
109 REASON_PARTIALLY_READY,
110 format!("{ready_instances}/{instance_count} instances are ready"),
111 )
112 } else {
113 debug!("Waiting for instances to become ready");
114 (
115 "False",
116 REASON_NOT_READY,
117 "No instances are ready".to_string(),
118 )
119 };
120
121 let encompassing_condition = Condition {
122 r#type: CONDITION_TYPE_READY.to_string(),
123 status: encompassing_status.to_string(),
124 reason: Some(encompassing_reason.to_string()),
125 message: Some(encompassing_message.clone()),
126 last_transition_time: Some(Utc::now().to_rfc3339()),
127 };
128
129 let mut all_conditions = vec![encompassing_condition];
131 all_conditions.extend(instance_conditions);
132
133 debug!(
134 status = %encompassing_status,
135 message = %encompassing_message,
136 num_conditions = all_conditions.len(),
137 "Determined cluster status"
138 );
139
140 (
141 instance_count,
142 ready_instances,
143 instance_names,
144 all_conditions,
145 )
146}
147
148#[must_use]
170pub fn cluster_status_changed(
171 current: Option<&Bind9ClusterStatus>,
172 conditions: &[Condition],
173 instance_count: i32,
174 ready_instances: i32,
175 instances: &[String],
176 generation: Option<i64>,
177) -> bool {
178 let Some(current) = current else {
179 return true;
181 };
182
183 if current.instance_count != Some(instance_count)
185 || current.ready_instances != Some(ready_instances)
186 || current.instances != instances
187 {
188 return true;
189 }
190
191 if current.observed_generation != generation {
196 return true;
197 }
198
199 if current.conditions.len() != conditions.len() {
201 return true;
202 }
203
204 current
205 .conditions
206 .iter()
207 .zip(conditions.iter())
208 .any(|(current_cond, new_cond)| {
209 current_cond.r#type != new_cond.r#type
210 || current_cond.status != new_cond.status
211 || current_cond.message != new_cond.message
212 || current_cond.reason != new_cond.reason
213 })
214}
215
216pub(super) async fn update_status(
234 client: &Client,
235 cluster: &Bind9Cluster,
236 conditions: Vec<Condition>,
237 instance_count: i32,
238 ready_instances: i32,
239 instances: Vec<String>,
240) -> Result<()> {
241 let api: Api<Bind9Cluster> =
242 Api::namespaced(client.clone(), &cluster.namespace().unwrap_or_default());
243
244 let status_changed = cluster_status_changed(
246 cluster.status.as_ref(),
247 &conditions,
248 instance_count,
249 ready_instances,
250 &instances,
251 cluster.metadata.generation,
252 );
253
254 if !status_changed {
256 debug!(
257 namespace = %cluster.namespace().unwrap_or_default(),
258 name = %cluster.name_any(),
259 "Status unchanged, skipping update"
260 );
261 info!(
262 "Bind9Cluster {}/{} status unchanged, skipping update",
263 cluster.namespace().unwrap_or_default(),
264 cluster.name_any()
265 );
266 return Ok(());
267 }
268
269 debug!(
270 instance_count,
271 ready_instances,
272 instances_count = instances.len(),
273 num_conditions = conditions.len(),
274 "Preparing status update"
275 );
276
277 let new_status = Bind9ClusterStatus {
278 conditions,
279 observed_generation: cluster.metadata.generation,
280 instance_count: Some(instance_count),
281 ready_instances: Some(ready_instances),
282 instances,
283 };
284
285 info!(
286 "Updating Bind9Cluster {}/{} status: {} instances, {} ready",
287 cluster.namespace().unwrap_or_default(),
288 cluster.name_any(),
289 instance_count,
290 ready_instances
291 );
292
293 let patch = json!({ "status": new_status });
294 api.patch_status(
295 &cluster.name_any(),
296 &PatchParams::apply("bindy-controller"),
297 &Patch::Merge(&patch),
298 )
299 .await?;
300
301 Ok(())
302}
303
304#[cfg(test)]
305#[path = "status_helpers_tests.rs"]
306mod status_helpers_tests;