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;