bindy/reconcilers/dnszone/status_helpers.rs
1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Status calculation and finalization helpers for DNSZone reconciliation.
5//!
6//! This module contains functions for calculating expected instance counts
7//! and determining the final Ready/Degraded status of a DNSZone.
8
9use anyhow::Result;
10use kube::Client;
11
12use crate::crd::InstanceReference;
13
14/// Calculate expected instance counts (primary and secondary).
15///
16/// This function filters the instance references to determine how many
17/// primary and secondary instances should be configured.
18///
19/// # Arguments
20///
21/// * `client` - Kubernetes API client
22/// * `instance_refs` - List of instance references assigned to the zone
23///
24/// # Returns
25///
26/// Tuple of `(expected_primary_count, expected_secondary_count)`
27///
28/// # Errors
29///
30/// Returns an error if Kubernetes API calls fail
31pub async fn calculate_expected_instance_counts(
32 client: &Client,
33 instance_refs: &[InstanceReference],
34) -> Result<(usize, usize)> {
35 let expected_primary_count = super::primary::filter_primary_instances(client, instance_refs)
36 .await
37 .map(|refs| refs.len())
38 .unwrap_or(0);
39
40 let expected_secondary_count =
41 super::secondary::filter_secondary_instances(client, instance_refs)
42 .await
43 .map(|refs| refs.len())
44 .unwrap_or(0);
45
46 Ok((expected_primary_count, expected_secondary_count))
47}
48
49/// Determine final zone status and apply conditions.
50///
51/// This function calculates the final Ready or Degraded status based on:
52/// - Whether any degraded conditions were set during reconciliation
53/// - Whether all expected instances were successfully configured
54/// - Number of records discovered
55///
56/// The function then applies all accumulated status changes to the API server
57/// in a single atomic operation.
58///
59/// # Arguments
60///
61/// * `status_updater` - Status updater with accumulated changes
62/// * `client` - Kubernetes API client
63/// * `zone_name` - DNS zone name (e.g., "example.com")
64/// * `namespace` - Kubernetes namespace of the DNSZone resource
65/// * `name` - Name of the DNSZone resource
66/// * `primary_count` - Number of primary instances successfully configured
67/// * `secondary_count` - Number of secondary instances successfully configured
68/// * `expected_primary_count` - Expected number of primary instances
69/// * `expected_secondary_count` - Expected number of secondary instances
70/// * `records_count` - Number of DNS records discovered
71/// * `generation` - Metadata generation to set as observed
72///
73/// # Errors
74///
75/// Returns an error if status update fails to apply
76#[allow(clippy::too_many_arguments)]
77pub async fn finalize_zone_status(
78 status_updater: &mut crate::reconcilers::status::DNSZoneStatusUpdater,
79 client: &Client,
80 zone_name: &str,
81 namespace: &str,
82 name: &str,
83 primary_count: usize,
84 secondary_count: usize,
85 expected_primary_count: usize,
86 expected_secondary_count: usize,
87 records_count: usize,
88 generation: Option<i64>,
89) -> Result<()> {
90 // Set observed generation
91 status_updater.set_observed_generation(generation);
92
93 // Set final Ready/Degraded status based on reconciliation outcome
94 // Only set Ready=True if there were NO degraded conditions during reconciliation
95 // AND all expected instances were successfully configured
96 if status_updater.has_degraded_condition() {
97 // Keep the Degraded condition that was already set, don't overwrite with Ready
98 tracing::info!(
99 "DNSZone {}/{} reconciliation completed with degraded state - will retry faster",
100 namespace,
101 name
102 );
103 } else if primary_count < expected_primary_count || secondary_count < expected_secondary_count {
104 // Not all instances were configured - set Degraded condition
105 status_updater.set_condition(
106 "Degraded",
107 "True",
108 "PartialReconciliation",
109 &format!(
110 "Zone {} configured on {}/{} primary and {}/{} secondary instance(s) - {} instance(s) pending",
111 zone_name,
112 primary_count,
113 expected_primary_count,
114 secondary_count,
115 expected_secondary_count,
116 (expected_primary_count - primary_count)
117 + (expected_secondary_count - secondary_count)
118 ),
119 );
120 tracing::info!(
121 "DNSZone {}/{} partially configured: {}/{} primaries, {}/{} secondaries",
122 namespace,
123 name,
124 primary_count,
125 expected_primary_count,
126 secondary_count,
127 expected_secondary_count
128 );
129 } else {
130 // All reconciliation steps succeeded - set Ready status and clear any stale Degraded condition
131 status_updater.set_condition(
132 "Ready",
133 "True",
134 "ReconcileSucceeded",
135 &format!(
136 "Zone {} configured on {} primary and {} secondary instance(s), discovered {} DNS record(s)",
137 zone_name, primary_count, secondary_count, records_count
138 ),
139 );
140 // Clear any stale Degraded condition from previous failures
141 status_updater.clear_degraded_condition();
142 }
143
144 // Apply all status changes in a single atomic operation
145 status_updater.apply(client).await?;
146
147 Ok(())
148}
149
150#[cfg(test)]
151#[path = "status_helpers_tests.rs"]
152mod status_helpers_tests;