bindy/reconcilers/dnszone/
cleanup.rs1use anyhow::Result;
9use kube::{Api, Client};
10use tracing::{debug, info, warn};
11
12use super::helpers::HTTP_STATUS_NOT_FOUND;
13use crate::crd::DNSZone;
14
15pub(super) fn existence_from_get_result<K>(
37 result: std::result::Result<K, kube::Error>,
38) -> Result<bool> {
39 match result {
40 Ok(_) => Ok(true),
41 Err(kube::Error::Api(ae)) if ae.code == HTTP_STATUS_NOT_FOUND => Ok(false),
42 Err(e) => Err(e.into()),
43 }
44}
45
46async fn resource_exists<K>(api: &Api<K>, name: &str) -> Result<bool>
53where
54 K: kube::Resource + Clone + std::fmt::Debug + serde::de::DeserializeOwned,
55{
56 existence_from_get_result(api.get(name).await)
57}
58
59pub async fn cleanup_deleted_instances(
78 client: &Client,
79 dnszone: &DNSZone,
80 status_updater: &mut crate::reconcilers::status::DNSZoneStatusUpdater,
81) -> Result<usize> {
82 use crate::crd::Bind9Instance;
83 use kube::{Api, ResourceExt};
84
85 let namespace = dnszone.namespace().unwrap_or_default();
86 let zone_name = &dnszone.spec.zone_name;
87
88 let current_instances = dnszone
90 .status
91 .as_ref()
92 .map(|s| s.bind9_instances.clone())
93 .unwrap_or_default();
94
95 if current_instances.is_empty() {
96 debug!(
97 "No instances in status for zone {}/{} - skipping cleanup",
98 namespace, zone_name
99 );
100 return Ok(0);
101 }
102
103 info!(
104 "Cleaning up deleted instances for zone {}/{}: checking {} instance(s)",
105 namespace,
106 zone_name,
107 current_instances.len()
108 );
109
110 let mut deleted_count = 0;
111
112 for instance_ref in current_instances {
116 let instance_api: Api<Bind9Instance> =
117 Api::namespaced(client.clone(), &instance_ref.namespace);
118
119 let instance_exists = resource_exists(&instance_api, &instance_ref.name).await?;
120
121 if !instance_exists {
122 info!(
123 "Instance {}/{} no longer exists - removing from zone {}/{}",
124 instance_ref.namespace, instance_ref.name, namespace, zone_name
125 );
126 status_updater.remove_instance(&instance_ref.name, &instance_ref.namespace);
127 deleted_count += 1;
128 }
129 }
130
131 Ok(deleted_count)
132}
133
134#[allow(clippy::too_many_lines)]
155pub async fn cleanup_stale_records(
156 client: &Client,
157 dnszone: &DNSZone,
158 status_updater: &mut crate::reconcilers::status::DNSZoneStatusUpdater,
159 bind9_instances_store: &kube::runtime::reflector::Store<crate::crd::Bind9Instance>,
160) -> Result<usize> {
161 use crate::bind9::records::query_dns_record;
162 use crate::crd::{
163 AAAARecord, ARecord, CAARecord, CNAMERecord, DNSRecordKind, MXRecord, NSRecord,
164 RecordReferenceWithTimestamp, SRVRecord, TXTRecord,
165 };
166 use kube::{Api, ResourceExt};
167
168 let namespace = dnszone.namespace().unwrap_or_default();
169 let zone_name = &dnszone.spec.zone_name;
170
171 let current_records = dnszone
173 .status
174 .as_ref()
175 .map(|s| s.records.clone())
176 .unwrap_or_default();
177
178 if current_records.is_empty() {
179 debug!(
180 "No records in status for zone {}/{} - skipping cleanup",
181 namespace, zone_name
182 );
183 return Ok(0);
184 }
185
186 info!(
187 "Cleaning up stale records for zone {}/{}: checking {} record(s)",
188 namespace,
189 zone_name,
190 current_records.len()
191 );
192
193 let instance_refs = super::validation::get_instances_from_zone(dnszone, bind9_instances_store)?;
195 let primary_refs = super::primary::filter_primary_instances(client, &instance_refs).await?;
196
197 let mut records_to_keep: Vec<RecordReferenceWithTimestamp> = Vec::new();
198 let mut stale_count = 0;
199
200 for record_ref in current_records {
206 let kind = DNSRecordKind::try_from(record_ref.kind.as_str())?;
207 let record_exists = match kind {
208 DNSRecordKind::A => {
209 let api: Api<ARecord> = Api::namespaced(client.clone(), &record_ref.namespace);
210 resource_exists(&api, &record_ref.name).await?
211 }
212 DNSRecordKind::AAAA => {
213 let api: Api<AAAARecord> = Api::namespaced(client.clone(), &record_ref.namespace);
214 resource_exists(&api, &record_ref.name).await?
215 }
216 DNSRecordKind::TXT => {
217 let api: Api<TXTRecord> = Api::namespaced(client.clone(), &record_ref.namespace);
218 resource_exists(&api, &record_ref.name).await?
219 }
220 DNSRecordKind::CNAME => {
221 let api: Api<CNAMERecord> = Api::namespaced(client.clone(), &record_ref.namespace);
222 resource_exists(&api, &record_ref.name).await?
223 }
224 DNSRecordKind::MX => {
225 let api: Api<MXRecord> = Api::namespaced(client.clone(), &record_ref.namespace);
226 resource_exists(&api, &record_ref.name).await?
227 }
228 DNSRecordKind::NS => {
229 let api: Api<NSRecord> = Api::namespaced(client.clone(), &record_ref.namespace);
230 resource_exists(&api, &record_ref.name).await?
231 }
232 DNSRecordKind::SRV => {
233 let api: Api<SRVRecord> = Api::namespaced(client.clone(), &record_ref.namespace);
234 resource_exists(&api, &record_ref.name).await?
235 }
236 DNSRecordKind::CAA => {
237 let api: Api<CAARecord> = Api::namespaced(client.clone(), &record_ref.namespace);
238 resource_exists(&api, &record_ref.name).await?
239 }
240 };
241
242 if record_exists {
243 debug!(
246 "Record {} {}/{} still exists - keeping in status",
247 record_ref.kind, record_ref.namespace, record_ref.name
248 );
249 records_to_keep.push(record_ref);
250 } else {
251 info!(
253 "Record {} {}/{} no longer exists in Kubernetes",
254 record_ref.kind, record_ref.namespace, record_ref.name
255 );
256
257 let kind = DNSRecordKind::try_from(record_ref.kind.as_str())?;
260 let record_type = kind.to_hickory_record_type();
261
262 let dns_record_name = if let Some(name) = &record_ref.record_name {
265 name.as_str()
266 } else {
267 warn!(
268 "Record {} {}/{} has no recordName in status - skipping BIND9 cleanup",
269 record_ref.kind, record_ref.namespace, record_ref.name
270 );
271 stale_count += 1;
272 continue;
273 };
274
275 let dns_record_name_clone = dns_record_name.to_string();
278 let dns_zone_name_clone = zone_name.clone();
279 let record_kind = record_ref.kind.clone();
280 let record_namespace = record_ref.namespace.clone();
281 let record_name = record_ref.name.clone();
282
283 let _ = super::helpers::for_each_instance_endpoint(
285 client,
286 &primary_refs,
287 true, "dns-tcp", |pod_endpoint, _instance_name, rndc_key| {
290 let server = pod_endpoint.clone();
291 let zone = dns_zone_name_clone.clone();
292 let dns_name = dns_record_name_clone.clone();
293 let r_type = record_type;
294 let r_kind = record_kind.clone();
295 let r_namespace = record_namespace.clone();
296 let r_name = record_name.clone();
297
298 async move {
299 match query_dns_record(&zone, &dns_name, r_type, &server).await {
301 Ok(records) if !records.is_empty() => {
302 warn!(
303 "SELF-HEALING: Record {} {}/{} deleted from K8s but still exists in BIND9 on {}",
304 r_kind, r_namespace, r_name, server
305 );
306
307 if let Some(key_data) = rndc_key {
309 match crate::bind9::records::delete_dns_record(
310 &zone,
311 &dns_name,
312 r_type,
313 &server,
314 &key_data,
315 )
316 .await
317 {
318 Ok(()) => {
319 info!(
320 "SELF-HEALING: Successfully deleted orphaned {} record {} from BIND9 on {}",
321 r_kind, dns_name, server
322 );
323 }
324 Err(e) => {
325 warn!(
326 "SELF-HEALING: Failed to delete orphaned record from BIND9 on {}: {}",
327 server, e
328 );
329 }
330 }
331 } else {
332 warn!(
333 "No RNDC key available for {} - cannot delete orphaned record",
334 server
335 );
336 }
337 }
338 Ok(_) => {
339 debug!(
341 "Record {} not found in BIND9 on {} - already cleaned up",
342 dns_name, server
343 );
344 }
345 Err(e) => {
346 debug!(
347 "Failed to query DNS on {} for {} (may not exist): {}",
348 server, dns_name, e
349 );
350 }
351 }
352
353 Ok(())
354 }
355 },
356 )
357 .await;
358
359 stale_count += 1;
361 }
362 }
363
364 if stale_count > 0 {
366 status_updater.set_records(&records_to_keep);
367 info!(
368 "Removed {} stale record(s) from zone {}/{} status",
369 stale_count, namespace, zone_name
370 );
371 }
372
373 Ok(stale_count)
374}
375
376#[cfg(test)]
377#[path = "cleanup_tests.rs"]
378mod cleanup_tests;