1pub mod cluster_helpers;
20pub mod config;
21pub mod resources;
22pub mod status_helpers;
23pub mod types;
24pub mod zones;
25
26pub use zones::reconcile_instance_zones;
28
29use cluster_helpers::{build_cluster_reference, fetch_cluster_info};
31use resources::{create_or_update_resources, delete_resources};
32use status_helpers::{update_status, update_status_from_deployment};
33#[allow(clippy::wildcard_imports)]
34use types::*;
35use zones::reconcile_instance_zones as reconcile_zones_internal;
36
37use crate::reconcilers::finalizers::{ensure_finalizer, handle_deletion, FinalizerCleanup};
38
39#[allow(dead_code)] fn calculate_requeue_duration(
85 config: &crate::crd::RndcKeyConfig,
86 secret: &Secret,
87) -> Option<std::time::Duration> {
88 use chrono::Utc;
89
90 if !config.auto_rotate {
92 return None;
93 }
94
95 let annotations = secret.metadata.annotations.as_ref()?;
97 let (_created_at, rotate_at, _rotation_count) =
98 crate::bind9::rndc::parse_rotation_annotations(annotations).ok()?;
99
100 let rotate_at = rotate_at?;
102
103 let now = Utc::now();
104 let time_until_rotation = rotate_at.signed_duration_since(now);
105
106 if time_until_rotation.num_seconds() <= 0 {
108 return Some(std::time::Duration::from_secs(30));
109 }
110
111 let requeue_secs = time_until_rotation
113 .num_seconds()
114 .saturating_sub(300) .max(30); #[allow(clippy::cast_sign_loss)] Some(std::time::Duration::from_secs(requeue_secs as u64))
119}
120
121async fn update_rotation_status(
144 client: &Client,
145 instance: &Bind9Instance,
146 secret: &Secret,
147 config: &crate::crd::RndcKeyConfig,
148) -> Result<()> {
149 use crate::crd::RndcKeyRotationStatus;
150
151 if !config.auto_rotate {
153 return Ok(());
154 }
155
156 let Some(annotations) = &secret.metadata.annotations else {
157 debug!("Secret has no annotations, skipping rotation status update");
158 return Ok(());
159 };
160
161 let (created_at, rotate_at, rotation_count) =
162 crate::bind9::rndc::parse_rotation_annotations(annotations)?;
163
164 let last_rotated_at = if rotation_count > 0 {
166 Some(created_at.to_rfc3339())
167 } else {
168 None
169 };
170
171 let rotation_status = RndcKeyRotationStatus {
172 created_at: created_at.to_rfc3339(),
173 rotate_at: rotate_at.map(|dt| dt.to_rfc3339()),
174 last_rotated_at,
175 rotation_count,
176 };
177
178 let namespace = instance.namespace().unwrap_or_default();
180 let name = instance.name_any();
181
182 let status = serde_json::json!({
183 "status": {
184 "rndcKeyRotation": rotation_status
185 }
186 });
187
188 let api: Api<Bind9Instance> = Api::namespaced(client.clone(), &namespace);
189 api.patch_status(
190 &name,
191 &PatchParams::default(),
192 &kube::api::Patch::Merge(&status),
193 )
194 .await?;
195
196 debug!(
197 "Updated rotation status for {}/{}: rotation_count={}, rotate_at={:?}",
198 namespace, name, rotation_count, rotate_at
199 );
200
201 Ok(())
202}
203
204#[async_trait::async_trait]
206impl FinalizerCleanup for Bind9Instance {
207 async fn cleanup(&self, client: &Client) -> Result<()> {
208 let namespace = self.namespace().unwrap_or_default();
209 let name = self.name_any();
210
211 let is_managed: bool = self
213 .metadata
214 .labels
215 .as_ref()
216 .and_then(|labels| labels.get(BINDY_MANAGED_BY_LABEL))
217 .is_some();
218
219 if is_managed {
220 info!(
221 "Bind9Instance {}/{} is managed by a Bind9Cluster, skipping resource cleanup (cluster will handle it)",
222 namespace, name
223 );
224 Ok(())
225 } else {
226 info!(
227 "Running cleanup for standalone Bind9Instance {}/{}",
228 namespace, name
229 );
230 delete_resources(client, &namespace, &name).await
231 }
232 }
233}
234
235#[allow(clippy::too_many_lines)]
270pub async fn reconcile_bind9instance(ctx: Arc<Context>, instance: Bind9Instance) -> Result<()> {
271 let client = ctx.client.clone();
272 let namespace = instance.namespace().unwrap_or_default();
273 let name = instance.name_any();
274
275 info!("Reconciling Bind9Instance: {}/{}", namespace, name);
276 debug!(
277 namespace = %namespace,
278 name = %name,
279 generation = ?instance.metadata.generation,
280 "Starting Bind9Instance reconciliation"
281 );
282
283 if instance.metadata.deletion_timestamp.is_some() {
285 return handle_deletion(&client, &instance, FINALIZER_BIND9_INSTANCE).await;
286 }
287
288 ensure_finalizer(&client, &instance, FINALIZER_BIND9_INSTANCE).await?;
290
291 let spec = &instance.spec;
292 let replicas = spec.replicas.unwrap_or(1);
293 let version = spec
294 .version
295 .as_deref()
296 .unwrap_or(crate::constants::DEFAULT_BIND9_VERSION);
297
298 debug!(
299 cluster_ref = %spec.cluster_ref,
300 replicas,
301 version = %version,
302 role = ?spec.role,
303 "Instance configuration"
304 );
305
306 info!(
307 "Bind9Instance {} configured with {} replicas, version {}",
308 name, replicas, version
309 );
310
311 let current_generation = instance.metadata.generation;
313 let observed_generation = instance.status.as_ref().and_then(|s| s.observed_generation);
314
315 let is_managed: bool = instance
317 .metadata
318 .labels
319 .as_ref()
320 .and_then(|labels| labels.get(BINDY_MANAGED_BY_LABEL))
321 .is_some();
322
323 let (cluster, cluster_provider) = fetch_cluster_info(&client, &namespace, &instance).await;
326
327 let parent_config_changed = {
330 let cluster_changed = if let Some(ref c) = cluster {
332 let cluster_generation = c.metadata.generation.unwrap_or(0);
333 let instance_observed_gen = observed_generation.unwrap_or(0);
334
335 if cluster_generation > instance_observed_gen {
339 debug!(
340 "Parent Bind9Cluster generation ({}) is newer than instance observed generation ({}), checking for config changes",
341 cluster_generation, instance_observed_gen
342 );
343 true
344 } else {
345 false
346 }
347 } else {
348 false
349 };
350
351 let provider_changed = if let Some(ref cp) = cluster_provider {
353 let provider_generation = cp.metadata.generation.unwrap_or(0);
354 let instance_observed_gen = observed_generation.unwrap_or(0);
355
356 if provider_generation > instance_observed_gen {
358 debug!(
359 "Parent ClusterBind9Provider generation ({}) is newer than instance observed generation ({}), checking for config changes",
360 provider_generation, instance_observed_gen
361 );
362 true
363 } else {
364 false
365 }
366 } else {
367 false
368 };
369
370 cluster_changed || provider_changed
371 };
372
373 if parent_config_changed {
374 info!(
375 "Parent cluster configuration may have changed for Bind9Instance {}/{}, will check for drift",
376 namespace, name
377 );
378 }
379
380 let (all_resources_exist, deployment_labels_match, rotation_needed) = {
382 let deployment_api: Api<Deployment> = Api::namespaced(client.clone(), &namespace);
383 let service_api: Api<Service> = Api::namespaced(client.clone(), &namespace);
384 let configmap_api: Api<ConfigMap> = Api::namespaced(client.clone(), &namespace);
385 let secret_api: Api<Secret> = Api::namespaced(client.clone(), &namespace);
386
387 let (deployment_exists, labels_match) = match deployment_api.get(&name).await {
389 Ok(deployment) => {
390 let desired_labels =
392 crate::bind9_resources::build_labels_from_instance(&name, &instance);
393
394 let labels_match = if let Some(actual_labels) = &deployment.metadata.labels {
398 desired_labels
399 .iter()
400 .all(|(key, value)| actual_labels.get(key) == Some(value))
401 } else {
402 false };
404
405 (true, labels_match)
406 }
407 Err(_) => (false, false),
408 };
409
410 let service_exists = service_api.get(&name).await.is_ok();
411
412 let configmap_name = if is_managed {
414 format!("{}-config", spec.cluster_ref)
415 } else {
416 format!("{name}-config")
417 };
418 let configmap_exists = configmap_api.get(&configmap_name).await.is_ok();
419
420 let secret_name = format!("{name}-rndc-key");
422 let (secret_exists, needs_rotation) = match secret_api.get(&secret_name).await {
423 Ok(secret) => {
424 let rndc_config = resources::resolve_full_rndc_config(
426 &instance,
427 cluster.as_ref(),
428 cluster_provider.as_ref(),
429 );
430
431 let needs_rotation =
433 resources::should_rotate_secret(&secret, &rndc_config).unwrap_or(false);
434
435 if needs_rotation {
436 debug!(
437 "RNDC Secret {}/{} rotation is due, will trigger reconciliation",
438 namespace, secret_name
439 );
440 }
441
442 (true, needs_rotation)
443 }
444 Err(_) => (false, false),
445 };
446
447 let all_exist = deployment_exists && service_exists && configmap_exists && secret_exists;
448 (all_exist, labels_match, needs_rotation)
449 };
450 let cluster_ref = build_cluster_reference(cluster.as_ref(), cluster_provider.as_ref());
451
452 if let Some(ref cr) = cluster_ref {
453 debug!(
454 "Built cluster reference for instance {}/{}: {}/{} in namespace {:?}",
455 namespace, name, cr.kind, cr.name, cr.namespace
456 );
457 } else {
458 debug!(
459 "No cluster reference built for instance {}/{} - spec.clusterRef may be empty or cluster not found",
460 namespace, name
461 );
462 }
463
464 let should_reconcile =
471 crate::reconcilers::should_reconcile(current_generation, observed_generation);
472
473 if !should_reconcile
478 && all_resources_exist
479 && deployment_labels_match
480 && !rotation_needed
481 && !parent_config_changed
482 {
483 debug!(
484 "Spec unchanged (generation={:?}), all resources exist, deployment labels match, no rotation needed, and parent config unchanged - skipping resource reconciliation",
485 current_generation
486 );
487 let cluster_ref = instance.status.as_ref().and_then(|s| s.cluster_ref.clone());
490 update_status_from_deployment(&client, &namespace, &name, &instance, cluster_ref).await?;
491
492 reconcile_zones_internal(&client, &ctx.stores, &instance).await?;
494
495 return Ok(());
496 }
497
498 if !deployment_labels_match && all_resources_exist {
505 info!(
506 "Deployment labels don't match desired state for {}/{}, triggering reconciliation to update labels",
507 namespace, name
508 );
509 }
510
511 if !should_reconcile && !all_resources_exist {
512 info!(
513 "Drift detected for Bind9Instance {}/{}: One or more resources missing, will recreate",
514 namespace, name
515 );
516 }
517
518 if rotation_needed {
519 info!(
520 "RNDC Secret rotation is due for Bind9Instance {}/{}, triggering reconciliation",
521 namespace, name
522 );
523 }
524
525 if parent_config_changed {
526 info!(
527 "Parent cluster configuration changed for Bind9Instance {}/{}, triggering reconciliation to apply new config",
528 namespace, name
529 );
530 }
531
532 debug!(
533 "Reconciliation needed: current_generation={:?}, observed_generation={:?}",
534 current_generation, observed_generation
535 );
536
537 match create_or_update_resources(&client, &namespace, &name, &instance).await {
539 Ok((cluster, cluster_provider, secret)) => {
540 info!(
541 "Successfully created/updated resources for {}/{}",
542 namespace, name
543 );
544
545 let cluster_ref = build_cluster_reference(cluster.as_ref(), cluster_provider.as_ref());
547
548 update_status_from_deployment(&client, &namespace, &name, &instance, cluster_ref)
550 .await?;
551
552 if let Some(ref secret) = secret {
554 let rndc_config = resources::resolve_full_rndc_config(
556 &instance,
557 cluster.as_ref(),
558 cluster_provider.as_ref(),
559 );
560
561 if let Err(e) =
562 update_rotation_status(&client, &instance, secret, &rndc_config).await
563 {
564 warn!(
565 "Failed to update rotation status for {}/{}: {}",
566 namespace, name, e
567 );
568 }
570 }
571
572 reconcile_zones_internal(&client, &ctx.stores, &instance).await?;
574 }
575 Err(e) => {
576 error!(
577 "Failed to create/update resources for {}/{}: {}",
578 namespace, name, e
579 );
580
581 let error_condition = Condition {
583 r#type: CONDITION_TYPE_READY.to_string(),
584 status: "False".to_string(),
585 reason: Some(REASON_NOT_READY.to_string()),
586 message: Some(format!("Failed to create resources: {e}")),
587 last_transition_time: Some(Utc::now().to_rfc3339()),
588 };
589 update_status(&client, &instance, vec![error_condition], None).await?;
591
592 return Err(e);
593 }
594 }
595
596 Ok(())
597}
598
599pub async fn delete_bind9instance(ctx: Arc<Context>, instance: Bind9Instance) -> Result<()> {
608 let _client = ctx.client.clone();
609 let namespace = instance.namespace().unwrap_or_default();
610 let name = instance.name_any();
611
612 info!(
613 "Delete called for Bind9Instance {}/{} (handled by finalizer)",
614 namespace, name
615 );
616
617 Ok(())
619}
620
621#[cfg(test)]
622#[path = "mod_tests.rs"]
623mod mod_tests;