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}