bindy/reconcilers/dnszone/
primary.rs1use anyhow::{anyhow, Result};
14use k8s_openapi::api::core::v1::Pod;
15use kube::{api::ListParams, Api, Client};
16use tracing::{debug, error, info, warn};
17
18use super::helpers::{get_endpoint, load_rndc_key};
19use super::types::PodInfo;
20use crate::bind9::RndcKeyData;
21
22pub async fn filter_primary_instances(
37 client: &Client,
38 instance_refs: &[crate::crd::InstanceReference],
39) -> Result<Vec<crate::crd::InstanceReference>> {
40 use crate::crd::{Bind9Instance, ServerRole};
41
42 let mut primary_refs = Vec::new();
43
44 for instance_ref in instance_refs {
45 let instance_api: Api<Bind9Instance> =
46 Api::namespaced(client.clone(), &instance_ref.namespace);
47
48 match instance_api.get(&instance_ref.name).await {
49 Ok(instance) => {
50 if instance.spec.role == ServerRole::Primary {
51 primary_refs.push(instance_ref.clone());
52 }
53 }
54 Err(e) => {
55 warn!(
56 "Failed to get instance {}/{}: {}. Skipping.",
57 instance_ref.namespace, instance_ref.name, e
58 );
59 }
60 }
61 }
62
63 Ok(primary_refs)
64}
65
66pub async fn find_all_primary_pods(
86 client: &Client,
87 namespace: &str,
88 cluster_name: &str,
89 is_cluster_provider: bool,
90) -> Result<Vec<PodInfo>> {
91 use crate::crd::{Bind9Instance, ServerRole};
92
93 let instance_api: Api<Bind9Instance> = if is_cluster_provider {
95 Api::all(client.clone())
96 } else {
97 Api::namespaced(client.clone(), namespace)
98 };
99 let instances = instance_api.list(&ListParams::default()).await?;
100
101 let mut primary_instances: Vec<(String, String)> = Vec::new();
103 for instance in instances.items {
104 if instance.spec.cluster_ref == cluster_name && instance.spec.role == ServerRole::Primary {
105 if let (Some(name), Some(ns)) = (instance.metadata.name, instance.metadata.namespace) {
106 primary_instances.push((name, ns));
107 }
108 }
109 }
110
111 if primary_instances.is_empty() {
112 let search_scope = if is_cluster_provider {
113 "all namespaces".to_string()
114 } else {
115 format!("namespace {namespace}")
116 };
117 return Err(anyhow!(
118 "No PRIMARY Bind9Instance resources found for cluster {cluster_name} in {search_scope}"
119 ));
120 }
121
122 info!(
123 "Found {} PRIMARY instance(s) for cluster {}: {:?}",
124 primary_instances.len(),
125 cluster_name,
126 primary_instances
127 );
128
129 let mut all_pod_infos = Vec::new();
130
131 for (instance_name, instance_namespace) in &primary_instances {
132 let pod_api: Api<Pod> = Api::namespaced(client.clone(), instance_namespace);
134 let label_selector = format!("app=bind9,instance={instance_name}");
136 let lp = ListParams::default().labels(&label_selector);
137
138 let pods = pod_api.list(&lp).await?;
139
140 debug!(
141 "Found {} pod(s) for PRIMARY instance {}",
142 pods.items.len(),
143 instance_name
144 );
145
146 for pod in &pods.items {
147 let Some((pod_name, pod_ip)) = super::helpers::running_pod_name_and_ip(pod) else {
151 continue;
152 };
153
154 all_pod_infos.push(PodInfo {
155 name: pod_name.clone(),
156 ip: pod_ip.clone(),
157 instance_name: instance_name.clone(),
158 namespace: instance_namespace.clone(),
159 });
160 debug!(
161 "Found running pod {} with IP {} in namespace {}",
162 pod_name, pod_ip, instance_namespace
163 );
164 }
165 }
166
167 if all_pod_infos.is_empty() {
168 return Err(anyhow!(
169 "No running PRIMARY pods found for cluster {cluster_name} in namespace {namespace}"
170 ));
171 }
172
173 info!(
174 "Found {} running PRIMARY pod(s) across {} instance(s) for cluster {}",
175 all_pod_infos.len(),
176 primary_instances.len(),
177 cluster_name
178 );
179
180 Ok(all_pod_infos)
181}
182
183pub async fn find_primary_ips_from_instances(
201 client: &Client,
202 instance_refs: &[crate::crd::InstanceReference],
203) -> Result<Vec<String>> {
204 use crate::crd::{Bind9Instance, ServerRole};
205 use k8s_openapi::api::core::v1::Pod;
206
207 info!(
208 "Finding PRIMARY pod IPs from {} instance reference(s)",
209 instance_refs.len()
210 );
211
212 let mut primary_ips = Vec::new();
213
214 for instance_ref in instance_refs {
215 let instance_api: Api<Bind9Instance> =
217 Api::namespaced(client.clone(), &instance_ref.namespace);
218
219 let instance = match instance_api.get(&instance_ref.name).await {
220 Ok(inst) => inst,
221 Err(e) => {
222 warn!(
223 "Failed to get instance {}/{}: {}",
224 instance_ref.namespace, instance_ref.name, e
225 );
226 continue;
227 }
228 };
229
230 if instance.spec.role != ServerRole::Primary {
232 continue;
233 }
234
235 let pod_api: Api<Pod> = Api::namespaced(client.clone(), &instance_ref.namespace);
237 let label_selector = format!("app=bind9,instance={}", instance_ref.name);
238 let lp = ListParams::default().labels(&label_selector);
239
240 match pod_api.list(&lp).await {
241 Ok(pods) => {
242 for pod in pods.items {
243 if let Some(pod_ip) = pod.status.as_ref().and_then(|s| s.pod_ip.as_ref()) {
244 let phase = pod
246 .status
247 .as_ref()
248 .and_then(|s| s.phase.as_ref())
249 .map_or("Unknown", std::string::String::as_str);
250
251 if phase == "Running" {
252 primary_ips.push(pod_ip.clone());
253 debug!(
254 "Added IP {} from running PRIMARY pod {} (instance {}/{})",
255 pod_ip,
256 pod.metadata.name.as_ref().unwrap_or(&"unknown".to_string()),
257 instance_ref.namespace,
258 instance_ref.name
259 );
260 }
261 }
262 }
263 }
264 Err(e) => {
265 warn!(
266 "Failed to list pods for PRIMARY instance {}/{}: {}",
267 instance_ref.namespace, instance_ref.name, e
268 );
269 }
270 }
271 }
272
273 info!(
274 "Found total of {} PRIMARY pod IP(s) across all instances: {:?}",
275 primary_ips.len(),
276 primary_ips
277 );
278
279 Ok(primary_ips)
280}
281pub async fn for_each_primary_endpoint<F, Fut>(
316 client: &Client,
317 namespace: &str,
318 cluster_ref: &str,
319 is_cluster_provider: bool,
320 with_rndc_key: bool,
321 port_name: &str,
322 operation: F,
323) -> Result<(Option<String>, usize)>
324where
325 F: Fn(String, String, Option<RndcKeyData>) -> Fut,
326 Fut: std::future::Future<Output = Result<()>>,
327{
328 let primary_pods =
330 find_all_primary_pods(client, namespace, cluster_ref, is_cluster_provider).await?;
331
332 info!(
333 "Found {} PRIMARY pod(s) for cluster {}",
334 primary_pods.len(),
335 cluster_ref
336 );
337
338 let mut instance_tuples: Vec<(String, String)> = primary_pods
341 .iter()
342 .map(|pod| (pod.instance_name.clone(), pod.namespace.clone()))
343 .collect();
344 instance_tuples.sort();
345 instance_tuples.dedup();
346
347 info!(
348 "Found {} primary instance(s) for cluster {}: {:?}",
349 instance_tuples.len(),
350 cluster_ref,
351 instance_tuples
352 );
353
354 let mut first_endpoint: Option<String> = None;
355 let mut total_endpoints = 0;
356 let mut errors: Vec<String> = Vec::new();
357
358 for (instance_name, instance_namespace) in &instance_tuples {
362 info!(
363 "Getting endpoints for instance {}/{} in cluster {}",
364 instance_namespace, instance_name, cluster_ref
365 );
366
367 let key_data = if with_rndc_key {
370 Some(load_rndc_key(client, instance_namespace, instance_name).await?)
371 } else {
372 None
373 };
374
375 let endpoints = get_endpoint(client, instance_namespace, instance_name, port_name).await?;
378
379 info!(
380 "Found {} endpoint(s) for instance {}",
381 endpoints.len(),
382 instance_name
383 );
384
385 for endpoint in &endpoints {
386 let pod_endpoint = format!("{}:{}", endpoint.ip, endpoint.port);
387
388 if first_endpoint.is_none() {
390 first_endpoint = Some(pod_endpoint.clone());
391 }
392
393 if let Err(e) = operation(
396 pod_endpoint.clone(),
397 instance_name.clone(),
398 key_data.clone(),
399 )
400 .await
401 {
402 error!(
403 "Failed operation on endpoint {} (instance {}): {}",
404 pod_endpoint, instance_name, e
405 );
406 errors.push(format!(
407 "endpoint {pod_endpoint} (instance {instance_name}): {e}"
408 ));
409 } else {
410 total_endpoints += 1;
411 }
412 }
413 }
414
415 if !errors.is_empty() {
417 return Err(anyhow::anyhow!(
418 "Failed to process {} endpoint(s): {}",
419 errors.len(),
420 errors.join("; ")
421 ));
422 }
423
424 Ok((first_endpoint, total_endpoints))
425}
426
427#[cfg(test)]
428#[path = "primary_tests.rs"]
429mod primary_tests;