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: &crate::context::MultiStore<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, PTRRecord,
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 DNSRecordKind::PTR => {
241 let api: Api<PTRRecord> = Api::namespaced(client.clone(), &record_ref.namespace);
242 resource_exists(&api, &record_ref.name).await?
243 }
244 };
245
246 if record_exists {
247 debug!(
250 "Record {} {}/{} still exists - keeping in status",
251 record_ref.kind, record_ref.namespace, record_ref.name
252 );
253 records_to_keep.push(record_ref);
254 } else {
255 info!(
257 "Record {} {}/{} no longer exists in Kubernetes",
258 record_ref.kind, record_ref.namespace, record_ref.name
259 );
260
261 let kind = DNSRecordKind::try_from(record_ref.kind.as_str())?;
264 let record_type = kind.to_hickory_record_type();
265
266 let dns_record_name = if let Some(name) = &record_ref.record_name {
269 name.as_str()
270 } else {
271 warn!(
272 "Record {} {}/{} has no recordName in status - skipping BIND9 cleanup",
273 record_ref.kind, record_ref.namespace, record_ref.name
274 );
275 stale_count += 1;
276 continue;
277 };
278
279 let dns_record_name_clone = dns_record_name.to_string();
282 let dns_zone_name_clone = zone_name.clone();
283 let record_kind = record_ref.kind.clone();
284 let record_namespace = record_ref.namespace.clone();
285 let record_name = record_ref.name.clone();
286
287 let _ = super::helpers::for_each_instance_endpoint(
289 client,
290 &primary_refs,
291 true, "dns-tcp", |pod_endpoint, _instance_name, rndc_key| {
294 let server = pod_endpoint.clone();
295 let zone = dns_zone_name_clone.clone();
296 let dns_name = dns_record_name_clone.clone();
297 let r_type = record_type;
298 let r_kind = record_kind.clone();
299 let r_namespace = record_namespace.clone();
300 let r_name = record_name.clone();
301
302 async move {
303 match query_dns_record(&zone, &dns_name, r_type, &server).await {
305 Ok(records) if !records.is_empty() => {
306 warn!(
307 "SELF-HEALING: Record {} {}/{} deleted from K8s but still exists in BIND9 on {}",
308 r_kind, r_namespace, r_name, server
309 );
310
311 if let Some(key_data) = rndc_key {
313 match crate::bind9::records::delete_dns_record(
314 &zone,
315 &dns_name,
316 r_type,
317 &server,
318 &key_data,
319 )
320 .await
321 {
322 Ok(()) => {
323 info!(
324 "SELF-HEALING: Successfully deleted orphaned {} record {} from BIND9 on {}",
325 r_kind, dns_name, server
326 );
327 }
328 Err(e) => {
329 warn!(
330 "SELF-HEALING: Failed to delete orphaned record from BIND9 on {}: {}",
331 server, e
332 );
333 }
334 }
335 } else {
336 warn!(
337 "No RNDC key available for {} - cannot delete orphaned record",
338 server
339 );
340 }
341 }
342 Ok(_) => {
343 debug!(
345 "Record {} not found in BIND9 on {} - already cleaned up",
346 dns_name, server
347 );
348 }
349 Err(e) => {
350 debug!(
351 "Failed to query DNS on {} for {} (may not exist): {}",
352 server, dns_name, e
353 );
354 }
355 }
356
357 Ok(())
358 }
359 },
360 )
361 .await;
362
363 stale_count += 1;
365 }
366 }
367
368 if stale_count > 0 {
370 status_updater.set_records(&records_to_keep);
371 info!(
372 "Removed {} stale record(s) from zone {}/{} status",
373 stale_count, namespace, zone_name
374 );
375 }
376
377 Ok(stale_count)
378}
379
380#[cfg(test)]
381#[path = "cleanup_tests.rs"]
382mod cleanup_tests;