bindy/reconcilers/bind9instance/
zones.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Zone reconciliation logic for `Bind9Instance` resources.
5//!
6//! This module handles updating the instance status with zone references
7//! when zones select this instance.
8
9#[allow(clippy::wildcard_imports)]
10use super::types::*;
11
12use crate::constants::API_GROUP_VERSION;
13
14/// Reconciles the zones list for a `Bind9Instance` based on current `DNSZone` state.
15///
16/// This function performs status-only updates by:
17/// 1. Querying all `DNSZones` from the reflector store (in-memory, no API call)
18/// 2. Filtering to zones that have this instance in their `status.bind9Instances`
19/// 3. Patching the instance's `status.zones` field if changed
20///
21/// # Arguments
22///
23/// * `client` - Kubernetes API client for status patching
24/// * `stores` - Reflector stores containing all `DNSZones`
25/// * `instance` - The `Bind9Instance` to reconcile zones for
26///
27/// # Returns
28///
29/// * `Ok(())` - If zone reconciliation succeeded (or no change needed)
30///
31/// # Errors
32///
33/// Returns an error if the Kubernetes API status patch fails.
34pub async fn reconcile_instance_zones(
35    client: &Client,
36    stores: &crate::context::Stores,
37    instance: &Bind9Instance,
38) -> Result<()> {
39    let namespace = instance.namespace().unwrap_or_default();
40    let instance_name = instance.name_any();
41
42    // Get all DNSZones from reflector store (no API call)
43    let all_zones = stores.dnszones.state();
44
45    let mut new_zones = Vec::new();
46
47    // Filter zones that have this instance in their status.bind9Instances
48    for zone in &all_zones {
49        let zone_namespace = zone.namespace().unwrap_or_default();
50
51        // Only consider zones in the same namespace
52        if zone_namespace != namespace {
53            continue;
54        }
55
56        // Check if this instance is in the zone's status.bind9instances list
57        if let Some(status) = &zone.status {
58            let instance_found = status
59                .bind9_instances
60                .iter()
61                .any(|inst_ref| inst_ref.name == instance_name && inst_ref.namespace == namespace);
62
63            if instance_found {
64                new_zones.push(ZoneReference {
65                    api_version: API_GROUP_VERSION.to_string(),
66                    kind: crate::constants::KIND_DNS_ZONE.to_string(),
67                    name: zone.name_any(),
68                    namespace: zone_namespace,
69                    zone_name: zone.spec.zone_name.clone(),
70                    last_reconciled_at: None, // Populated by DNSZone reconciler
71                });
72            }
73        }
74    }
75
76    // Check if zones changed (avoid unnecessary patches)
77    let current_zones = instance
78        .status
79        .as_ref()
80        .map(|s| s.zones.clone())
81        .unwrap_or_default();
82
83    if zones_equal(&current_zones, &new_zones) {
84        debug!(
85            "Zones unchanged for Bind9Instance {}/{}, skipping status patch",
86            namespace, instance_name
87        );
88        return Ok(());
89    }
90
91    // Patch status with new zones list and zones_count
92    let api: Api<Bind9Instance> = Api::namespaced(client.clone(), &namespace);
93
94    let zones_count = i32::try_from(new_zones.len()).ok();
95
96    let status_patch = serde_json::json!({
97        "status": {
98            "zones": new_zones,
99            "zonesCount": zones_count
100        }
101    });
102
103    api.patch_status(
104        &instance_name,
105        &PatchParams::default(),
106        &Patch::Merge(&status_patch),
107    )
108    .await?;
109
110    info!(
111        "Updated zones for Bind9Instance {}/{}: {} zone(s)",
112        namespace,
113        instance_name,
114        new_zones.len()
115    );
116
117    Ok(())
118}
119
120/// Compare two zone lists for equality (order-independent).
121///
122/// This helper function compares zone lists by content, not order.
123/// Two lists are equal if they contain the same zones (by name and namespace).
124pub(super) fn zones_equal(a: &[ZoneReference], b: &[ZoneReference]) -> bool {
125    if a.len() != b.len() {
126        return false;
127    }
128
129    // Create sets of (name, namespace) tuples for comparison
130    let set_a: std::collections::HashSet<_> = a.iter().map(|z| (&z.name, &z.namespace)).collect();
131    let set_b: std::collections::HashSet<_> = b.iter().map(|z| (&z.name, &z.namespace)).collect();
132
133    set_a == set_b
134}
135
136#[cfg(test)]
137#[path = "zones_tests.rs"]
138mod zones_tests;