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_count, secondary_count)` - number of successfully configured instances
37///
38/// # Errors
39///
40/// Returns an error if:
41/// - No primary servers are found (cannot configure secondary zones)
42/// - Primary configuration fails completely
43/// - Kubernetes API operations fail
44///
45/// Note: Secondary configuration failure is non-fatal and logged as a warning
46#[allow(clippy::too_many_arguments)]
47#[allow(clippy::too_many_lines)]
48pub async fn configure_zone_on_instances(
49    ctx: Arc<crate::context::Context>,
50    dnszone: &DNSZone,
51    zone_manager: &crate::bind9::Bind9Manager,
52    status_updater: &mut crate::reconcilers::status::DNSZoneStatusUpdater,
53    instance_refs: &[InstanceReference],
54    _unreconciled_instances: &[InstanceReference],
55) -> Result<(usize, usize)> {
56    let client = ctx.client.clone();
57    let namespace = dnszone.namespace().unwrap_or_default();
58    let spec = &dnszone.spec;
59
60    tracing::debug!("Ensuring BIND9 zone exists on all instances (declarative reconciliation)");
61
62    // Set initial Progressing status (in-memory)
63    status_updater.set_condition(
64        "Progressing",
65        "True",
66        "PrimaryReconciling",
67        "Configuring zone on primary servers",
68    );
69
70    // Get current primary IPs for secondary zone configuration
71    // Find all primary instances from our instance refs and get their pod IPs
72    let primary_ips =
73        match super::primary::find_primary_ips_from_instances(&client, instance_refs).await {
74            Ok(ips) if !ips.is_empty() => {
75                info!(
76                    "Found {} primary server IP(s) for zone {}/{}: {:?}",
77                    ips.len(),
78                    namespace,
79                    spec.zone_name,
80                    ips
81                );
82                ips
83            }
84            Ok(_) => {
85                status_updater.set_condition(
86                    "Degraded",
87                    "True",
88                    "PrimaryFailed",
89                    "No primary servers found - cannot configure secondary zones",
90                );
91                // Apply status before returning error
92                status_updater.apply(&client).await?;
93                return Err(anyhow!(
94                    "No primary servers found for zone {}/{} - cannot configure secondary zones",
95                    namespace,
96                    spec.zone_name
97                ));
98            }
99            Err(e) => {
100                status_updater.set_condition(
101                    "Degraded",
102                    "True",
103                    "PrimaryFailed",
104                    &format!("Failed to find primary servers: {e}"),
105                );
106                // Apply status before returning error
107                status_updater.apply(&client).await?;
108                return Err(e);
109            }
110        };
111
112    // Add/update zone on all primary instances
113    // Primary instances are marked as reconciled inside add_dnszone() immediately after success
114    // CRITICAL: We pass ALL instances (not just unreconciled ones) to ensure zones are recreated
115    // after pod restarts. The add_zones() function is idempotent (checks zone_exists first).
116    let primary_count = match super::add_dnszone(
117        ctx.clone(),
118        dnszone.clone(),
119        zone_manager,
120        status_updater,
121        instance_refs,
122    )
123    .await
124    {
125        Ok(count) => {
126            // Update status after successful primary reconciliation (in-memory)
127            status_updater.set_condition(
128                "Progressing",
129                "True",
130                "PrimaryReconciled",
131                &format!(
132                    "Zone {} configured on {} primary server(s)",
133                    spec.zone_name, count
134                ),
135            );
136            count
137        }
138        Err(e) => {
139            status_updater.set_condition(
140                "Degraded",
141                "True",
142                "PrimaryFailed",
143                &format!("Failed to configure zone on primary servers: {e}"),
144            );
145            // Apply status before returning error
146            status_updater.apply(&client).await?;
147            return Err(e);
148        }
149    };
150
151    // Update to secondary reconciliation phase (in-memory)
152    status_updater.set_condition(
153        "Progressing",
154        "True",
155        "SecondaryReconciling",
156        "Configuring zone on secondary servers",
157    );
158
159    // Add/update zone on all secondary instances with primaries configured
160    // Secondary instances are marked as reconciled inside add_dnszone_to_secondaries() immediately after success
161    // CRITICAL: We pass ALL instances (not just unreconciled ones) to ensure zones are recreated
162    // after pod restarts. The add_zones() function is idempotent (checks zone_exists first).
163    let secondary_count = match super::add_dnszone_to_secondaries(
164        ctx.clone(),
165        dnszone.clone(),
166        zone_manager,
167        &primary_ips,
168        status_updater,
169        instance_refs,
170    )
171    .await
172    {
173        Ok(count) => {
174            // Update status after successful secondary reconciliation (in-memory)
175            if count > 0 {
176                status_updater.set_condition(
177                    "Progressing",
178                    "True",
179                    "SecondaryReconciled",
180                    &format!(
181                        "Zone {} configured on {} secondary server(s)",
182                        spec.zone_name, count
183                    ),
184                );
185            }
186            count
187        }
188        Err(e) => {
189            // Secondary failure is non-fatal - primaries still work
190            tracing::warn!(
191                "Failed to configure zone on secondary servers: {}. Primary servers are still operational.",
192                e
193            );
194            status_updater.set_condition(
195                "Degraded",
196                "True",
197                "SecondaryFailed",
198                &format!(
199                    "Zone configured on {primary_count} primary server(s) but secondary configuration failed: {e}"
200                ),
201            );
202            0
203        }
204    };
205
206    Ok((primary_count, secondary_count))
207}
208
209#[cfg(test)]
210#[path = "bind9_config_tests.rs"]
211mod bind9_config_tests;