bindy/reconcilers/dnszone/
bind9_config.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! BIND9 configuration orchestration for DNS zones.
5//!
6//! This module coordinates zone configuration on primary and secondary BIND9 instances,
7//! managing status updates and error handling throughout the configuration process.
8
9use anyhow::{anyhow, Result};
10use kube::ResourceExt;
11use std::sync::Arc;
12use tracing::info;
13
14use crate::crd::{DNSZone, InstanceReference};
15
16/// Configure zone on all BIND9 instances (primary and secondary).
17///
18/// This function orchestrates the complete BIND9 configuration workflow:
19/// 1. Sets initial "Progressing" status
20/// 2. Finds primary server IPs for secondary configuration
21/// 3. Configures zone on all primary instances
22/// 4. Configures zone on all secondary instances
23/// 5. Updates status conditions based on success/failure
24///
25/// # Arguments
26///
27/// * `ctx` - Application context with Kubernetes client
28/// * `dnszone` - The DNSZone resource being reconciled
29/// * `zone_manager` - BIND9 manager for zone operations
30/// * `status_updater` - Status updater for condition updates
31/// * `instance_refs` - All instance references assigned to the zone
32/// * `unreconciled_instances` - Instances that need reconciliation (Phase 2 optimization)
33///
34/// # Returns
35///
36/// Tuple of `(primary_outcome, secondary_outcome)` - per-instance and per-endpoint
37/// configuration counts for primary and secondary instances (see
38/// [`super::types::ZoneConfigOutcome`])
39///
40/// # Errors
41///
42/// Returns an error if:
43/// - No primary servers are found (cannot configure secondary zones)
44/// - Primary configuration fails completely
45/// - Kubernetes API operations fail
46///
47/// Note: Secondary configuration failure is non-fatal and logged as a warning.
48/// On every fatal error path the Ready condition is set to False and the
49/// Progressing condition is resolved before the error is returned.
50#[allow(clippy::too_many_arguments)]
51#[allow(clippy::too_many_lines)]
52pub async fn configure_zone_on_instances(
53    ctx: Arc<crate::context::Context>,
54    dnszone: &DNSZone,
55    zone_manager: &crate::bind9::Bind9Manager,
56    status_updater: &mut crate::reconcilers::status::DNSZoneStatusUpdater,
57    instance_refs: &[InstanceReference],
58    _unreconciled_instances: &[InstanceReference],
59) -> Result<(
60    super::types::ZoneConfigOutcome,
61    super::types::ZoneConfigOutcome,
62)> {
63    let client = ctx.client.clone();
64    let namespace = dnszone.namespace().unwrap_or_default();
65    let spec = &dnszone.spec;
66
67    tracing::debug!("Ensuring BIND9 zone exists on all instances (declarative reconciliation)");
68
69    // Set initial Progressing status (in-memory)
70    status_updater.set_condition(
71        "Progressing",
72        "True",
73        "PrimaryReconciling",
74        "Configuring zone on primary servers",
75    );
76
77    // Get current primary IPs for secondary zone configuration
78    // Find all primary instances from our instance refs and get their pod IPs
79    let primary_ips =
80        match super::primary::find_primary_ips_from_instances(&client, instance_refs).await {
81            Ok(ips) if !ips.is_empty() => {
82                info!(
83                    "Found {} primary server IP(s) for zone {}/{}: {:?}",
84                    ips.len(),
85                    namespace,
86                    spec.zone_name,
87                    ips
88                );
89                ips
90            }
91            Ok(_) => {
92                let message = "No primary servers found - cannot configure secondary zones";
93                set_failure_conditions(status_updater, "PrimaryFailed", message);
94                // Apply status before returning error
95                status_updater.apply(&client).await?;
96                return Err(anyhow!(
97                    "No primary servers found for zone {}/{} - cannot configure secondary zones",
98                    namespace,
99                    spec.zone_name
100                ));
101            }
102            Err(e) => {
103                set_failure_conditions(
104                    status_updater,
105                    "PrimaryFailed",
106                    &format!("Failed to find primary servers: {e}"),
107                );
108                // Apply status before returning error
109                status_updater.apply(&client).await?;
110                return Err(e);
111            }
112        };
113
114    // Add/update zone on all primary instances
115    // Primary instances are marked as reconciled inside add_dnszone() immediately after success
116    // CRITICAL: We pass ALL instances (not just unreconciled ones) to ensure zones are recreated
117    // after pod restarts. The add_zones() function is idempotent (checks zone_exists first).
118    let primary_outcome = match super::add_dnszone(
119        ctx.clone(),
120        dnszone.clone(),
121        zone_manager,
122        status_updater,
123        instance_refs,
124    )
125    .await
126    {
127        Ok(outcome) => {
128            // Update status after successful primary reconciliation (in-memory)
129            status_updater.set_condition(
130                "Progressing",
131                "True",
132                "PrimaryReconciled",
133                &format!(
134                    "Zone {} configured on {} primary instance(s) ({} endpoint(s))",
135                    spec.zone_name, outcome.instances_configured, outcome.endpoints_configured
136                ),
137            );
138            outcome
139        }
140        Err(e) => {
141            set_failure_conditions(
142                status_updater,
143                "PrimaryFailed",
144                &format!("Failed to configure zone on primary servers: {e}"),
145            );
146            // Apply status before returning error
147            status_updater.apply(&client).await?;
148            return Err(e);
149        }
150    };
151
152    // Update to secondary reconciliation phase (in-memory)
153    status_updater.set_condition(
154        "Progressing",
155        "True",
156        "SecondaryReconciling",
157        "Configuring zone on secondary servers",
158    );
159
160    // Add/update zone on all secondary instances with primaries configured
161    // Secondary instances are marked as reconciled inside add_dnszone_to_secondaries() immediately after success
162    // CRITICAL: We pass ALL instances (not just unreconciled ones) to ensure zones are recreated
163    // after pod restarts. The add_zones() function is idempotent (checks zone_exists first).
164    let secondary_outcome = match super::add_dnszone_to_secondaries(
165        ctx.clone(),
166        dnszone.clone(),
167        zone_manager,
168        &primary_ips,
169        status_updater,
170        instance_refs,
171    )
172    .await
173    {
174        Ok(outcome) => {
175            // Update status after successful secondary reconciliation (in-memory)
176            if outcome.endpoints_configured > 0 {
177                status_updater.set_condition(
178                    "Progressing",
179                    "True",
180                    "SecondaryReconciled",
181                    &format!(
182                        "Zone {} configured on {} secondary instance(s) ({} endpoint(s))",
183                        spec.zone_name, outcome.instances_configured, outcome.endpoints_configured
184                    ),
185                );
186            }
187            outcome
188        }
189        Err(e) => {
190            // Secondary failure is non-fatal - primaries still work
191            tracing::warn!(
192                "Failed to configure zone on secondary servers: {}. Primary servers are still operational.",
193                e
194            );
195            status_updater.set_condition(
196                "Degraded",
197                "True",
198                "SecondaryFailed",
199                &format!(
200                    "Zone configured on {} primary instance(s) but secondary configuration failed: {e}",
201                    primary_outcome.instances_configured
202                ),
203            );
204            super::types::ZoneConfigOutcome::default()
205        }
206    };
207
208    Ok((primary_outcome, secondary_outcome))
209}
210
211/// Sets the condition triple for a fatal zone configuration failure (in-memory).
212///
213/// Ensures the conditions converge to a consistent failed state:
214/// - `Degraded=True` with the failure reason and message
215/// - `Ready=False` with the same reason and message (a previously set
216///   `Ready=True` must never survive a failed reconciliation)
217/// - `Progressing=False` (the reconciliation attempt has finished)
218///
219/// # Arguments
220///
221/// * `status_updater` - Status updater collecting in-memory condition changes
222/// * `reason` - Programmatic failure reason in `CamelCase` (e.g. `PrimaryFailed`)
223/// * `message` - Human-readable failure explanation
224pub fn set_failure_conditions(
225    status_updater: &mut crate::reconcilers::status::DNSZoneStatusUpdater,
226    reason: &str,
227    message: &str,
228) {
229    status_updater.set_condition("Degraded", "True", reason, message);
230    status_updater.set_condition("Ready", "False", reason, message);
231    status_updater.set_condition(
232        "Progressing",
233        "False",
234        reason,
235        "Reconciliation attempt finished with errors",
236    );
237}
238
239#[cfg(test)]
240#[path = "bind9_config_tests.rs"]
241mod bind9_config_tests;