bindy/record_wrappers.rs
1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Record reconciliation wrapper helpers and macro.
5//!
6//! This module provides helper functions and a macro to generate reconciliation
7//! wrapper functions for all DNS record types, eliminating ~900 lines of duplicate code.
8
9use crate::crd::RecordStatus;
10use kube::runtime::controller::Action;
11use std::time::Duration;
12
13/// Requeue interval for resources that are ready (5 minutes)
14pub const REQUEUE_WHEN_READY_SECS: u64 = 300;
15
16/// Requeue interval for resources that are not ready (30 seconds)
17pub const REQUEUE_WHEN_NOT_READY_SECS: u64 = 30;
18
19/// Condition type for resource readiness
20pub const CONDITION_TYPE_READY: &str = "Ready";
21
22/// Condition status indicating ready state
23pub const CONDITION_STATUS_TRUE: &str = "True";
24
25/// Error type label for reconciliation errors
26pub const ERROR_TYPE_RECONCILE: &str = "reconcile_error";
27
28/// Check if a resource with status conditions is ready.
29///
30/// A resource is considered ready if it has a status with at least one condition
31/// where type="Ready" and status="True".
32///
33/// # Arguments
34///
35/// * `status` - Optional status containing conditions
36///
37/// # Returns
38///
39/// `true` if the resource is ready, `false` otherwise
40#[must_use]
41pub fn is_resource_ready(status: &Option<RecordStatus>) -> bool {
42 status.as_ref().is_some_and(|s| {
43 s.conditions.first().is_some_and(|condition| {
44 condition.r#type == CONDITION_TYPE_READY && condition.status == CONDITION_STATUS_TRUE
45 })
46 })
47}
48
49/// Determine requeue action based on readiness status.
50///
51/// # Arguments
52///
53/// * `is_ready` - Whether the resource is ready
54///
55/// # Returns
56///
57/// * `Action::requeue(5 minutes)` if ready
58/// * `Action::requeue(30 seconds)` if not ready
59#[must_use]
60pub fn requeue_based_on_readiness(is_ready: bool) -> Action {
61 if is_ready {
62 Action::requeue(Duration::from_secs(REQUEUE_WHEN_READY_SECS))
63 } else {
64 Action::requeue(Duration::from_secs(REQUEUE_WHEN_NOT_READY_SECS))
65 }
66}
67
68/// Macro to generate record reconciliation wrapper functions.
69///
70/// This eliminates ~900 lines of duplicate code by generating identical wrappers
71/// for all 8 DNS record types with only the type and constant names changing.
72///
73/// # Generated Function Pattern
74///
75/// For each record type, generates an async function that:
76/// 1. Tracks reconciliation timing
77/// 2. Calls the type-specific reconcile function
78/// 3. Records metrics (success/error)
79/// 4. Checks resource readiness status
80/// 5. Returns appropriate requeue action
81///
82/// # Example
83///
84/// ```ignore
85/// generate_record_wrapper!(
86/// reconcile_arecord_wrapper, // Function name
87/// ARecord, // Record type
88/// reconcile_a_record, // Reconcile function
89/// KIND_A_RECORD, // Metrics constant
90/// "ARecord" // Display name
91/// );
92/// ```
93#[macro_export]
94macro_rules! generate_record_wrapper {
95 ($wrapper_fn:ident, $record_type:ty, $reconcile_fn:path, $kind_const:path, $display_name:expr) => {
96 pub async fn $wrapper_fn(
97 record: ::std::sync::Arc<$record_type>,
98 ctx: ::std::sync::Arc<(
99 ::kube::Client,
100 ::std::sync::Arc<$crate::bind9::Bind9Manager>,
101 )>,
102 ) -> ::std::result::Result<::kube::runtime::controller::Action, ReconcileError> {
103 let start = ::std::time::Instant::now();
104
105 let result = $reconcile_fn(ctx.0.clone(), (*record).clone()).await;
106 let duration = start.elapsed();
107
108 match result {
109 Ok(()) => {
110 ::tracing::info!(
111 "Successfully reconciled {}: {}",
112 $display_name,
113 ::kube::ResourceExt::name_any(&*record)
114 );
115 $crate::metrics::record_reconciliation_success($kind_const, duration);
116
117 // Fetch the latest status to check if record is ready
118 let namespace = ::kube::ResourceExt::namespace(&*record).unwrap_or_default();
119 let name = ::kube::ResourceExt::name_any(&*record);
120 let api: ::kube::Api<$record_type> =
121 ::kube::Api::namespaced(ctx.0.clone(), &namespace);
122
123 let is_ready = if let Ok(updated_record) = api.get(&name).await {
124 $crate::record_wrappers::is_resource_ready(&updated_record.status)
125 } else {
126 false
127 };
128
129 Ok($crate::record_wrappers::requeue_based_on_readiness(
130 is_ready,
131 ))
132 }
133 Err(e) => {
134 ::tracing::error!("Failed to reconcile {}: {}", $display_name, e);
135 $crate::metrics::record_reconciliation_error($kind_const, duration);
136 $crate::metrics::record_error(
137 $kind_const,
138 $crate::record_wrappers::ERROR_TYPE_RECONCILE,
139 );
140 Err(e.into())
141 }
142 }
143 }
144 };
145}