bindy/reconcilers/bind9cluster/
drift.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Instance drift detection for `Bind9Cluster` resources.
5//!
6//! This module detects when the actual managed instances don't match
7//! the desired replica counts in the cluster spec.
8
9#[allow(clippy::wildcard_imports)]
10use super::types::*;
11use crate::reconcilers::pagination::list_all_paginated;
12
13/// Detects if the actual managed instances match the desired replica counts.
14///
15/// Compares the number of primary and secondary instances that exist against
16/// the desired replica counts in the cluster spec.
17///
18/// # Arguments
19///
20/// * `client` - Kubernetes API client
21/// * `cluster` - The `Bind9Cluster` to check
22/// * `namespace` - Cluster namespace
23/// * `name` - Cluster name
24///
25/// # Returns
26///
27/// * `Ok(true)` - Drift detected (instances don't match desired state)
28/// * `Ok(false)` - No drift (instances match desired state)
29/// * `Err(_)` - Failed to check drift
30///
31/// # Errors
32///
33/// Returns an error if listing instances fails.
34pub(super) async fn detect_instance_drift(
35    client: &Client,
36    cluster: &Bind9Cluster,
37    namespace: &str,
38    name: &str,
39) -> Result<bool> {
40    // Get desired replica counts from spec
41    let desired_primary = cluster
42        .spec
43        .common
44        .primary
45        .as_ref()
46        .and_then(|p| p.replicas)
47        .unwrap_or(0);
48
49    let desired_secondary = cluster
50        .spec
51        .common
52        .secondary
53        .as_ref()
54        .and_then(|s| s.replicas)
55        .unwrap_or(0);
56
57    // List existing managed instances
58    let api: Api<Bind9Instance> = Api::namespaced(client.clone(), namespace);
59    let instances = list_all_paginated(&api, ListParams::default()).await?;
60
61    // Filter for managed instances of this cluster
62    let managed_instances: Vec<_> = instances
63        .into_iter()
64        .filter(|instance| {
65            instance.metadata.labels.as_ref().is_some_and(|labels| {
66                labels.get(BINDY_MANAGED_BY_LABEL) == Some(&MANAGED_BY_BIND9_CLUSTER.to_string())
67                    && labels.get(BINDY_CLUSTER_LABEL) == Some(&name.to_string())
68            })
69        })
70        .collect();
71
72    // Count by role
73    let actual_primary = managed_instances
74        .iter()
75        .filter(|i| i.spec.role == ServerRole::Primary)
76        .count();
77
78    let actual_secondary = managed_instances
79        .iter()
80        .filter(|i| i.spec.role == ServerRole::Secondary)
81        .count();
82
83    // Drift detected if counts don't match
84    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
85    let drift = actual_primary != desired_primary as usize
86        || actual_secondary != desired_secondary as usize;
87
88    if drift {
89        info!(
90            "Instance drift detected for cluster {}/{}: desired (primary={}, secondary={}), actual (primary={}, secondary={})",
91            namespace, name, desired_primary, desired_secondary, actual_primary, actual_secondary
92        );
93    }
94
95    Ok(drift)
96}
97
98#[cfg(test)]
99#[path = "drift_tests.rs"]
100mod drift_tests;