bindy/reconcilers/bind9cluster/
mod.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! BIND9 cluster reconciliation logic.
5//!
6//! This module handles the lifecycle of BIND9 cluster resources in Kubernetes.
7//! It manages the `Bind9Instance` resources that belong to a cluster and updates
8//! the cluster status to reflect the overall health.
9//!
10//! ## Module Structure
11//!
12//! - [`config`] - Cluster `ConfigMap` management
13//! - [`drift`] - Instance drift detection
14//! - [`instances`] - Instance lifecycle management
15//! - [`status_helpers`] - Status calculation and updates
16//! - [`types`] - Shared types and imports
17
18// Submodules
19pub mod config;
20pub mod drift;
21pub mod instances;
22pub mod status_helpers;
23pub mod types;
24
25// Re-export public APIs for external use
26pub use instances::{create_managed_instance, delete_bind9cluster, delete_managed_instance};
27pub use status_helpers::calculate_cluster_status;
28
29// Internal imports
30use config::create_or_update_cluster_configmap;
31use drift::detect_instance_drift;
32use instances::reconcile_managed_instances;
33use status_helpers::update_status;
34#[allow(clippy::wildcard_imports)]
35use types::*;
36
37use crate::labels::FINALIZER_BIND9_CLUSTER;
38use crate::reconcilers::finalizers::{ensure_finalizer, handle_deletion, FinalizerCleanup};
39
40/// Implement cleanup trait for `Bind9Cluster` finalizer management.
41#[async_trait::async_trait]
42impl FinalizerCleanup for Bind9Cluster {
43    async fn cleanup(&self, client: &Client) -> Result<()> {
44        let namespace = self.namespace().unwrap_or_default();
45        let name = self.name_any();
46        instances::delete_cluster_instances(client, &namespace, &name).await
47    }
48}
49
50/// Reconciles a `Bind9Cluster` resource.
51///
52/// This function orchestrates the complete cluster reconciliation workflow:
53/// 1. Checks if the cluster is being deleted and handles cleanup
54/// 2. Adds finalizer if not present
55/// 3. Detects drift in managed instances
56/// 4. Creates/updates cluster `ConfigMap`
57/// 5. Reconciles managed instances
58/// 6. Updates cluster status based on instance health
59///
60/// # Arguments
61///
62/// * `ctx` - Operator context with Kubernetes client and reflector stores
63/// * `cluster` - The `Bind9Cluster` resource to reconcile
64///
65/// # Returns
66///
67/// * `Ok(())` - If reconciliation succeeded
68/// * `Err(_)` - If status update failed
69///
70/// # Errors
71///
72/// Returns an error if Kubernetes API operations fail or status update fails.
73pub async fn reconcile_bind9cluster(ctx: Arc<Context>, cluster: Bind9Cluster) -> Result<()> {
74    let client = ctx.client.clone();
75    let namespace = cluster.namespace().unwrap_or_default();
76    let name = cluster.name_any();
77
78    info!("Reconciling Bind9Cluster: {}/{}", namespace, name);
79    debug!(
80        namespace = %namespace,
81        name = %name,
82        generation = ?cluster.metadata.generation,
83        "Starting Bind9Cluster reconciliation"
84    );
85
86    // Handle deletion if cluster is being deleted
87    if cluster.metadata.deletion_timestamp.is_some() {
88        return handle_deletion(&client, &cluster, FINALIZER_BIND9_CLUSTER).await;
89    }
90
91    // Ensure finalizer is present
92    ensure_finalizer(&client, &cluster, FINALIZER_BIND9_CLUSTER).await?;
93
94    // Check if spec has changed using the standard generation check
95    let current_generation = cluster.metadata.generation;
96    let observed_generation = cluster.status.as_ref().and_then(|s| s.observed_generation);
97
98    // Only reconcile spec-related resources if spec changed OR drift detected
99    let spec_changed =
100        crate::reconcilers::should_reconcile(current_generation, observed_generation);
101
102    // DRIFT DETECTION: Check if managed instances match desired state
103    let drift_detected = if spec_changed {
104        false
105    } else {
106        detect_instance_drift(&client, &cluster, &namespace, &name).await?
107    };
108
109    if spec_changed || drift_detected {
110        if drift_detected {
111            info!(
112                "Spec unchanged but instance drift detected for cluster {}/{}",
113                namespace, name
114            );
115        } else {
116            debug!(
117                "Reconciliation needed: current_generation={:?}, observed_generation={:?}",
118                current_generation, observed_generation
119            );
120        }
121
122        // Create or update shared cluster ConfigMap
123        create_or_update_cluster_configmap(&client, &cluster).await?;
124
125        // Reconcile managed instances (create/update as needed)
126        reconcile_managed_instances(&ctx, &cluster).await?;
127    } else {
128        debug!(
129            "Spec unchanged (generation={:?}) and no drift detected, skipping resource reconciliation",
130            current_generation
131        );
132    }
133
134    // ALWAYS list and analyze cluster instances to update status
135    // This ensures status reflects current instance health even when spec hasn't changed
136    let instances: Vec<Bind9Instance> =
137        list_cluster_instances(&client, &cluster, &namespace, &name).await?;
138
139    // Calculate cluster status from instances
140    let (instance_count, ready_instances, instance_names, conditions) =
141        calculate_cluster_status(&instances, &namespace, &name);
142
143    // Update cluster status with all conditions
144    update_status(
145        &client,
146        &cluster,
147        conditions,
148        instance_count,
149        ready_instances,
150        instance_names,
151    )
152    .await?;
153
154    Ok(())
155}
156
157/// List all `Bind9Instance` resources that reference a cluster.
158///
159/// Filters instances in the namespace that have `clusterRef` matching the cluster name.
160///
161/// # Arguments
162///
163/// * `client` - Kubernetes API client
164/// * `cluster` - The `Bind9Cluster` to find instances for
165/// * `namespace` - Cluster namespace
166/// * `name` - Cluster name
167///
168/// # Returns
169///
170/// Vector of `Bind9Instance` resources that reference this cluster
171///
172/// # Errors
173///
174/// Returns an error if:
175/// - Failed to list instances
176/// - Failed to update cluster status on error
177async fn list_cluster_instances(
178    client: &Client,
179    cluster: &Bind9Cluster,
180    namespace: &str,
181    name: &str,
182) -> Result<Vec<Bind9Instance>> {
183    // List all Bind9Instance resources in the namespace that reference this cluster
184    let instances_api: Api<Bind9Instance> = Api::namespaced(client.clone(), namespace);
185    let list_params = ListParams::default();
186    debug!(namespace = %namespace, "Listing Bind9Instance resources");
187
188    match instances_api.list(&list_params).await {
189        Ok(list) => {
190            debug!(
191                total_instances_in_ns = list.items.len(),
192                "Listed Bind9Instance resources"
193            );
194            // Filter instances that reference this cluster
195            let filtered: Vec<_> = list
196                .items
197                .into_iter()
198                .filter(|instance| instance.spec.cluster_ref == name)
199                .collect();
200            debug!(
201                filtered_instances = filtered.len(),
202                cluster_ref = %name,
203                "Filtered instances by cluster reference"
204            );
205            Ok(filtered)
206        }
207        Err(e) => {
208            error!(
209                "Failed to list Bind9Instance resources for cluster {}/{}: {}",
210                namespace, name, e
211            );
212
213            // Update status to show error
214            let error_condition = Condition {
215                r#type: CONDITION_TYPE_READY.to_string(),
216                status: "False".to_string(),
217                reason: Some(REASON_NOT_READY.to_string()),
218                message: Some(format!("Failed to list instances: {e}")),
219                last_transition_time: Some(Utc::now().to_rfc3339()),
220            };
221            update_status(client, cluster, vec![error_condition], 0, 0, vec![]).await?;
222
223            Err(e.into())
224        }
225    }
226}