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;