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;