bindy/reconcilers/dnszone/
secondary.rs1use 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_secondary_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 secondary_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::Secondary {
51 secondary_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(secondary_refs)
64}
65
66pub async fn find_secondary_pod_ips_from_instances(
85 client: &Client,
86 instance_refs: &[crate::crd::InstanceReference],
87) -> Result<Vec<String>> {
88 use crate::crd::{Bind9Instance, ServerRole};
89 use k8s_openapi::api::core::v1::Pod;
90
91 let mut secondary_ips = Vec::new();
92
93 for instance_ref in instance_refs {
94 let instance_api: Api<Bind9Instance> =
96 Api::namespaced(client.clone(), &instance_ref.namespace);
97
98 let instance = match instance_api.get(&instance_ref.name).await {
99 Ok(inst) => inst,
100 Err(e) => {
101 warn!(
102 "Failed to get Bind9Instance {}/{}: {}. Skipping.",
103 instance_ref.namespace, instance_ref.name, e
104 );
105 continue;
106 }
107 };
108
109 if instance.spec.role != ServerRole::Secondary {
111 debug!(
112 "Skipping instance {}/{} - role is {:?}, not Secondary",
113 instance_ref.namespace, instance_ref.name, instance.spec.role
114 );
115 continue;
116 }
117
118 let pod_api: Api<Pod> = Api::namespaced(client.clone(), &instance_ref.namespace);
120 let label_selector = format!("app=bind9,instance={}", instance_ref.name);
121 let lp = ListParams::default().labels(&label_selector);
122
123 match pod_api.list(&lp).await {
124 Ok(pods) => {
125 for pod in pods.items {
126 if let Some(pod_ip) = pod.status.as_ref().and_then(|s| s.pod_ip.as_ref()) {
127 let phase = pod
129 .status
130 .as_ref()
131 .and_then(|s| s.phase.as_ref())
132 .map_or("Unknown", std::string::String::as_str);
133
134 if phase == "Running" {
135 secondary_ips.push(pod_ip.clone());
136 } else {
137 debug!(
138 "Skipping pod {} in phase {} for instance {}/{}",
139 pod.metadata.name.as_ref().unwrap_or(&"unknown".to_string()),
140 phase,
141 instance_ref.namespace,
142 instance_ref.name
143 );
144 }
145 }
146 }
147 }
148 Err(e) => {
149 warn!(
150 "Failed to list pods for instance {}/{}: {}. Skipping.",
151 instance_ref.namespace, instance_ref.name, e
152 );
153 }
154 }
155 }
156
157 Ok(secondary_ips)
158}
159
160async fn find_all_secondary_pods(
161 client: &Client,
162 namespace: &str,
163 cluster_name: &str,
164 is_cluster_provider: bool,
165) -> Result<Vec<PodInfo>> {
166 use crate::crd::{Bind9Instance, ServerRole};
167
168 let instance_api: Api<Bind9Instance> = if is_cluster_provider {
170 Api::all(client.clone())
171 } else {
172 Api::namespaced(client.clone(), namespace)
173 };
174 let instances = instance_api.list(&ListParams::default()).await?;
175
176 let mut secondary_instances: Vec<(String, String)> = Vec::new();
178 for instance in instances.items {
179 if instance.spec.cluster_ref == cluster_name && instance.spec.role == ServerRole::Secondary
180 {
181 if let (Some(name), Some(ns)) = (instance.metadata.name, instance.metadata.namespace) {
182 secondary_instances.push((name, ns));
183 }
184 }
185 }
186
187 if secondary_instances.is_empty() {
188 info!("No SECONDARY instances found for cluster {cluster_name}");
189 return Ok(Vec::new());
190 }
191
192 info!(
193 "Found {} SECONDARY instance(s) for cluster {}: {:?}",
194 secondary_instances.len(),
195 cluster_name,
196 secondary_instances
197 );
198
199 let mut all_pod_infos = Vec::new();
200
201 for (instance_name, instance_namespace) in &secondary_instances {
202 let pod_api: Api<Pod> = Api::namespaced(client.clone(), instance_namespace);
204 let label_selector = format!("app=bind9,instance={instance_name}");
205 let lp = ListParams::default().labels(&label_selector);
206
207 let pods = pod_api.list(&lp).await?;
208
209 debug!(
210 "Found {} pod(s) for SECONDARY instance {}",
211 pods.items.len(),
212 instance_name
213 );
214
215 for pod in &pods.items {
216 let Some((pod_name, pod_ip)) = super::helpers::running_pod_name_and_ip(pod) else {
220 continue;
221 };
222
223 all_pod_infos.push(PodInfo {
224 name: pod_name.clone(),
225 ip: pod_ip.clone(),
226 instance_name: instance_name.clone(),
227 namespace: instance_namespace.clone(),
228 });
229 debug!(
230 "Found running secondary pod {} with IP {} in namespace {}",
231 pod_name, pod_ip, instance_namespace
232 );
233 }
234 }
235
236 info!(
237 "Found {} running SECONDARY pod(s) across {} instance(s) for cluster {}",
238 all_pod_infos.len(),
239 secondary_instances.len(),
240 cluster_name
241 );
242
243 Ok(all_pod_infos)
244}
245
246pub async fn for_each_secondary_endpoint<F, Fut>(
290 client: &Client,
291 namespace: &str,
292 cluster_ref: &str,
293 is_cluster_provider: bool,
294 with_rndc_key: bool,
295 port_name: &str,
296 operation: F,
297) -> Result<(Option<String>, usize)>
298where
299 F: Fn(String, String, Option<RndcKeyData>) -> Fut,
300 Fut: std::future::Future<Output = Result<()>>,
301{
302 let secondary_pods =
304 find_all_secondary_pods(client, namespace, cluster_ref, is_cluster_provider).await?;
305
306 info!(
307 "Found {} SECONDARY pod(s) for cluster {}",
308 secondary_pods.len(),
309 cluster_ref
310 );
311
312 let mut instance_tuples: Vec<(String, String)> = secondary_pods
315 .iter()
316 .map(|pod| (pod.instance_name.clone(), pod.namespace.clone()))
317 .collect();
318 instance_tuples.sort();
319 instance_tuples.dedup();
320
321 info!(
322 "Found {} secondary instance(s) for cluster {}: {:?}",
323 instance_tuples.len(),
324 cluster_ref,
325 instance_tuples
326 );
327
328 let mut first_endpoint: Option<String> = None;
329 let mut total_endpoints = 0;
330 let mut errors: Vec<String> = Vec::new();
331
332 for (instance_name, instance_namespace) in &instance_tuples {
334 info!(
335 "Getting endpoints for secondary instance {}/{} in cluster {}",
336 instance_namespace, instance_name, cluster_ref
337 );
338
339 let key_data = if with_rndc_key {
342 Some(load_rndc_key(client, instance_namespace, instance_name).await?)
343 } else {
344 None
345 };
346
347 let endpoints = get_endpoint(client, instance_namespace, instance_name, port_name).await?;
350
351 info!(
352 "Found {} endpoint(s) for secondary instance {}",
353 endpoints.len(),
354 instance_name
355 );
356
357 for endpoint in &endpoints {
358 let pod_endpoint = format!("{}:{}", endpoint.ip, endpoint.port);
359
360 if first_endpoint.is_none() {
362 first_endpoint = Some(pod_endpoint.clone());
363 }
364
365 if let Err(e) = operation(
368 pod_endpoint.clone(),
369 instance_name.clone(),
370 key_data.clone(),
371 )
372 .await
373 {
374 error!(
375 "Failed operation on secondary endpoint {} (instance {}): {}",
376 pod_endpoint, instance_name, e
377 );
378 errors.push(format!(
379 "endpoint {pod_endpoint} (instance {instance_name}): {e}"
380 ));
381 } else {
382 total_endpoints += 1;
383 }
384 }
385 }
386
387 if !errors.is_empty() {
389 return Err(anyhow::anyhow!(
390 "Failed to process {} secondary endpoint(s): {}",
391 errors.len(),
392 errors.join("; ")
393 ));
394 }
395
396 Ok((first_endpoint, total_endpoints))
397}
398
399#[cfg(test)]
400#[path = "secondary_tests.rs"]
401mod secondary_tests;