bindy/reconcilers/mod.rs
1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Kubernetes reconciliation controllers for DNS resources.
5//!
6//! This module contains the reconciliation logic for all Bindy Custom Resources.
7//! Each reconciler watches for changes to its respective resource type and updates
8//! BIND9 configurations accordingly.
9//!
10//! # Reconciliation Architecture
11//!
12//! Bindy follows the standard Kubernetes controller pattern:
13//!
14//! 1. **Watch** - Monitor resource changes via Kubernetes API
15//! 2. **Reconcile** - Compare desired state (CRD spec) with actual state
16//! 3. **Update** - Modify BIND9 configuration to match desired state
17//! 4. **Status** - Report reconciliation results back to Kubernetes
18//!
19//! # Available Reconcilers
20//!
21//! ## Infrastructure
22//!
23//! - [`reconcile_bind9cluster`] - Manages namespace-scoped BIND9 cluster status
24//! - [`delete_bind9cluster`] - Cleans up namespace-scoped BIND9 cluster resources
25//! - [`reconcile_clusterbind9provider`] - Manages cluster-scoped BIND9 provider status
26//! - [`delete_clusterbind9provider`] - Cleans up cluster-scoped BIND9 provider resources
27//! - [`reconcile_bind9instance`] - Creates/updates BIND9 server deployments
28//! - [`delete_bind9instance`] - Cleans up BIND9 server resources
29//!
30//! ## DNS Zones
31//!
32//! - [`reconcile_dnszone`] - Creates/updates DNS zone files
33//! - [`delete_dnszone`] - Removes DNS zones
34//!
35//! ## DNS Records
36//!
37//! - [`reconcile_a_record`] - Manages IPv4 address records
38//! - [`reconcile_aaaa_record`] - Manages IPv6 address records
39//! - [`reconcile_cname_record`] - Manages canonical name aliases
40//! - [`reconcile_mx_record`] - Manages mail exchange records
41//! - [`reconcile_txt_record`] - Manages text records
42//! - [`reconcile_ns_record`] - Manages nameserver delegation
43//! - [`reconcile_srv_record`] - Manages service location records
44//! - [`reconcile_caa_record`] - Manages certificate authority authorization
45//!
46//! # Example: Using a Reconciler
47//!
48//! ```rust,no_run
49//! use bindy::reconcilers::reconcile_dnszone;
50//! use bindy::crd::DNSZone;
51//! use bindy::bind9::Bind9Manager;
52//! use kube::Client;
53//!
54//! async fn reconcile_zone(dnszone: DNSZone) -> anyhow::Result<()> {
55//! let client = Client::try_default().await?;
56//! let zone_manager = Bind9Manager::new();
57//!
58//! reconcile_dnszone(client, dnszone, &zone_manager).await?;
59//! Ok(())
60//! }
61//! ```
62
63pub mod bind9cluster;
64pub mod bind9instance;
65pub mod clusterbind9provider;
66pub mod dnszone;
67pub mod finalizers;
68pub mod records;
69pub mod resources;
70pub mod status;
71
72#[cfg(test)]
73mod bind9cluster_tests;
74#[cfg(test)]
75mod bind9instance_tests;
76#[cfg(test)]
77mod clusterbind9provider_tests;
78#[cfg(test)]
79mod dnszone_tests;
80#[cfg(test)]
81mod records_tests;
82
83pub use bind9cluster::{delete_bind9cluster, reconcile_bind9cluster};
84pub use bind9instance::{delete_bind9instance, reconcile_bind9instance};
85pub use clusterbind9provider::{delete_clusterbind9provider, reconcile_clusterbind9provider};
86pub use dnszone::{delete_dnszone, find_zones_selecting_record, reconcile_dnszone};
87pub use records::{
88 reconcile_a_record, reconcile_aaaa_record, reconcile_caa_record, reconcile_cname_record,
89 reconcile_mx_record, reconcile_ns_record, reconcile_srv_record, reconcile_txt_record,
90};
91
92/// Check if a resource's spec has changed by comparing generation with `observed_generation`.
93///
94/// This is the standard Kubernetes pattern for determining if reconciliation is needed.
95/// The `metadata.generation` field is incremented by Kubernetes only when the spec changes,
96/// while `status.observed_generation` is set by the controller after processing a spec.
97///
98/// # Arguments
99///
100/// * `current_generation` - The resource's current `metadata.generation`
101/// * `observed_generation` - The controller's last `status.observed_generation`
102///
103/// # Returns
104///
105/// * `true` - Reconciliation is needed (spec changed or first reconciliation)
106/// * `false` - No reconciliation needed (spec unchanged, status-only update)
107///
108/// # Example
109///
110/// ```rust,ignore
111/// use bindy::reconcilers::should_reconcile;
112///
113/// fn check_if_reconcile_needed(resource: &MyResource) -> bool {
114/// let current = resource.metadata.generation;
115/// let observed = resource.status.as_ref()
116/// .and_then(|s| s.observed_generation);
117///
118/// should_reconcile(current, observed)
119/// }
120/// ```
121///
122/// # Kubernetes Generation Semantics
123///
124/// - **`metadata.generation`**: Incremented by Kubernetes API server when spec changes
125/// - **`status.observed_generation`**: Set by controller to match `metadata.generation` after reconciliation
126/// - When they match: spec hasn't changed since last reconciliation → skip work
127/// - When they differ: spec has changed → reconcile
128/// - When `observed_generation` is None: first reconciliation → reconcile
129#[must_use]
130pub fn should_reconcile(current_generation: Option<i64>, observed_generation: Option<i64>) -> bool {
131 match (current_generation, observed_generation) {
132 (Some(current), Some(observed)) => current != observed,
133 (Some(_), None) => true, // First reconciliation
134 _ => false, // No generation tracking available
135 }
136}
137
138/// Check if a status value has actually changed compared to the current status.
139///
140/// This helper prevents unnecessary status updates that would trigger reconciliation loops.
141/// It compares a new status value with the existing status and returns `true` only if
142/// they differ, indicating an update is needed.
143///
144/// # Arguments
145///
146/// * `current_value` - The current status value (from existing resource)
147/// * `new_value` - The new status value to potentially set
148///
149/// # Returns
150///
151/// * `true` - Status has changed and needs updating
152/// * `false` - Status is unchanged, skip the update
153///
154/// # Example
155///
156/// ```rust,ignore
157/// use bindy::reconcilers::status_changed;
158///
159/// let current_ready = instance.status.as_ref()
160/// .and_then(|s| s.ready_replicas);
161/// let new_ready = Some(3);
162///
163/// if status_changed(¤t_ready, &new_ready) {
164/// // Status has changed, safe to update
165/// update_status(client, instance, new_ready).await?;
166/// }
167/// ```
168///
169/// # Why This Matters
170///
171/// In kube-rs, status updates trigger "object updated" events which cause new reconciliations.
172/// Without this check, updating status on every reconciliation creates a tight loop:
173///
174/// 1. Reconcile → Update status
175/// 2. Status update → "object updated" event
176/// 3. Event → New reconciliation
177/// 4. Repeat from step 1 (infinite loop)
178///
179/// By only updating when status actually changes, we break this cycle.
180#[must_use]
181pub fn status_changed<T: PartialEq>(current_value: &Option<T>, new_value: &Option<T>) -> bool {
182 current_value != new_value
183}