bindy/
context.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Shared context for all operators with reflector stores.
5//!
6//! This module provides the core infrastructure for the shared reflector store pattern.
7//! All operators receive an `Arc<Context>` that contains:
8//! - Kubernetes client
9//! - Reflector stores for all CRD types
10//! - Metrics registry
11//!
12//! The stores enable O(1) in-memory lookups for label-based resource selection,
13//! eliminating the need for API queries in watch mappers.
14
15use crate::crd::{
16    AAAARecord, ARecord, Bind9Cluster, Bind9Instance, CAARecord, CNAMERecord, ClusterBind9Provider,
17    DNSZone, LabelSelector, MXRecord, NSRecord, SRVRecord, TXTRecord,
18};
19use k8s_openapi::api::apps::v1::Deployment;
20use kube::runtime::reflector::Store;
21use kube::{Client, ResourceExt};
22use std::collections::BTreeMap;
23use std::sync::Arc;
24
25/// Shared context passed to all operators.
26///
27/// This context provides access to:
28/// - Kubernetes client for API operations
29/// - Reflector stores for efficient label-based queries
30/// - HTTP client for bindcar API calls
31/// - Metrics for observability
32#[derive(Clone)]
33pub struct Context {
34    /// Kubernetes client for API operations
35    pub client: Client,
36
37    /// Reflector stores for all CRD types
38    pub stores: Stores,
39
40    /// HTTP client for bindcar zone synchronization API calls
41    pub http_client: reqwest::Client,
42
43    /// Metrics registry for observability
44    pub metrics: Metrics,
45}
46
47/// Collection of all reflector stores for cross-operator queries.
48///
49/// Each store is populated by a dedicated reflector task and provides
50/// in-memory access to resources without API calls.
51#[derive(Clone)]
52pub struct Stores {
53    // Cluster-scoped resources
54    pub cluster_bind9_providers: Store<ClusterBind9Provider>,
55
56    // Namespace-scoped resources
57    pub bind9_clusters: Store<Bind9Cluster>,
58    pub bind9_instances: Store<Bind9Instance>,
59    pub bind9_deployments: Store<Deployment>,
60    pub dnszones: Store<DNSZone>,
61
62    // DNS Record types
63    pub a_records: Store<ARecord>,
64    pub aaaa_records: Store<AAAARecord>,
65    pub cname_records: Store<CNAMERecord>,
66    pub txt_records: Store<TXTRecord>,
67    pub mx_records: Store<MXRecord>,
68    pub ns_records: Store<NSRecord>,
69    pub srv_records: Store<SRVRecord>,
70    pub caa_records: Store<CAARecord>,
71}
72
73impl Stores {
74    /// Query all record stores and return matching records for a label selector.
75    ///
76    /// This method searches across all 8 record type stores to find records that:
77    /// 1. Exist in the specified namespace
78    /// 2. Match the provided label selector
79    ///
80    /// # Arguments
81    /// * `selector` - The label selector to match against record labels
82    /// * `namespace` - The namespace to search within (namespace-isolated)
83    ///
84    /// # Returns
85    /// A vector of [`RecordRef`] enums containing references to all matching records
86    #[must_use]
87    pub fn records_matching_selector(
88        &self,
89        selector: &LabelSelector,
90        namespace: &str,
91    ) -> Vec<RecordRef> {
92        let mut results = Vec::new();
93
94        // Helper macro to reduce boilerplate
95        macro_rules! collect_matching {
96            ($store:expr, $variant:ident) => {
97                for record in $store.state() {
98                    if record.namespace().as_deref() == Some(namespace)
99                        && crate::selector::matches_selector(selector, &record.labels())
100                    {
101                        results.push(RecordRef::$variant(
102                            record.name_any(),
103                            record.namespace().unwrap_or_default(),
104                        ));
105                    }
106                }
107            };
108        }
109
110        collect_matching!(self.a_records, A);
111        collect_matching!(self.aaaa_records, AAAA);
112        collect_matching!(self.cname_records, CNAME);
113        collect_matching!(self.txt_records, TXT);
114        collect_matching!(self.mx_records, MX);
115        collect_matching!(self.ns_records, NS);
116        collect_matching!(self.srv_records, SRV);
117        collect_matching!(self.caa_records, CAA);
118
119        results
120    }
121
122    /// Query dnszones matching a label selector.
123    ///
124    /// # Arguments
125    /// * `selector` - The label selector to match against zone labels
126    /// * `namespace` - The namespace to search within
127    ///
128    /// # Returns
129    /// A vector of (name, namespace) tuples for matching zones
130    #[must_use]
131    pub fn dnszones_matching_selector(
132        &self,
133        selector: &LabelSelector,
134        namespace: &str,
135    ) -> Vec<(String, String)> {
136        self.dnszones
137            .state()
138            .iter()
139            .filter(|zone| {
140                zone.namespace().as_deref() == Some(namespace)
141                    && crate::selector::matches_selector(selector, zone.labels())
142            })
143            .map(|zone| (zone.name_any(), zone.namespace().unwrap_or_default()))
144            .collect()
145    }
146
147    /// Query `Bind9Instance`s matching a label selector.
148    ///
149    /// # Arguments
150    /// * `selector` - The label selector to match against instance labels
151    /// * `namespace` - The namespace to search within
152    ///
153    /// # Returns
154    /// A vector of (name, namespace) tuples for matching instances
155    #[must_use]
156    pub fn bind9instances_matching_selector(
157        &self,
158        selector: &LabelSelector,
159        namespace: &str,
160    ) -> Vec<(String, String)> {
161        self.bind9_instances
162            .state()
163            .iter()
164            .filter(|inst| {
165                inst.namespace().as_deref() == Some(namespace)
166                    && crate::selector::matches_selector(selector, inst.labels())
167            })
168            .map(|inst| (inst.name_any(), inst.namespace().unwrap_or_default()))
169            .collect()
170    }
171
172    /// Find all `DNSZone`s whose `recordsFrom` selector matches given record labels.
173    ///
174    /// This is a "reverse lookup" - given a record's labels, find which zones select it.
175    /// Used by record watch mappers to determine which zones need reconciliation
176    /// when a record changes.
177    ///
178    /// # Arguments
179    /// * `record_labels` - The labels of the record to match
180    /// * `record_namespace` - The namespace of the record
181    ///
182    /// # Returns
183    /// A vector of (name, namespace) tuples for zones that select this record
184    #[must_use]
185    pub fn dnszones_selecting_record(
186        &self,
187        record_labels: &BTreeMap<String, String>,
188        record_namespace: &str,
189    ) -> Vec<(String, String)> {
190        self.dnszones
191            .state()
192            .iter()
193            .filter(|zone| {
194                zone.namespace().as_deref() == Some(record_namespace)
195                    && zone.spec.records_from.as_ref().is_some_and(|sources| {
196                        sources.iter().any(|source| {
197                            crate::selector::matches_selector(&source.selector, record_labels)
198                        })
199                    })
200            })
201            .map(|zone| (zone.name_any(), zone.namespace().unwrap_or_default()))
202            .collect()
203    }
204
205    /// Get a specific `DNSZone` by name and namespace from the store.
206    ///
207    /// # Arguments
208    /// * `name` - The name of the zone
209    /// * `namespace` - The namespace of the zone
210    ///
211    /// # Returns
212    /// An [`Arc<DNSZone>`] if found, `None` otherwise
213    #[must_use]
214    pub fn get_dnszone(&self, name: &str, namespace: &str) -> Option<Arc<DNSZone>> {
215        self.dnszones
216            .state()
217            .iter()
218            .find(|zone| zone.name_any() == name && zone.namespace().as_deref() == Some(namespace))
219            .cloned()
220    }
221
222    /// Get a specific `Bind9Instance` by name and namespace from the store.
223    ///
224    /// # Arguments
225    /// * `name` - The name of the instance
226    /// * `namespace` - The namespace of the instance
227    ///
228    /// # Returns
229    /// An [`Arc<Bind9Instance>`] if found, `None` otherwise
230    #[must_use]
231    pub fn get_bind9instance(&self, name: &str, namespace: &str) -> Option<Arc<Bind9Instance>> {
232        self.bind9_instances
233            .state()
234            .iter()
235            .find(|inst| inst.name_any() == name && inst.namespace().as_deref() == Some(namespace))
236            .cloned()
237    }
238
239    /// Get a specific `Deployment` by name and namespace from the store.
240    ///
241    /// # Arguments
242    /// * `name` - The name of the deployment
243    /// * `namespace` - The namespace of the deployment
244    ///
245    /// # Returns
246    /// An [`Arc<Deployment>`] if found, `None` otherwise
247    #[must_use]
248    pub fn get_deployment(&self, name: &str, namespace: &str) -> Option<Arc<Deployment>> {
249        self.bind9_deployments
250            .state()
251            .iter()
252            .find(|dep| {
253                dep.metadata.name.as_deref() == Some(name)
254                    && dep.metadata.namespace.as_deref() == Some(namespace)
255            })
256            .cloned()
257    }
258
259    /// Create a `Bind9Manager` for a specific instance with deployment-aware auth.
260    ///
261    /// This helper function looks up the deployment for the given instance and creates
262    /// a `Bind9Manager` with proper authentication detection. If the deployment is found,
263    /// it creates a manager that can determine auth status by inspecting the bindcar
264    /// container's environment variables. If not found, it falls back to a basic manager
265    /// that assumes auth is enabled.
266    ///
267    /// # Arguments
268    /// * `instance_name` - Name of the `Bind9Instance`
269    /// * `instance_namespace` - Namespace of the instance
270    ///
271    /// # Returns
272    /// A `Bind9Manager` configured for the instance
273    ///
274    /// # Examples
275    ///
276    /// ```rust,no_run
277    /// # use bindy::context::Stores;
278    /// # fn example(stores: &Stores) {
279    /// let manager = stores.create_bind9_manager_for_instance(
280    ///     "my-instance",
281    ///     "bindy-system"
282    /// );
283    /// # }
284    /// ```
285    #[must_use]
286    pub fn create_bind9_manager_for_instance(
287        &self,
288        instance_name: &str,
289        instance_namespace: &str,
290    ) -> crate::bind9::Bind9Manager {
291        // Try to get the deployment for this instance
292        if let Some(deployment) = self.get_deployment(instance_name, instance_namespace) {
293            // Found deployment - create manager with auth detection
294            crate::bind9::Bind9Manager::new_with_deployment(
295                deployment,
296                instance_name.to_string(),
297                instance_namespace.to_string(),
298            )
299        } else {
300            // No deployment found - fall back to basic manager (auth assumed enabled)
301            tracing::debug!(
302                instance = instance_name,
303                namespace = instance_namespace,
304                "Deployment not found in store, using basic Bind9Manager (auth enabled)"
305            );
306            crate::bind9::Bind9Manager::new()
307        }
308    }
309}
310
311/// Enum representing a reference to any DNS record type.
312///
313/// This enum provides a type-safe way to reference records of different types
314/// in a unified collection. Each variant contains the name and namespace of the record.
315#[derive(Clone, Debug, PartialEq, Eq)]
316pub enum RecordRef {
317    /// A record (IPv4 address)
318    A(String, String),
319    /// AAAA record (IPv6 address)
320    AAAA(String, String),
321    /// CNAME record (canonical name)
322    CNAME(String, String),
323    /// TXT record (text data)
324    TXT(String, String),
325    /// MX record (mail exchange)
326    MX(String, String),
327    /// NS record (name server)
328    NS(String, String),
329    /// SRV record (service locator)
330    SRV(String, String),
331    /// CAA record (certificate authority authorization)
332    CAA(String, String),
333}
334
335impl RecordRef {
336    /// Get the name of the record.
337    #[must_use]
338    pub fn name(&self) -> &str {
339        match self {
340            RecordRef::A(name, _)
341            | RecordRef::AAAA(name, _)
342            | RecordRef::CNAME(name, _)
343            | RecordRef::TXT(name, _)
344            | RecordRef::MX(name, _)
345            | RecordRef::NS(name, _)
346            | RecordRef::SRV(name, _)
347            | RecordRef::CAA(name, _) => name,
348        }
349    }
350
351    /// Get the namespace of the record.
352    #[must_use]
353    pub fn namespace(&self) -> &str {
354        match self {
355            RecordRef::A(_, ns)
356            | RecordRef::AAAA(_, ns)
357            | RecordRef::CNAME(_, ns)
358            | RecordRef::TXT(_, ns)
359            | RecordRef::MX(_, ns)
360            | RecordRef::NS(_, ns)
361            | RecordRef::SRV(_, ns)
362            | RecordRef::CAA(_, ns) => ns,
363        }
364    }
365
366    /// Get the record type as a string.
367    #[must_use]
368    pub fn record_type(&self) -> &str {
369        match self {
370            RecordRef::A(_, _) => "A",
371            RecordRef::AAAA(_, _) => "AAAA",
372            RecordRef::CNAME(_, _) => "CNAME",
373            RecordRef::TXT(_, _) => "TXT",
374            RecordRef::MX(_, _) => "MX",
375            RecordRef::NS(_, _) => "NS",
376            RecordRef::SRV(_, _) => "SRV",
377            RecordRef::CAA(_, _) => "CAA",
378        }
379    }
380}
381
382/// Metrics for observability.
383///
384/// This struct will hold Prometheus metrics for monitoring operator behavior.
385/// For now, it's a placeholder that can be extended with actual metrics.
386#[derive(Clone, Default)]
387pub struct Metrics {
388    // Future: Add prometheus metrics here
389    // pub reconciliations_total: IntCounter,
390    // pub reconciliation_errors_total: IntCounter,
391    // pub reconciliation_duration: Histogram,
392    // pub store_size_dnszones: IntGauge,
393    // pub store_size_records: IntGauge,
394    // pub store_size_instances: IntGauge,
395}
396
397#[cfg(test)]
398#[path = "context_tests.rs"]
399mod context_tests;