bindy/crd.rs
1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Custom Resource Definitions (CRDs) for DNS management.
5//!
6//! This module defines all Kubernetes Custom Resource Definitions used by Bindy
7//! to manage BIND9 DNS infrastructure declaratively.
8//!
9//! # Resource Types
10//!
11//! ## Infrastructure
12//!
13//! - [`Bind9Instance`] - Represents a BIND9 DNS server deployment
14//!
15//! ## DNS Zones
16//!
17//! - [`DNSZone`] - Defines DNS zones with SOA records and instance targeting
18//!
19//! ## DNS Records
20//!
21//! - [`ARecord`] - IPv4 address records
22//! - [`AAAARecord`] - IPv6 address records
23//! - [`CNAMERecord`] - Canonical name (alias) records
24//! - [`MXRecord`] - Mail exchange records
25//! - [`TXTRecord`] - Text records (SPF, DKIM, DMARC, etc.)
26//! - [`NSRecord`] - Nameserver delegation records
27//! - [`SRVRecord`] - Service location records
28//! - [`CAARecord`] - Certificate authority authorization records
29//!
30//! # Example: Creating a DNS Zone
31//!
32//! ```rust,no_run,ignore
33//! use bindy::crd::{DNSZoneSpec, SOARecord};
34//!
35//! let soa = SOARecord {
36//! primary_ns: "ns1.example.com.".to_string(),
37//! admin_email: "admin@example.com".to_string(),
38//! serial: 2024010101,
39//! refresh: 3600,
40//! retry: 600,
41//! expire: 604800,
42//! negative_ttl: 86400,
43//! };
44//!
45//! // Example showing DNSZone spec structure
46//! // Note: Actual spec fields may vary - see DNSZoneSpec definition
47//! let spec = DNSZoneSpec {
48//! zone_name: "example.com".to_string(),
49//! soa_record: soa,
50//! ttl: Some(3600),
51//! name_server_ips: None,
52//! records_from: None,
53//! };
54//! ```
55//!
56//! # Example: Creating DNS Records
57//!
58//! ```rust,no_run
59//! use bindy::crd::{ARecordSpec, MXRecordSpec};
60//!
61//! // A Record for www.example.com
62//! let a_record = ARecordSpec {
63//! name: "www".to_string(),
64//! ipv4_addresses: vec!["192.0.2.1".to_string()],
65//! ttl: Some(300),
66//! };
67//!
68//! // MX Record for mail routing
69//! let mx_record = MXRecordSpec {
70//! name: "@".to_string(),
71//! priority: 10,
72//! mail_server: "mail.example.com.".to_string(),
73//! ttl: Some(3600),
74//! };
75//! ```
76
77use k8s_openapi::api::core::v1::{EnvVar, ServiceSpec, Volume, VolumeMount};
78use kube::CustomResource;
79use schemars::JsonSchema;
80use serde::{Deserialize, Serialize};
81use std::collections::{BTreeMap, HashMap};
82
83/// DNS record kind (type) enumeration.
84///
85/// Represents the Kubernetes `kind` field for all DNS record custom resources.
86/// This enum eliminates magic strings when matching record types and provides
87/// type-safe conversions between string representations and enum values.
88///
89/// # Example
90///
91/// ```rust,ignore
92/// use bindy::crd::DNSRecordKind;
93///
94/// // Parse from string
95/// let kind = DNSRecordKind::from("ARecord");
96/// assert_eq!(kind, DNSRecordKind::A);
97///
98/// // Convert to string
99/// assert_eq!(kind.as_str(), "ARecord");
100/// ```
101#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
102pub enum DNSRecordKind {
103 /// IPv4 address record (A)
104 A,
105 /// IPv6 address record (AAAA)
106 AAAA,
107 /// Text record (TXT)
108 TXT,
109 /// Canonical name record (CNAME)
110 CNAME,
111 /// Mail exchange record (MX)
112 MX,
113 /// Nameserver record (NS)
114 NS,
115 /// Service record (SRV)
116 SRV,
117 /// Certificate authority authorization record (CAA)
118 CAA,
119}
120
121impl DNSRecordKind {
122 /// Returns the Kubernetes `kind` string for this record type.
123 ///
124 /// # Example
125 ///
126 /// ```rust,ignore
127 /// use bindy::crd::DNSRecordKind;
128 ///
129 /// assert_eq!(DNSRecordKind::A.as_str(), "ARecord");
130 /// assert_eq!(DNSRecordKind::MX.as_str(), "MXRecord");
131 /// ```
132 #[must_use]
133 pub const fn as_str(self) -> &'static str {
134 match self {
135 Self::A => "ARecord",
136 Self::AAAA => "AAAARecord",
137 Self::TXT => "TXTRecord",
138 Self::CNAME => "CNAMERecord",
139 Self::MX => "MXRecord",
140 Self::NS => "NSRecord",
141 Self::SRV => "SRVRecord",
142 Self::CAA => "CAARecord",
143 }
144 }
145
146 /// Returns all DNS record kinds as a slice.
147 ///
148 /// Useful for iterating over all supported record types.
149 ///
150 /// # Example
151 ///
152 /// ```rust,ignore
153 /// use bindy::crd::DNSRecordKind;
154 ///
155 /// for kind in DNSRecordKind::all() {
156 /// println!("Record type: {}", kind.as_str());
157 /// }
158 /// ```
159 #[must_use]
160 pub const fn all() -> &'static [Self] {
161 &[
162 Self::A,
163 Self::AAAA,
164 Self::TXT,
165 Self::CNAME,
166 Self::MX,
167 Self::NS,
168 Self::SRV,
169 Self::CAA,
170 ]
171 }
172
173 /// Converts this DNS record kind to a Hickory DNS `RecordType`.
174 ///
175 /// This is useful when interfacing with the Hickory DNS library for
176 /// zone file generation or DNS protocol operations.
177 ///
178 /// # Example
179 ///
180 /// ```rust,ignore
181 /// use bindy::crd::DNSRecordKind;
182 /// use hickory_client::rr::RecordType;
183 ///
184 /// let kind = DNSRecordKind::A;
185 /// let record_type = kind.to_hickory_record_type();
186 /// assert_eq!(record_type, RecordType::A);
187 /// ```
188 #[must_use]
189 pub const fn to_hickory_record_type(self) -> hickory_client::rr::RecordType {
190 use hickory_client::rr::RecordType;
191 match self {
192 Self::A => RecordType::A,
193 Self::AAAA => RecordType::AAAA,
194 Self::TXT => RecordType::TXT,
195 Self::CNAME => RecordType::CNAME,
196 Self::MX => RecordType::MX,
197 Self::NS => RecordType::NS,
198 Self::SRV => RecordType::SRV,
199 Self::CAA => RecordType::CAA,
200 }
201 }
202}
203
204impl From<&str> for DNSRecordKind {
205 fn from(s: &str) -> Self {
206 match s {
207 "ARecord" => Self::A,
208 "AAAARecord" => Self::AAAA,
209 "TXTRecord" => Self::TXT,
210 "CNAMERecord" => Self::CNAME,
211 "MXRecord" => Self::MX,
212 "NSRecord" => Self::NS,
213 "SRVRecord" => Self::SRV,
214 "CAARecord" => Self::CAA,
215 _ => panic!("Unknown DNS record kind: {s}"),
216 }
217 }
218}
219
220impl From<String> for DNSRecordKind {
221 fn from(s: String) -> Self {
222 Self::from(s.as_str())
223 }
224}
225
226impl std::fmt::Display for DNSRecordKind {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(f, "{}", self.as_str())
229 }
230}
231
232/// Label selector to match Kubernetes resources.
233///
234/// A label selector is a label query over a set of resources. The result of matchLabels and
235/// matchExpressions are `ANDed`. An empty label selector matches all objects.
236#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
237#[serde(rename_all = "camelCase")]
238pub struct LabelSelector {
239 /// Map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent
240 /// to an element of matchExpressions, whose key field is "key", the operator is "In",
241 /// and the values array contains only "value". All requirements must be satisfied.
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub match_labels: Option<BTreeMap<String, String>>,
244
245 /// List of label selector requirements. All requirements must be satisfied.
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub match_expressions: Option<Vec<LabelSelectorRequirement>>,
248}
249
250/// A label selector requirement is a selector that contains values, a key, and an operator
251/// that relates the key and values.
252#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
253#[serde(rename_all = "camelCase")]
254pub struct LabelSelectorRequirement {
255 /// The label key that the selector applies to.
256 pub key: String,
257
258 /// Operator represents a key's relationship to a set of values.
259 /// Valid operators are In, `NotIn`, Exists and `DoesNotExist`.
260 pub operator: String,
261
262 /// An array of string values. If the operator is In or `NotIn`,
263 /// the values array must be non-empty. If the operator is Exists or `DoesNotExist`,
264 /// the values array must be empty.
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub values: Option<Vec<String>>,
267}
268
269/// Source for DNS records to include in a zone.
270///
271/// Specifies how DNS records should be associated with this zone using label selectors.
272/// Records matching the selector criteria will be automatically included in the zone.
273///
274/// # Example
275///
276/// ```yaml
277/// recordsFrom:
278/// - selector:
279/// matchLabels:
280/// app: podinfo
281/// matchExpressions:
282/// - key: environment
283/// operator: In
284/// values:
285/// - dev
286/// - staging
287/// ```
288#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
289#[serde(rename_all = "camelCase")]
290pub struct RecordSource {
291 /// Label selector to match DNS records.
292 ///
293 /// Records (`ARecord`, `CNAMERecord`, `MXRecord`, etc.) with labels matching this selector
294 /// will be automatically associated with this zone.
295 ///
296 /// The selector uses standard Kubernetes label selector semantics:
297 /// - `matchLabels`: All specified labels must match (AND logic)
298 /// - `matchExpressions`: All expressions must be satisfied (AND logic)
299 /// - Both `matchLabels` and `matchExpressions` can be used together
300 pub selector: LabelSelector,
301}
302
303/// Source for `Bind9Instance` resources to target for zone configuration.
304///
305/// Specifies how `Bind9Instance` resources should be selected using label selectors.
306/// The `DNSZone` controller will configure zones on all matching instances.
307///
308/// # Example
309///
310/// ```yaml
311/// instancesFrom:
312/// - selector:
313/// matchLabels:
314/// environment: production
315/// role: primary
316/// matchExpressions:
317/// - key: region
318/// operator: In
319/// values:
320/// - us-east-1
321/// - us-west-2
322/// ```
323#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
324#[serde(rename_all = "camelCase")]
325pub struct InstanceSource {
326 /// Label selector to match `Bind9Instance` resources.
327 ///
328 /// `Bind9Instance` resources with labels matching this selector will be automatically
329 /// targeted for zone configuration by this `DNSZone`.
330 ///
331 /// The selector uses standard Kubernetes label selector semantics:
332 /// - `matchLabels`: All specified labels must match (AND logic)
333 /// - `matchExpressions`: All expressions must be satisfied (AND logic)
334 /// - Both `matchLabels` and `matchExpressions` can be used together
335 pub selector: LabelSelector,
336}
337
338impl LabelSelector {
339 /// Checks if this label selector matches the given labels.
340 ///
341 /// Returns `true` if all match requirements are satisfied.
342 ///
343 /// # Arguments
344 ///
345 /// * `labels` - The labels to match against (from `metadata.labels`)
346 ///
347 /// # Examples
348 ///
349 /// ```rust
350 /// use std::collections::BTreeMap;
351 /// use bindy::crd::LabelSelector;
352 ///
353 /// let selector = LabelSelector {
354 /// match_labels: Some(BTreeMap::from([
355 /// ("app".to_string(), "podinfo".to_string()),
356 /// ])),
357 /// match_expressions: None,
358 /// };
359 ///
360 /// let labels = BTreeMap::from([
361 /// ("app".to_string(), "podinfo".to_string()),
362 /// ("env".to_string(), "dev".to_string()),
363 /// ]);
364 ///
365 /// assert!(selector.matches(&labels));
366 /// ```
367 #[must_use]
368 pub fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
369 // Check matchLabels (all must match)
370 if let Some(ref match_labels) = self.match_labels {
371 for (key, value) in match_labels {
372 if labels.get(key) != Some(value) {
373 return false;
374 }
375 }
376 }
377
378 // Check matchExpressions (all must be satisfied)
379 if let Some(ref expressions) = self.match_expressions {
380 for expr in expressions {
381 if !expr.matches(labels) {
382 return false;
383 }
384 }
385 }
386
387 true
388 }
389}
390
391impl LabelSelectorRequirement {
392 /// Checks if this requirement matches the given labels.
393 ///
394 /// # Arguments
395 ///
396 /// * `labels` - The labels to match against
397 ///
398 /// # Returns
399 ///
400 /// * `true` if the requirement is satisfied, `false` otherwise
401 #[must_use]
402 pub fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
403 match self.operator.as_str() {
404 "In" => {
405 // Label value must be in the values list
406 if let Some(ref values) = self.values {
407 if let Some(label_value) = labels.get(&self.key) {
408 values.contains(label_value)
409 } else {
410 false
411 }
412 } else {
413 false
414 }
415 }
416 "NotIn" => {
417 // Label value must NOT be in the values list
418 if let Some(ref values) = self.values {
419 if let Some(label_value) = labels.get(&self.key) {
420 !values.contains(label_value)
421 } else {
422 true // Label doesn't exist, so it's not in the list
423 }
424 } else {
425 true
426 }
427 }
428 "Exists" => {
429 // Label key must exist (any value)
430 labels.contains_key(&self.key)
431 }
432 "DoesNotExist" => {
433 // Label key must NOT exist
434 !labels.contains_key(&self.key)
435 }
436 _ => false, // Unknown operator
437 }
438 }
439}
440
441/// SOA (Start of Authority) Record specification.
442///
443/// The SOA record defines authoritative information about a DNS zone, including
444/// the primary nameserver, responsible party's email, and timing parameters for
445/// zone transfers and caching.
446///
447/// # Example
448///
449/// ```rust
450/// use bindy::crd::SOARecord;
451///
452/// let soa = SOARecord {
453/// primary_ns: "ns1.example.com.".to_string(),
454/// admin_email: "admin.example.com.".to_string(), // Note: @ replaced with .
455/// serial: 2024010101,
456/// refresh: 3600, // Check for updates every hour
457/// retry: 600, // Retry after 10 minutes on failure
458/// expire: 604800, // Expire after 1 week
459/// negative_ttl: 86400, // Cache negative responses for 1 day
460/// };
461/// ```
462#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
463#[serde(rename_all = "camelCase")]
464pub struct SOARecord {
465 /// Primary nameserver for this zone (must be a FQDN ending with .).
466 ///
467 /// Example: `ns1.example.com.`
468 #[schemars(regex(
469 pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.$"
470 ))]
471 pub primary_ns: String,
472
473 /// Email address of the zone administrator (@ replaced with ., must end with .).
474 ///
475 /// Example: `admin.example.com.` for admin@example.com
476 #[schemars(regex(
477 pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.$"
478 ))]
479 pub admin_email: String,
480
481 /// Serial number for this zone. Typically in YYYYMMDDNN format.
482 /// Secondaries use this to determine if they need to update.
483 ///
484 /// Must be a 32-bit unsigned integer (0 to 4294967295).
485 /// The field is i64 to accommodate the full u32 range.
486 #[schemars(range(min = 0, max = 4_294_967_295_i64))]
487 pub serial: i64,
488
489 /// Refresh interval in seconds. How often secondaries should check for updates.
490 ///
491 /// Typical values: 3600-86400 (1 hour to 1 day).
492 #[schemars(range(min = 1, max = 2_147_483_647))]
493 pub refresh: i32,
494
495 /// Retry interval in seconds. How long to wait before retrying a failed refresh.
496 ///
497 /// Should be less than refresh. Typical values: 600-7200 (10 minutes to 2 hours).
498 #[schemars(range(min = 1, max = 2_147_483_647))]
499 pub retry: i32,
500
501 /// Expire time in seconds. After this time, secondaries stop serving the zone
502 /// if they can't contact the primary.
503 ///
504 /// Should be much larger than refresh+retry. Typical values: 604800-2419200 (1-4 weeks).
505 #[schemars(range(min = 1, max = 2_147_483_647))]
506 pub expire: i32,
507
508 /// Negative caching TTL in seconds. How long to cache NXDOMAIN responses.
509 ///
510 /// Typical values: 300-86400 (5 minutes to 1 day).
511 #[schemars(range(min = 0, max = 2_147_483_647))]
512 pub negative_ttl: i32,
513}
514
515/// Authoritative nameserver configuration for a DNS zone.
516///
517/// Defines an authoritative nameserver that will have an NS record automatically
518/// generated in the zone. Optionally includes IP addresses for glue record generation
519/// when the nameserver is within the zone's own domain.
520///
521/// # Examples
522///
523/// ## In-zone nameserver with glue records
524///
525/// ```yaml
526/// nameServers:
527/// - hostname: ns2.example.com.
528/// ipv4Address: "192.0.2.2"
529/// ipv6Address: "2001:db8::2"
530/// ```
531///
532/// ## Out-of-zone nameserver (no glue needed)
533///
534/// ```yaml
535/// nameServers:
536/// - hostname: ns1.external-provider.net.
537/// ```
538#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
539#[serde(rename_all = "camelCase")]
540pub struct NameServer {
541 /// Fully qualified domain name of the nameserver.
542 ///
543 /// Must end with a dot (.) for FQDN. This nameserver will have an NS record
544 /// automatically generated at the zone apex (@).
545 ///
546 /// Example: `ns2.example.com.`
547 #[schemars(regex(
548 pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.$"
549 ))]
550 pub hostname: String,
551
552 /// Optional IPv4 address for glue record generation.
553 ///
554 /// Required when the nameserver is within the zone's own domain (in-zone delegation).
555 /// When provided, an A record will be automatically generated for the nameserver.
556 ///
557 /// Example: For `ns2.example.com.` in zone `example.com`, provide `"192.0.2.2"`
558 ///
559 /// Glue records allow resolvers to find the IP addresses of nameservers that are
560 /// within the zone they serve, avoiding circular dependencies.
561 #[serde(skip_serializing_if = "Option::is_none")]
562 #[schemars(regex(pattern = r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$"))]
563 pub ipv4_address: Option<String>,
564
565 /// Optional IPv6 address for glue record generation (AAAA record).
566 ///
567 /// When provided along with (or instead of) `ipv4Address`, an AAAA record will be
568 /// automatically generated for the nameserver.
569 ///
570 /// Example: `"2001:db8::2"`
571 #[serde(skip_serializing_if = "Option::is_none")]
572 #[schemars(regex(
573 pattern = r"^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::$|^::1$|^([0-9a-fA-F]{1,4}:){1,7}:$|^([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}$|^([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}$|^([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}$|^([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}$|^[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})$|^:((:[0-9a-fA-F]{1,4}){1,7}|:)$"
574 ))]
575 pub ipv6_address: Option<String>,
576}
577
578/// Condition represents an observation of a resource's current state.
579///
580/// Conditions are used in status subresources to communicate the state of
581/// a resource to users and controllers.
582#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
583#[serde(rename_all = "camelCase")]
584pub struct Condition {
585 /// Type of condition. Common types include: Ready, Available, Progressing, Degraded, Failed.
586 pub r#type: String,
587
588 /// Status of the condition: True, False, or Unknown.
589 pub status: String,
590
591 /// Brief CamelCase reason for the condition's last transition.
592 #[serde(skip_serializing_if = "Option::is_none")]
593 pub reason: Option<String>,
594
595 /// Human-readable message indicating details about the transition.
596 #[serde(skip_serializing_if = "Option::is_none")]
597 pub message: Option<String>,
598
599 /// Last time the condition transitioned from one status to another (RFC3339 format).
600 #[serde(skip_serializing_if = "Option::is_none")]
601 pub last_transition_time: Option<String>,
602}
603
604/// Reference to a DNS record associated with a zone
605#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
606#[serde(rename_all = "camelCase")]
607pub struct RecordReference {
608 /// API version of the record (e.g., "bindy.firestoned.io/v1beta1")
609 pub api_version: String,
610 /// Kind of the record (e.g., `ARecord`, `CNAMERecord`, `MXRecord`)
611 pub kind: String,
612 /// Name of the record resource
613 pub name: String,
614 /// Namespace of the record resource
615 pub namespace: String,
616 /// DNS record name from spec.name (e.g., "www", "@", "_service._tcp")
617 /// Used for self-healing cleanup when verifying records in BIND9
618 #[serde(skip_serializing_if = "Option::is_none")]
619 pub record_name: Option<String>,
620 /// DNS zone name (e.g., "example.com")
621 /// Used for self-healing cleanup when querying BIND9
622 #[serde(skip_serializing_if = "Option::is_none")]
623 pub zone_name: Option<String>,
624}
625
626/// Reference to a DNS record with reconciliation timestamp tracking.
627///
628/// This struct tracks which records are assigned to a zone and whether
629/// they need reconciliation based on the `lastReconciledAt` timestamp.
630///
631/// **Event-Driven Pattern:**
632/// - Records with `lastReconciledAt == None` need reconciliation
633/// - Records with `lastReconciledAt == Some(timestamp)` are already configured
634///
635/// This pattern prevents redundant BIND9 API calls for already-configured records,
636/// following the same architecture as `Bind9Instance.status.selectedZones[]`.
637#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
638#[serde(rename_all = "camelCase")]
639pub struct RecordReferenceWithTimestamp {
640 /// API version of the record (e.g., "bindy.firestoned.io/v1beta1")
641 pub api_version: String,
642 /// Kind of the record (e.g., "`ARecord`", "`CNAMERecord`", "`MXRecord`")
643 pub kind: String,
644 /// Name of the record resource
645 pub name: String,
646 /// Namespace of the record resource
647 pub namespace: String,
648 /// DNS record name from spec.name (e.g., "www", "@", "_service._tcp")
649 #[serde(skip_serializing_if = "Option::is_none")]
650 pub record_name: Option<String>,
651 /// Timestamp when this record was last successfully reconciled to BIND9.
652 ///
653 /// - `None` = Record needs reconciliation (new or spec changed)
654 /// - `Some(timestamp)` = Record already configured, skip reconciliation
655 ///
656 /// This field is set by the record operator after successful BIND9 update.
657 /// The zone controller resets it to `None` when spec changes or zone is recreated.
658 #[serde(skip_serializing_if = "Option::is_none")]
659 pub last_reconciled_at: Option<k8s_openapi::apimachinery::pkg::apis::meta::v1::Time>,
660}
661
662/// Status of a `Bind9Instance` relationship with a `DNSZone`.
663///
664/// Tracks the lifecycle of zone assignment from initial selection through configuration.
665#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
666#[serde(rename_all = "PascalCase")]
667pub enum InstanceStatus {
668 /// Zone has selected this instance via bind9InstancesFrom, but zone not yet configured
669 Claimed,
670 /// Zone successfully configured on instance
671 Configured,
672 /// Zone configuration failed on instance
673 Failed,
674 /// Instance no longer selected by this zone (cleanup pending)
675 Unclaimed,
676}
677
678impl InstanceStatus {
679 #[must_use]
680 pub fn as_str(&self) -> &'static str {
681 match self {
682 InstanceStatus::Claimed => "Claimed",
683 InstanceStatus::Configured => "Configured",
684 InstanceStatus::Failed => "Failed",
685 InstanceStatus::Unclaimed => "Unclaimed",
686 }
687 }
688}
689
690/// Reference to a `Bind9Instance` with status and timestamp.
691///
692/// Extends `InstanceReference` with status tracking for zone claiming and configuration.
693#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
694#[serde(rename_all = "camelCase")]
695pub struct InstanceReferenceWithStatus {
696 /// API version of the `Bind9Instance` resource
697 pub api_version: String,
698 /// Kind of the resource (always "`Bind9Instance`")
699 pub kind: String,
700 /// Name of the `Bind9Instance` resource
701 pub name: String,
702 /// Namespace of the `Bind9Instance` resource
703 pub namespace: String,
704 /// Current status of this instance's relationship with the zone
705 pub status: InstanceStatus,
706 /// Timestamp when the instance status was last reconciled for this zone
707 #[serde(skip_serializing_if = "Option::is_none")]
708 pub last_reconciled_at: Option<String>,
709 /// Additional message (for Failed status, error details, etc.)
710 #[serde(skip_serializing_if = "Option::is_none")]
711 pub message: Option<String>,
712}
713
714/// `DNSZone` status
715#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
716#[serde(rename_all = "camelCase")]
717pub struct DNSZoneStatus {
718 #[serde(default)]
719 pub conditions: Vec<Condition>,
720 #[serde(skip_serializing_if = "Option::is_none")]
721 pub observed_generation: Option<i64>,
722 /// Count of records selected by recordsFrom label selectors.
723 ///
724 /// This field is automatically calculated from the length of `records`.
725 /// It provides a quick view of how many records are associated with this zone.
726 ///
727 /// Defaults to 0 when no records are selected.
728 #[serde(default)]
729 pub records_count: i32,
730 /// List of DNS records selected by recordsFrom label selectors.
731 ///
732 /// **Event-Driven Pattern:**
733 /// - Records with `lastReconciledAt == None` need reconciliation
734 /// - Records with `lastReconciledAt == Some(timestamp)` are already configured
735 ///
736 /// This field is populated by the `DNSZone` controller when evaluating `recordsFrom` selectors.
737 /// The timestamp is set by the record operator after successful BIND9 update.
738 ///
739 /// **Single Source of Truth:**
740 /// This status field is authoritative for which records belong to this zone and whether
741 /// they need reconciliation, preventing redundant BIND9 API calls.
742 #[serde(default)]
743 pub records: Vec<RecordReferenceWithTimestamp>,
744 /// List of `Bind9Instance` resources and their status for this zone.
745 ///
746 /// **Single Source of Truth for Instance-Zone Relationships:**
747 /// This field tracks all `Bind9Instances` selected by this zone via `bind9InstancesFrom` selectors,
748 /// along with the current status of zone configuration on each instance.
749 ///
750 /// **Status Lifecycle:**
751 /// - `Claimed`: Zone selected this instance (via `bind9InstancesFrom`), waiting for configuration
752 /// - `Configured`: Zone successfully configured on instance
753 /// - `Failed`: Zone configuration failed on instance
754 /// - `Unclaimed`: Instance no longer selected by this zone (cleanup pending)
755 ///
756 /// **Event-Driven Pattern:**
757 /// - `DNSZone` controller evaluates `bind9InstancesFrom` selectors to find matching instances
758 /// - `DNSZone` controller reads this field to track configuration status
759 /// - `DNSZone` controller updates status after configuration attempts
760 ///
761 /// **Automatic Selection:**
762 /// When a `DNSZone` reconciles, the controller automatically:
763 /// 1. Queries all `Bind9Instances` matching `bind9InstancesFrom` selectors
764 /// 2. Adds them to this list with status="Claimed"
765 /// 3. Configures zones on each instance
766 ///
767 /// # Example
768 ///
769 /// ```yaml
770 /// status:
771 /// bind9Instances:
772 /// - apiVersion: bindy.firestoned.io/v1beta1
773 /// kind: Bind9Instance
774 /// name: primary-dns-0
775 /// namespace: bindy-system
776 /// status: Configured
777 /// lastReconciledAt: "2026-01-03T20:00:00Z"
778 /// - apiVersion: bindy.firestoned.io/v1beta1
779 /// kind: Bind9Instance
780 /// name: secondary-dns-0
781 /// namespace: bindy-system
782 /// status: Claimed
783 /// lastReconciledAt: "2026-01-03T20:01:00Z"
784 /// ```
785 #[serde(default, skip_serializing_if = "Vec::is_empty")]
786 pub bind9_instances: Vec<InstanceReferenceWithStatus>,
787 /// Number of `Bind9Instance` resources in the `bind9_instances` list.
788 ///
789 /// This field is automatically updated whenever the `bind9_instances` list changes.
790 /// It provides a quick view of how many instances are serving this zone without
791 /// requiring clients to count array elements.
792 #[serde(skip_serializing_if = "Option::is_none")]
793 pub bind9_instances_count: Option<i32>,
794
795 /// DNSSEC signing status for this zone
796 ///
797 /// Populated when DNSSEC signing is enabled. Contains DS records,
798 /// key tags, and rotation information.
799 ///
800 /// **Important**: DS records must be published in the parent zone
801 /// to complete the DNSSEC chain of trust.
802 ///
803 /// # Example
804 ///
805 /// ```yaml
806 /// dnssec:
807 /// signed: true
808 /// dsRecords:
809 /// - "example.com. IN DS 12345 13 2 ABC123..."
810 /// keyTag: 12345
811 /// algorithm: "ECDSAP256SHA256"
812 /// nextKeyRollover: "2026-04-02T00:00:00Z"
813 /// ```
814 #[serde(default, skip_serializing_if = "Option::is_none")]
815 pub dnssec: Option<DNSSECStatus>,
816}
817
818/// Secondary Zone configuration
819#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
820#[serde(rename_all = "camelCase")]
821pub struct SecondaryZoneConfig {
822 /// Primary server addresses for zone transfer
823 pub primary_servers: Vec<String>,
824 /// Optional TSIG key for authenticated transfers
825 #[serde(skip_serializing_if = "Option::is_none")]
826 pub tsig_key: Option<String>,
827}
828
829/// `DNSZone` defines a DNS zone to be managed by BIND9.
830///
831/// A `DNSZone` represents an authoritative DNS zone (e.g., example.com) that will be
832/// served by a BIND9 cluster. The zone includes SOA record information and will be
833/// synchronized to all instances in the referenced cluster via AXFR/IXFR.
834///
835/// `DNSZones` can reference either:
836/// - A namespace-scoped `Bind9Cluster` (using `clusterRef`)
837/// - A cluster-scoped `ClusterBind9Provider` (using `clusterProviderRef`)
838///
839/// Exactly one of `clusterRef` or `clusterProviderRef` must be specified.
840///
841/// # Example: Namespace-scoped Cluster
842///
843/// ```yaml
844/// apiVersion: bindy.firestoned.io/v1beta1
845/// kind: DNSZone
846/// metadata:
847/// name: example-com
848/// namespace: dev-team-alpha
849/// spec:
850/// zoneName: example.com
851/// clusterRef: dev-team-dns # References Bind9Cluster in same namespace
852/// soaRecord:
853/// primaryNs: ns1.example.com.
854/// adminEmail: admin.example.com.
855/// serial: 2024010101
856/// refresh: 3600
857/// retry: 600
858/// expire: 604800
859/// negativeTtl: 86400
860/// ttl: 3600
861/// ```
862///
863/// # Example: Cluster-scoped Global Cluster
864///
865/// ```yaml
866/// apiVersion: bindy.firestoned.io/v1beta1
867/// kind: DNSZone
868/// metadata:
869/// name: production-example-com
870/// namespace: production
871/// spec:
872/// zoneName: example.com
873/// clusterProviderRef: shared-production-dns # References ClusterBind9Provider (cluster-scoped)
874/// soaRecord:
875/// primaryNs: ns1.example.com.
876/// adminEmail: admin.example.com.
877/// serial: 2024010101
878/// refresh: 3600
879/// retry: 600
880/// expire: 604800
881/// negativeTtl: 86400
882/// ttl: 3600
883/// ```
884#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
885#[kube(
886 group = "bindy.firestoned.io",
887 version = "v1beta1",
888 kind = "DNSZone",
889 namespaced,
890 shortname = "zone",
891 shortname = "zones",
892 shortname = "dz",
893 shortname = "dzs",
894 doc = "DNSZone represents an authoritative DNS zone managed by BIND9. Each DNSZone defines a zone (e.g., example.com) with SOA record parameters. Can reference either a namespace-scoped Bind9Cluster or cluster-scoped ClusterBind9Provider.",
895 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".spec.zoneName"}"#,
896 printcolumn = r#"{"name":"Provider","type":"string","jsonPath":".spec.clusterProviderRef"}"#,
897 printcolumn = r#"{"name":"Records","type":"integer","jsonPath":".status.recordsCount"}"#,
898 printcolumn = r#"{"name":"Instances","type":"integer","jsonPath":".status.bind9InstancesCount"}"#,
899 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
900 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
901)]
902#[kube(status = "DNSZoneStatus")]
903#[serde(rename_all = "camelCase")]
904pub struct DNSZoneSpec {
905 /// DNS zone name (e.g., "example.com").
906 ///
907 /// Must be a valid DNS zone name. Can be a domain or subdomain.
908 /// Examples: "example.com", "internal.example.com", "10.in-addr.arpa"
909 #[schemars(regex(
910 pattern = r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"
911 ))]
912 pub zone_name: String,
913
914 /// SOA (Start of Authority) record - defines zone authority and refresh parameters.
915 ///
916 /// The SOA record is required for all authoritative zones and contains
917 /// timing information for zone transfers and caching.
918 pub soa_record: SOARecord,
919
920 /// Default TTL (Time To Live) for records in this zone, in seconds.
921 ///
922 /// If not specified, individual records must specify their own TTL.
923 /// Typical values: 300-86400 (5 minutes to 1 day).
924 #[serde(default)]
925 #[schemars(range(min = 0, max = 2_147_483_647))]
926 pub ttl: Option<i32>,
927
928 /// Reference to a `Bind9Cluster` or `ClusterBind9Provider` to serve this zone.
929 ///
930 /// When specified, this zone will be automatically configured on all `Bind9Instance`
931 /// resources that belong to the referenced cluster. This provides a simple way to
932 /// assign zones to entire clusters.
933 ///
934 /// **Relationship with `bind9_instances_from`:**
935 /// - If only `cluster_ref` is specified: Zone targets all instances in that cluster
936 /// - If only `bind9_instances_from` is specified: Zone targets instances matching label selectors
937 /// - If both are specified: Zone targets union of cluster instances AND label-selected instances
938 ///
939 /// # Example
940 ///
941 /// ```yaml
942 /// spec:
943 /// clusterRef: production-dns # Target all instances in this cluster
944 /// zoneName: example.com
945 /// ```
946 #[serde(default, skip_serializing_if = "Option::is_none")]
947 pub cluster_ref: Option<String>,
948
949 /// Authoritative nameservers for this zone (v0.4.0+).
950 ///
951 /// NS records are automatically generated at the zone apex (@) for all entries.
952 /// The primary nameserver from `soaRecord.primaryNs` is always included automatically.
953 ///
954 /// Each entry can optionally include IP addresses to generate glue records (A/AAAA)
955 /// for in-zone nameservers. Glue records are required when the nameserver is within
956 /// the zone's own domain to avoid circular dependencies.
957 ///
958 /// # Examples
959 ///
960 /// ```yaml
961 /// # In-zone nameservers with glue records
962 /// nameServers:
963 /// - hostname: ns2.example.com.
964 /// ipv4Address: "192.0.2.2"
965 /// - hostname: ns3.example.com.
966 /// ipv4Address: "192.0.2.3"
967 /// ipv6Address: "2001:db8::3"
968 ///
969 /// # Out-of-zone nameserver (no glue needed)
970 /// - hostname: ns4.external-provider.net.
971 /// ```
972 ///
973 /// **Generated Records:**
974 /// - `@ IN NS ns2.example.com.` (NS record)
975 /// - `ns2.example.com. IN A 192.0.2.2` (glue record for in-zone NS)
976 /// - `@ IN NS ns3.example.com.` (NS record)
977 /// - `ns3.example.com. IN A 192.0.2.3` (IPv4 glue)
978 /// - `ns3.example.com. IN AAAA 2001:db8::3` (IPv6 glue)
979 /// - `@ IN NS ns4.external-provider.net.` (NS record only, no glue)
980 ///
981 /// **Benefits over `nameServerIps` (deprecated):**
982 /// - Clearer purpose: authoritative nameservers, not just glue records
983 /// - IPv6 support via `ipv6Address` field
984 /// - Automatic NS record generation (no manual `NSRecord` CRs needed)
985 ///
986 /// **Migration:** See [docs/src/operations/migration-guide.md](../operations/migration-guide.md)
987 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub name_servers: Option<Vec<NameServer>>,
989
990 /// (DEPRECATED in v0.4.0) Map of nameserver hostnames to IP addresses for glue records.
991 ///
992 /// **Use `nameServers` instead.** This field will be removed in v1.0.0.
993 ///
994 /// Glue records provide IP addresses for nameservers within the zone's own domain.
995 /// This is necessary when delegating subdomains where the nameserver is within the
996 /// delegated zone itself.
997 ///
998 /// Example: When delegating `sub.example.com` with nameserver `ns1.sub.example.com`,
999 /// you must provide the IP address of `ns1.sub.example.com` as a glue record.
1000 ///
1001 /// Format: `{"ns1.example.com.": "192.0.2.1", "ns2.example.com.": "192.0.2.2"}`
1002 ///
1003 /// Note: Nameserver hostnames should end with a dot (.) for FQDN.
1004 ///
1005 /// **Migration to `nameServers`:**
1006 /// ```yaml
1007 /// # Old (deprecated):
1008 /// nameServerIps:
1009 /// ns2.example.com.: "192.0.2.2"
1010 ///
1011 /// # New (recommended):
1012 /// nameServers:
1013 /// - hostname: ns2.example.com.
1014 /// ipv4Address: "192.0.2.2"
1015 /// ```
1016 #[deprecated(
1017 since = "0.4.0",
1018 note = "Use `name_servers` instead. This field will be removed in v1.0.0. See migration guide at docs/src/operations/migration-guide.md"
1019 )]
1020 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub name_server_ips: Option<HashMap<String, String>>,
1022
1023 /// Sources for DNS records to include in this zone.
1024 ///
1025 /// This field defines label selectors that automatically associate DNS records with this zone.
1026 /// Records with matching labels will be included in the zone's DNS configuration.
1027 ///
1028 /// This follows the standard Kubernetes selector pattern used by Services, `NetworkPolicies`,
1029 /// and other resources for declarative resource association.
1030 ///
1031 /// # Example: Match podinfo records in dev/staging environments
1032 ///
1033 /// ```yaml
1034 /// recordsFrom:
1035 /// - selector:
1036 /// matchLabels:
1037 /// app: podinfo
1038 /// matchExpressions:
1039 /// - key: environment
1040 /// operator: In
1041 /// values:
1042 /// - dev
1043 /// - staging
1044 /// ```
1045 ///
1046 /// # Selector Operators
1047 ///
1048 /// - **In**: Label value must be in the specified values list
1049 /// - **`NotIn`**: Label value must NOT be in the specified values list
1050 /// - **Exists**: Label key must exist (any value)
1051 /// - **`DoesNotExist`**: Label key must NOT exist
1052 ///
1053 /// # Use Cases
1054 ///
1055 /// - **Multi-environment zones**: Dynamically include records based on environment labels
1056 /// - **Application-specific zones**: Group all records for an application using `app` label
1057 /// - **Team-based zones**: Use team labels to automatically route records to team-owned zones
1058 /// - **Temporary records**: Use labels to include/exclude records without changing `zoneRef`
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1060 pub records_from: Option<Vec<RecordSource>>,
1061
1062 /// Select `Bind9Instance` resources to target for zone configuration using label selectors.
1063 ///
1064 /// This field enables dynamic, label-based selection of DNS instances to serve this zone.
1065 /// Instances matching these selectors will automatically receive zone configuration from
1066 /// the `DNSZone` controller.
1067 ///
1068 /// This follows the standard Kubernetes selector pattern used by Services, `NetworkPolicies`,
1069 /// and other resources for declarative resource association.
1070 ///
1071 /// **IMPORTANT**: This is the **preferred** method for zone-instance association. It provides:
1072 /// - **Decoupled Architecture**: Zones select instances, not vice versa
1073 /// - **Zone Ownership**: Zone authors control which instances serve their zones
1074 /// - **Dynamic Scaling**: New instances matching labels automatically pick up zones
1075 /// - **Multi-Tenancy**: Zones can target specific instance groups (prod, staging, team-specific)
1076 ///
1077 /// # Example: Target production primary instances
1078 ///
1079 /// ```yaml
1080 /// apiVersion: bindy.firestoned.io/v1beta1
1081 /// kind: DNSZone
1082 /// metadata:
1083 /// name: example-com
1084 /// namespace: bindy-system
1085 /// spec:
1086 /// zoneName: example.com
1087 /// bind9InstancesFrom:
1088 /// - selector:
1089 /// matchLabels:
1090 /// environment: production
1091 /// bindy.firestoned.io/role: primary
1092 /// ```
1093 ///
1094 /// # Example: Target instances by region and tier
1095 ///
1096 /// ```yaml
1097 /// bind9InstancesFrom:
1098 /// - selector:
1099 /// matchLabels:
1100 /// tier: frontend
1101 /// atchExpressions:
1102 /// - key: region
1103 /// operator: In
1104 /// values:
1105 /// - us-east-1
1106 /// - us-west-2
1107 /// ```
1108 ///
1109 /// # Selector Operators
1110 ///
1111 /// - **In**: Label value must be in the specified values list
1112 /// - **`NotIn`**: Label value must NOT be in the specified values list
1113 /// - **Exists**: Label key must exist (any value)
1114 /// - **`DoesNotExist`**: Label key must NOT exist
1115 ///
1116 /// # Use Cases
1117 ///
1118 /// - **Environment Isolation**: Target only production instances (`environment: production`)
1119 /// - **Role-Based Selection**: Select only primary or secondary instances
1120 /// - **Geographic Distribution**: Target instances in specific regions
1121 /// - **Team Boundaries**: Select instances managed by specific teams
1122 /// - **Testing Zones**: Target staging instances for non-production zones
1123 ///
1124 /// # Relationship with `clusterRef`
1125 ///
1126 /// - **`clusterRef`**: Explicitly assigns zone to ALL instances in a cluster
1127 /// - **`bind9InstancesFrom`**: Dynamically selects specific instances using labels (more flexible)
1128 ///
1129 /// You can use both approaches together - the zone will target the **union** of:
1130 /// - All instances in `clusterRef` cluster
1131 /// - Plus any additional instances matching `bind9InstancesFrom` selectors
1132 ///
1133 /// # Event-Driven Architecture
1134 ///
1135 /// The `DNSZone` controller watches both `DNSZone` and `Bind9Instance` resources.
1136 /// When labels change on either:
1137 /// 1. Controller re-evaluates label selector matching
1138 /// 2. Automatically configures zones on newly-matched instances
1139 /// 3. Removes zone configuration from instances that no longer match
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1141 pub bind9_instances_from: Option<Vec<InstanceSource>>,
1142
1143 /// Override DNSSEC policy for this zone
1144 ///
1145 /// Allows per-zone override of the cluster's global DNSSEC signing policy.
1146 /// If not specified, the zone inherits the DNSSEC configuration from the
1147 /// cluster's `global.dnssec.signing.policy`.
1148 ///
1149 /// Use this to:
1150 /// - Disable signing for specific zones in a signing-enabled cluster
1151 /// - Use stricter security policies for sensitive zones
1152 /// - Test different signing algorithms on specific zones
1153 ///
1154 /// # Example: Custom High-Security Policy
1155 ///
1156 /// ```yaml
1157 /// apiVersion: bindy.firestoned.io/v1beta1
1158 /// kind: DNSZone
1159 /// metadata:
1160 /// name: secure-zone
1161 /// spec:
1162 /// zoneName: secure.example.com
1163 /// clusterRef: production-dns
1164 /// dnssecPolicy: "high-security" # Override cluster default
1165 /// ```
1166 ///
1167 /// # Example: Disable Signing for One Zone
1168 ///
1169 /// ```yaml
1170 /// dnssecPolicy: "none" # Disable signing (cluster has signing enabled)
1171 /// ```
1172 ///
1173 /// **Note**: Custom policies require BIND9 `dnssec-policy` configuration.
1174 /// Built-in policies: `"default"`, `"none"`
1175 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub dnssec_policy: Option<String>,
1177}
1178
1179/// `ARecord` maps a DNS name to an IPv4 address.
1180///
1181/// A records are the most common DNS record type, mapping hostnames to IPv4 addresses.
1182/// Multiple A records can exist for the same name (round-robin DNS).
1183///
1184/// # Example
1185///
1186/// ```yaml
1187/// apiVersion: bindy.firestoned.io/v1beta1
1188/// kind: ARecord
1189/// metadata:
1190/// name: www-example-com
1191/// namespace: bindy-system
1192/// labels:
1193/// zone: example.com
1194/// spec:
1195/// name: www
1196/// ipv4Address: 192.0.2.1
1197/// ttl: 300
1198/// ```
1199///
1200/// Records are associated with `DNSZones` via label selectors.
1201/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1202#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1203#[kube(
1204 group = "bindy.firestoned.io",
1205 version = "v1beta1",
1206 kind = "ARecord",
1207 namespaced,
1208 shortname = "a",
1209 doc = "ARecord maps a DNS hostname to an IPv4 address. Multiple A records for the same name enable round-robin DNS load balancing.",
1210 printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1211 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".status.zoneRef.zoneName"}"#,
1212 printcolumn = r#"{"name":"Addresses","type":"string","jsonPath":".status.addresses"}"#,
1213 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1214 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1215)]
1216#[kube(status = "RecordStatus")]
1217#[serde(rename_all = "camelCase")]
1218pub struct ARecordSpec {
1219 /// Record name within the zone. Use "@" for the zone apex.
1220 ///
1221 /// Examples: "www", "mail", "ftp", "@"
1222 /// The full DNS name will be: {name}.{zone}
1223 pub name: String,
1224
1225 /// List of IPv4 addresses for this DNS record.
1226 ///
1227 /// Multiple addresses create round-robin DNS (load balancing).
1228 /// All addresses in the list belong to the same DNS name.
1229 ///
1230 /// Must contain at least one valid IPv4 address in dotted-decimal notation.
1231 ///
1232 /// Examples: `["192.0.2.1"]`, `["192.0.2.1", "192.0.2.2", "192.0.2.3"]`
1233 #[schemars(length(min = 1))]
1234 pub ipv4_addresses: Vec<String>,
1235
1236 /// Time To Live in seconds. Overrides zone default TTL if specified.
1237 ///
1238 /// Typical values: 60-86400 (1 minute to 1 day).
1239 #[serde(default)]
1240 #[schemars(range(min = 0, max = 2_147_483_647))]
1241 pub ttl: Option<i32>,
1242}
1243
1244/// `AAAARecord` maps a DNS name to an IPv6 address.
1245///
1246/// AAAA records are the IPv6 equivalent of A records, mapping hostnames to IPv6 addresses.
1247///
1248/// # Example
1249///
1250/// ```yaml
1251/// apiVersion: bindy.firestoned.io/v1beta1
1252/// kind: AAAARecord
1253/// metadata:
1254/// name: www-example-com-ipv6
1255/// namespace: bindy-system
1256/// labels:
1257/// zone: example.com
1258/// spec:
1259/// name: www
1260/// ipv6Address: "2001:db8::1"
1261/// ttl: 300
1262/// ```
1263///
1264/// Records are associated with `DNSZones` via label selectors.
1265/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1266#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1267#[kube(
1268 group = "bindy.firestoned.io",
1269 version = "v1beta1",
1270 kind = "AAAARecord",
1271 namespaced,
1272 shortname = "aaaa",
1273 doc = "AAAARecord maps a DNS hostname to an IPv6 address. This is the IPv6 equivalent of an A record.",
1274 printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1275 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".status.zoneRef.zoneName"}"#,
1276 printcolumn = r#"{"name":"Addresses","type":"string","jsonPath":".status.addresses"}"#,
1277 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1278 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1279)]
1280#[kube(status = "RecordStatus")]
1281#[serde(rename_all = "camelCase")]
1282pub struct AAAARecordSpec {
1283 /// Record name within the zone.
1284 pub name: String,
1285
1286 /// List of IPv6 addresses for this DNS record.
1287 ///
1288 /// Multiple addresses create round-robin DNS (load balancing).
1289 /// All addresses in the list belong to the same DNS name.
1290 ///
1291 /// Must contain at least one valid IPv6 address in standard notation.
1292 ///
1293 /// Examples: `["2001:db8::1"]`, `["2001:db8::1", "2001:db8::2"]`
1294 #[schemars(length(min = 1))]
1295 pub ipv6_addresses: Vec<String>,
1296
1297 /// Time To Live in seconds.
1298 #[serde(default)]
1299 #[schemars(range(min = 0, max = 2_147_483_647))]
1300 pub ttl: Option<i32>,
1301}
1302
1303/// `TXTRecord` holds arbitrary text data.
1304///
1305/// TXT records are commonly used for SPF, DKIM, DMARC, domain verification,
1306/// and other text-based metadata.
1307///
1308/// # Example
1309///
1310/// ```yaml
1311/// apiVersion: bindy.firestoned.io/v1beta1
1312/// kind: TXTRecord
1313/// metadata:
1314/// name: spf-example-com
1315/// namespace: bindy-system
1316/// labels:
1317/// zone: example.com
1318/// spec:
1319/// name: "@"
1320/// text:
1321/// - "v=spf1 include:_spf.google.com ~all"
1322/// ttl: 3600
1323/// ```
1324///
1325/// Records are associated with `DNSZones` via label selectors.
1326/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1327#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1328#[kube(
1329 group = "bindy.firestoned.io",
1330 version = "v1beta1",
1331 kind = "TXTRecord",
1332 namespaced,
1333 shortname = "txt",
1334 doc = "TXTRecord stores arbitrary text data in DNS. Commonly used for SPF, DKIM, DMARC policies, and domain verification.",
1335 printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1336 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".status.zoneRef.zoneName"}"#,
1337 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1338 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1339)]
1340#[kube(status = "RecordStatus")]
1341#[serde(rename_all = "camelCase")]
1342pub struct TXTRecordSpec {
1343 /// Record name within the zone.
1344 pub name: String,
1345
1346 /// Array of text strings. Each string can be up to 255 characters.
1347 ///
1348 /// Multiple strings are concatenated by DNS resolvers.
1349 /// For long text, split into multiple strings.
1350 pub text: Vec<String>,
1351
1352 /// Time To Live in seconds.
1353 #[serde(default)]
1354 #[schemars(range(min = 0, max = 2_147_483_647))]
1355 pub ttl: Option<i32>,
1356}
1357
1358/// `CNAMERecord` creates an alias from one name to another.
1359///
1360/// CNAME (Canonical Name) records create an alias from one DNS name to another.
1361/// The target can be in the same zone or a different zone.
1362///
1363/// **Important**: A CNAME cannot coexist with other record types for the same name.
1364///
1365/// # Example
1366///
1367/// ```yaml
1368/// apiVersion: bindy.firestoned.io/v1beta1
1369/// kind: CNAMERecord
1370/// metadata:
1371/// name: blog-example-com
1372/// namespace: bindy-system
1373/// labels:
1374/// zone: example.com
1375/// spec:
1376/// name: blog
1377/// target: example.github.io.
1378/// ttl: 3600
1379/// ```
1380///
1381/// Records are associated with `DNSZones` via label selectors.
1382/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1383#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1384#[kube(
1385 group = "bindy.firestoned.io",
1386 version = "v1beta1",
1387 kind = "CNAMERecord",
1388 namespaced,
1389 shortname = "cname",
1390 doc = "CNAMERecord creates a DNS alias from one hostname to another. A CNAME cannot coexist with other record types for the same name.",
1391 printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1392 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".status.zoneRef.zoneName"}"#,
1393 printcolumn = r#"{"name":"Target","type":"string","jsonPath":".spec.target"}"#,
1394 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1395 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1396)]
1397#[kube(status = "RecordStatus")]
1398#[serde(rename_all = "camelCase")]
1399pub struct CNAMERecordSpec {
1400 /// Record name within the zone.
1401 ///
1402 /// Note: CNAME records cannot be created at the zone apex (@).
1403 pub name: String,
1404
1405 /// Target hostname (canonical name).
1406 ///
1407 /// Should be a fully qualified domain name ending with a dot.
1408 /// Example: "example.com." or "www.example.com."
1409 pub target: String,
1410
1411 /// Time To Live in seconds.
1412 #[serde(default)]
1413 #[schemars(range(min = 0, max = 2_147_483_647))]
1414 pub ttl: Option<i32>,
1415}
1416
1417/// `MXRecord` specifies mail servers for a domain.
1418///
1419/// MX (Mail Exchange) records specify the mail servers responsible for accepting email
1420/// for a domain. Lower priority values indicate higher preference.
1421///
1422/// # Example
1423///
1424/// ```yaml
1425/// apiVersion: bindy.firestoned.io/v1beta1
1426/// kind: MXRecord
1427/// metadata:
1428/// name: mail-example-com
1429/// namespace: bindy-system
1430/// labels:
1431/// zone: example.com
1432/// spec:
1433/// name: "@"
1434/// priority: 10
1435/// mailServer: mail.example.com.
1436/// ttl: 3600
1437/// ```
1438///
1439/// Records are associated with `DNSZones` via label selectors.
1440/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1441#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1442#[kube(
1443 group = "bindy.firestoned.io",
1444 version = "v1beta1",
1445 kind = "MXRecord",
1446 namespaced,
1447 shortname = "mx",
1448 doc = "MXRecord specifies mail exchange servers for a domain. Lower priority values indicate higher preference for mail delivery.",
1449 printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1450 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".status.zoneRef.zoneName"}"#,
1451 printcolumn = r#"{"name":"Priority","type":"integer","jsonPath":".spec.priority"}"#,
1452 printcolumn = r#"{"name":"Mail Server","type":"string","jsonPath":".spec.mailServer"}"#,
1453 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1454 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1455)]
1456#[kube(status = "RecordStatus")]
1457#[serde(rename_all = "camelCase")]
1458pub struct MXRecordSpec {
1459 /// Record name within the zone. Use "@" for the zone apex.
1460 pub name: String,
1461
1462 /// Priority (preference) of this mail server. Lower values = higher priority.
1463 ///
1464 /// Common values: 0-100. Multiple MX records can exist with different priorities.
1465 #[schemars(range(min = 0, max = 65535))]
1466 pub priority: i32,
1467
1468 /// Fully qualified domain name of the mail server.
1469 ///
1470 /// Must end with a dot. Example: "mail.example.com."
1471 pub mail_server: String,
1472
1473 /// Time To Live in seconds.
1474 #[serde(default)]
1475 #[schemars(range(min = 0, max = 2_147_483_647))]
1476 pub ttl: Option<i32>,
1477}
1478
1479/// `NSRecord` delegates a subdomain to other nameservers.
1480///
1481/// NS (Nameserver) records specify which DNS servers are authoritative for a subdomain.
1482/// They are used for delegating subdomains to different nameservers.
1483///
1484/// # Example
1485///
1486/// ```yaml
1487/// apiVersion: bindy.firestoned.io/v1beta1
1488/// kind: NSRecord
1489/// metadata:
1490/// name: subdomain-ns
1491/// namespace: bindy-system
1492/// labels:
1493/// zone: example.com
1494/// spec:
1495/// name: subdomain
1496/// nameserver: ns1.other-provider.com.
1497/// ttl: 86400
1498/// ```
1499///
1500/// Records are associated with `DNSZones` via label selectors.
1501/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1502#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1503#[kube(
1504 group = "bindy.firestoned.io",
1505 version = "v1beta1",
1506 kind = "NSRecord",
1507 namespaced,
1508 shortname = "ns",
1509 doc = "NSRecord delegates a subdomain to authoritative nameservers. Used for subdomain delegation to different DNS providers or servers.",
1510 printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1511 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".status.zoneRef.zoneName"}"#,
1512 printcolumn = r#"{"name":"Nameserver","type":"string","jsonPath":".spec.nameserver"}"#,
1513 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1514 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1515)]
1516#[kube(status = "RecordStatus")]
1517#[serde(rename_all = "camelCase")]
1518pub struct NSRecordSpec {
1519 /// Subdomain to delegate. For zone apex, use "@".
1520 pub name: String,
1521
1522 /// Fully qualified domain name of the nameserver.
1523 ///
1524 /// Must end with a dot. Example: "ns1.example.com."
1525 pub nameserver: String,
1526
1527 /// Time To Live in seconds.
1528 #[serde(default)]
1529 #[schemars(range(min = 0, max = 2_147_483_647))]
1530 pub ttl: Option<i32>,
1531}
1532
1533/// `SRVRecord` specifies the location of services.
1534///
1535/// SRV (Service) records specify the hostname and port of servers for specific services.
1536/// The name format is: _service._proto (e.g., _ldap._tcp, _sip._udp).
1537///
1538/// # Example
1539///
1540/// ```yaml
1541/// apiVersion: bindy.firestoned.io/v1beta1
1542/// kind: SRVRecord
1543/// metadata:
1544/// name: ldap-srv
1545/// namespace: bindy-system
1546/// labels:
1547/// zone: example.com
1548/// spec:
1549/// name: _ldap._tcp
1550/// priority: 10
1551/// weight: 60
1552/// port: 389
1553/// target: ldap.example.com.
1554/// ttl: 3600
1555/// ```
1556///
1557/// Records are associated with `DNSZones` via label selectors.
1558/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1559#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1560#[kube(
1561 group = "bindy.firestoned.io",
1562 version = "v1beta1",
1563 kind = "SRVRecord",
1564 namespaced,
1565 shortname = "srv",
1566 doc = "SRVRecord specifies the hostname and port of servers for specific services. The record name follows the format _service._proto (e.g., _ldap._tcp).",
1567 printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1568 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".status.zoneRef.zoneName"}"#,
1569 printcolumn = r#"{"name":"Target","type":"string","jsonPath":".spec.target"}"#,
1570 printcolumn = r#"{"name":"Port","type":"integer","jsonPath":".spec.port"}"#,
1571 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1572 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1573)]
1574#[kube(status = "RecordStatus")]
1575#[serde(rename_all = "camelCase")]
1576pub struct SRVRecordSpec {
1577 /// Service and protocol in the format: _service._proto
1578 ///
1579 /// Example: "_ldap._tcp", "_sip._udp", "_http._tcp"
1580 pub name: String,
1581
1582 /// Priority of the target host. Lower values = higher priority.
1583 #[schemars(range(min = 0, max = 65535))]
1584 pub priority: i32,
1585
1586 /// Relative weight for records with the same priority.
1587 ///
1588 /// Higher values = higher probability of selection.
1589 #[schemars(range(min = 0, max = 65535))]
1590 pub weight: i32,
1591
1592 /// TCP or UDP port where the service is available.
1593 #[schemars(range(min = 0, max = 65535))]
1594 pub port: i32,
1595
1596 /// Fully qualified domain name of the target host.
1597 ///
1598 /// Must end with a dot. Use "." for "service not available".
1599 pub target: String,
1600
1601 /// Time To Live in seconds.
1602 #[serde(default)]
1603 #[schemars(range(min = 0, max = 2_147_483_647))]
1604 pub ttl: Option<i32>,
1605}
1606
1607/// `CAARecord` specifies Certificate Authority Authorization.
1608///
1609/// CAA (Certification Authority Authorization) records specify which certificate
1610/// authorities are allowed to issue certificates for a domain.
1611///
1612/// # Example
1613///
1614/// ```yaml
1615/// apiVersion: bindy.firestoned.io/v1beta1
1616/// kind: CAARecord
1617/// metadata:
1618/// name: caa-letsencrypt
1619/// namespace: bindy-system
1620/// labels:
1621/// zone: example.com
1622/// spec:
1623/// name: "@"
1624/// flags: 0
1625/// tag: issue
1626/// value: letsencrypt.org
1627/// ttl: 86400
1628/// ```
1629///
1630/// Records are associated with `DNSZones` via label selectors.
1631/// The `DNSZone` must have a `recordsFrom` selector that matches this record's labels.
1632#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
1633#[kube(
1634 group = "bindy.firestoned.io",
1635 version = "v1beta1",
1636 kind = "CAARecord",
1637 namespaced,
1638 shortname = "caa",
1639 doc = "CAARecord specifies which certificate authorities are authorized to issue certificates for a domain. Enhances domain security and certificate issuance control.",
1640 printcolumn = r#"{"name":"Name","type":"string","jsonPath":".spec.name"}"#,
1641 printcolumn = r#"{"name":"Zone","type":"string","jsonPath":".status.zoneRef.zoneName"}"#,
1642 printcolumn = r#"{"name":"Tag","type":"string","jsonPath":".spec.tag"}"#,
1643 printcolumn = r#"{"name":"Value","type":"string","jsonPath":".spec.value"}"#,
1644 printcolumn = r#"{"name":"TTL","type":"integer","jsonPath":".spec.ttl"}"#,
1645 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
1646)]
1647#[kube(status = "RecordStatus")]
1648#[serde(rename_all = "camelCase")]
1649pub struct CAARecordSpec {
1650 /// Record name within the zone. Use "@" for the zone apex.
1651 pub name: String,
1652
1653 /// Flags byte. Use 0 for non-critical, 128 for critical.
1654 ///
1655 /// Critical flag (128) means CAs must understand the tag.
1656 #[schemars(range(min = 0, max = 255))]
1657 pub flags: i32,
1658
1659 /// Property tag. Common values: "issue", "issuewild", "iodef".
1660 ///
1661 /// - "issue": Authorize CA to issue certificates
1662 /// - "issuewild": Authorize CA to issue wildcard certificates
1663 /// - "iodef": URL/email for violation reports
1664 pub tag: String,
1665
1666 /// Property value. Format depends on the tag.
1667 ///
1668 /// For "issue"/"issuewild": CA domain (e.g., "letsencrypt.org")
1669 /// For "iodef": mailto: or https: URL
1670 pub value: String,
1671
1672 /// Time To Live in seconds.
1673 #[serde(default)]
1674 #[schemars(range(min = 0, max = 2_147_483_647))]
1675 pub ttl: Option<i32>,
1676}
1677
1678/// Generic record status
1679#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
1680#[serde(rename_all = "camelCase")]
1681pub struct RecordStatus {
1682 #[serde(default)]
1683 pub conditions: Vec<Condition>,
1684 #[serde(skip_serializing_if = "Option::is_none")]
1685 pub observed_generation: Option<i64>,
1686 /// The FQDN of the zone that owns this record (set by `DNSZone` controller).
1687 ///
1688 /// When a `DNSZone`'s label selector matches this record, the `DNSZone` controller
1689 /// sets this field to the zone's FQDN (e.g., `"example.com"`). The record reconciler
1690 /// uses this to determine which zone to update in BIND9.
1691 ///
1692 /// If this field is empty, the record is not matched by any zone and should not
1693 /// be reconciled into BIND9.
1694 ///
1695 /// **DEPRECATED**: Use `zone_ref` instead for structured zone reference.
1696 #[deprecated(
1697 since = "0.2.0",
1698 note = "Use zone_ref instead for structured zone reference"
1699 )]
1700 #[serde(skip_serializing_if = "Option::is_none")]
1701 pub zone: Option<String>,
1702 /// Structured reference to the `DNSZone` that owns this record.
1703 ///
1704 /// Set by the `DNSZone` controller when the zone's `recordsFrom` selector matches
1705 /// this record's labels. Contains the complete Kubernetes object reference including
1706 /// apiVersion, kind, name, namespace, and zoneName.
1707 ///
1708 /// The record reconciler uses this to:
1709 /// 1. Look up the parent `DNSZone` resource
1710 /// 2. Find the zone's primary `Bind9Instance` servers
1711 /// 3. Add this record to BIND9 on primaries
1712 /// 4. Trigger zone transfer (retransfer) on secondaries
1713 ///
1714 /// If this field is None, the record is not selected by any zone and will not
1715 /// be added to BIND9.
1716 #[serde(skip_serializing_if = "Option::is_none")]
1717 pub zone_ref: Option<ZoneReference>,
1718 /// SHA-256 hash of the record's spec data.
1719 ///
1720 /// Used to detect when a record's data has actually changed, avoiding
1721 /// unnecessary BIND9 updates and zone transfers.
1722 ///
1723 /// The hash is calculated from all fields in the record's spec that affect
1724 /// the DNS record data (name, addresses, TTL, etc.).
1725 #[serde(skip_serializing_if = "Option::is_none")]
1726 pub record_hash: Option<String>,
1727 /// Timestamp of the last successful update to BIND9.
1728 ///
1729 /// This is updated after a successful nsupdate operation.
1730 /// Uses RFC 3339 format (e.g., "2025-12-26T10:30:00Z").
1731 #[serde(skip_serializing_if = "Option::is_none")]
1732 pub last_updated: Option<String>,
1733 /// Comma-separated list of addresses for display purposes.
1734 ///
1735 /// For `ARecord` and `AAAARecord` resources, this field contains the IP addresses
1736 /// from `spec.ipv4Addresses` or `spec.ipv6Addresses` joined with commas.
1737 /// This is used for prettier kubectl output instead of showing JSON arrays.
1738 ///
1739 /// Example: "192.0.2.1,192.0.2.2,192.0.2.3"
1740 ///
1741 /// For other record types, this field is not used.
1742 #[serde(skip_serializing_if = "Option::is_none")]
1743 pub addresses: Option<String>,
1744}
1745
1746/// RNDC/TSIG algorithm for authenticated communication and zone transfers.
1747///
1748/// These HMAC algorithms are supported by BIND9 for securing RNDC communication
1749/// and zone transfers (AXFR/IXFR).
1750#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1751#[serde(rename_all = "kebab-case")]
1752pub enum RndcAlgorithm {
1753 /// HMAC-MD5 (legacy, not recommended for new deployments)
1754 HmacMd5,
1755 /// HMAC-SHA1
1756 HmacSha1,
1757 /// HMAC-SHA224
1758 HmacSha224,
1759 /// HMAC-SHA256 (recommended)
1760 #[default]
1761 HmacSha256,
1762 /// HMAC-SHA384
1763 HmacSha384,
1764 /// HMAC-SHA512
1765 HmacSha512,
1766}
1767
1768impl RndcAlgorithm {
1769 /// Convert enum to string representation expected by BIND9
1770 #[must_use]
1771 pub fn as_str(&self) -> &'static str {
1772 match self {
1773 Self::HmacMd5 => "hmac-md5",
1774 Self::HmacSha1 => "hmac-sha1",
1775 Self::HmacSha224 => "hmac-sha224",
1776 Self::HmacSha256 => "hmac-sha256",
1777 Self::HmacSha384 => "hmac-sha384",
1778 Self::HmacSha512 => "hmac-sha512",
1779 }
1780 }
1781
1782 /// Convert enum to string format expected by the rndc Rust crate.
1783 ///
1784 /// The rndc crate expects algorithm strings without the "hmac-" prefix
1785 /// (e.g., "sha256" instead of "hmac-sha256").
1786 #[must_use]
1787 pub fn as_rndc_str(&self) -> &'static str {
1788 match self {
1789 Self::HmacMd5 => "md5",
1790 Self::HmacSha1 => "sha1",
1791 Self::HmacSha224 => "sha224",
1792 Self::HmacSha256 => "sha256",
1793 Self::HmacSha384 => "sha384",
1794 Self::HmacSha512 => "sha512",
1795 }
1796 }
1797}
1798
1799/// Reference to a Kubernetes Secret containing RNDC/TSIG credentials.
1800///
1801/// This allows you to use an existing external Secret for RNDC authentication instead
1802/// of having the operator auto-generate one. The Secret is mounted as a directory at
1803/// `/etc/bind/keys/` in the BIND9 container, and BIND9 uses the `rndc.key` file.
1804///
1805/// # External (User-Managed) Secrets
1806///
1807/// For external secrets, you ONLY need to provide the `rndc.key` field containing
1808/// the complete BIND9 key file content. The other fields (`key-name`, `algorithm`,
1809/// `secret`) are optional metadata used by operator-generated secrets.
1810///
1811/// ## Minimal External Secret Example
1812///
1813/// ```yaml
1814/// apiVersion: v1
1815/// kind: Secret
1816/// metadata:
1817/// name: my-rndc-key
1818/// namespace: bindy-system
1819/// type: Opaque
1820/// stringData:
1821/// rndc.key: |
1822/// key "bindy-operator" {
1823/// algorithm hmac-sha256;
1824/// secret "base64EncodedSecretKeyMaterial==";
1825/// };
1826/// ```
1827///
1828/// # Auto-Generated (Operator-Managed) Secrets
1829///
1830/// When the operator auto-generates a Secret (no `rndcSecretRef` specified), it
1831/// creates a Secret with all 4 fields for internal metadata tracking:
1832///
1833/// ```yaml
1834/// apiVersion: v1
1835/// kind: Secret
1836/// metadata:
1837/// name: bind9-instance-rndc
1838/// namespace: bindy-system
1839/// type: Opaque
1840/// stringData:
1841/// key-name: "bindy-operator" # Operator metadata
1842/// algorithm: "hmac-sha256" # Operator metadata
1843/// secret: "randomBase64Key==" # Operator metadata
1844/// rndc.key: | # Used by BIND9
1845/// key "bindy-operator" {
1846/// algorithm hmac-sha256;
1847/// secret "randomBase64Key==";
1848/// };
1849/// ```
1850///
1851/// # Using with `Bind9Instance`
1852///
1853/// ```yaml
1854/// apiVersion: bindy.firestoned.io/v1beta1
1855/// kind: Bind9Instance
1856/// metadata:
1857/// name: production-dns-primary-0
1858/// spec:
1859/// clusterRef: production-dns
1860/// role: primary
1861/// rndcSecretRef:
1862/// name: my-rndc-key
1863/// algorithm: hmac-sha256
1864/// ```
1865///
1866/// # How It Works
1867///
1868/// When the Secret is mounted at `/etc/bind/keys/`, Kubernetes creates individual
1869/// files for each Secret key:
1870/// - `/etc/bind/keys/rndc.key` (the BIND9 key file) ← **This is what BIND9 uses**
1871/// - `/etc/bind/keys/key-name` (optional metadata for operator-generated secrets)
1872/// - `/etc/bind/keys/algorithm` (optional metadata for operator-generated secrets)
1873/// - `/etc/bind/keys/secret` (optional metadata for operator-generated secrets)
1874///
1875/// The `rndc.conf` file includes `/etc/bind/keys/rndc.key`, so BIND9 only needs
1876/// that one file to exist
1877#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1878#[serde(rename_all = "camelCase")]
1879pub struct RndcSecretRef {
1880 /// Name of the Kubernetes Secret containing RNDC credentials
1881 pub name: String,
1882
1883 /// HMAC algorithm for this key
1884 #[serde(default)]
1885 pub algorithm: RndcAlgorithm,
1886
1887 /// Key within the secret for the key name (default: "key-name")
1888 #[serde(default = "default_key_name_key")]
1889 pub key_name_key: String,
1890
1891 /// Key within the secret for the secret value (default: "secret")
1892 #[serde(default = "default_secret_key")]
1893 pub secret_key: String,
1894}
1895
1896fn default_key_name_key() -> String {
1897 "key-name".to_string()
1898}
1899
1900fn default_secret_key() -> String {
1901 "secret".to_string()
1902}
1903
1904fn default_rotate_after() -> String {
1905 crate::constants::DEFAULT_ROTATION_INTERVAL.to_string()
1906}
1907
1908fn default_secret_type() -> String {
1909 "Opaque".to_string()
1910}
1911
1912/// RNDC key lifecycle configuration with automatic rotation support.
1913///
1914/// Provides three configuration modes:
1915/// 1. **Auto-generated with optional rotation** (default) - Operator creates and manages keys
1916/// 2. **Reference to existing Secret** - Use pre-existing Kubernetes Secret (no rotation)
1917/// 3. **Inline Secret specification** - Define Secret inline with optional rotation
1918///
1919/// When `auto_rotate` is enabled, the operator automatically rotates keys after the
1920/// `rotate_after` duration has elapsed. Rotation timestamps are tracked in Secret annotations.
1921///
1922/// # Examples
1923///
1924/// ```yaml
1925/// # Auto-generated with 30-day rotation
1926/// rndcKeys:
1927/// autoRotate: true
1928/// rotateAfter: 720h
1929/// algorithm: hmac-sha256
1930///
1931/// # Reference existing Secret (no rotation)
1932/// rndcKeys:
1933/// secretRef:
1934/// name: my-rndc-key
1935/// algorithm: hmac-sha256
1936///
1937/// # Inline Secret with rotation
1938/// rndcKeys:
1939/// autoRotate: true
1940/// rotateAfter: 2160h # 90 days
1941/// secret:
1942/// metadata:
1943/// name: custom-rndc-key
1944/// labels:
1945/// app: bindy
1946/// ```
1947#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
1948#[serde(rename_all = "camelCase")]
1949pub struct RndcKeyConfig {
1950 /// Enable automatic key rotation (default: false for backward compatibility).
1951 ///
1952 /// When `true`, the operator automatically rotates the RNDC key after the
1953 /// `rotate_after` interval. When `false`, keys are generated once and never rotated.
1954 ///
1955 /// **Important**: Rotation only applies to operator-managed Secrets. If you
1956 /// specify `secret_ref`, that Secret will NOT be rotated automatically.
1957 ///
1958 /// Default: `false`
1959 #[serde(default)]
1960 pub auto_rotate: bool,
1961
1962 /// Duration after which to rotate the key (Go duration format: "720h", "30d").
1963 ///
1964 /// Supported units:
1965 /// - `h` (hours): "720h" = 30 days
1966 /// - `d` (days): "30d" = 30 days
1967 /// - `w` (weeks): "4w" = 28 days
1968 ///
1969 /// Constraints:
1970 /// - Minimum: 1h (1 hour)
1971 /// - Maximum: 8760h (365 days / 1 year)
1972 ///
1973 /// Only applies when `auto_rotate` is `true`.
1974 ///
1975 /// Default: `"720h"` (30 days)
1976 #[serde(default = "default_rotate_after")]
1977 pub rotate_after: String,
1978
1979 /// Reference to an existing Kubernetes Secret containing RNDC credentials.
1980 ///
1981 /// When specified, the operator uses this existing Secret instead of auto-generating
1982 /// one. The Secret must contain the `rndc.key` field with BIND9 key file content.
1983 ///
1984 /// **Mutually exclusive with `secret`** - if both are specified, `secret_ref` takes
1985 /// precedence and `secret` is ignored.
1986 ///
1987 /// **Rotation note**: User-managed Secrets (via `secret_ref`) are NOT automatically
1988 /// rotated even if `auto_rotate` is `true`. You must rotate these manually.
1989 ///
1990 /// Default: `None` (auto-generate key)
1991 #[serde(skip_serializing_if = "Option::is_none")]
1992 pub secret_ref: Option<RndcSecretRef>,
1993
1994 /// Inline Secret specification for operator-managed Secret with optional rotation.
1995 ///
1996 /// Embeds a full Kubernetes Secret specification. The operator will create and
1997 /// manage this Secret, and rotate it if `auto_rotate` is `true`.
1998 ///
1999 /// **Mutually exclusive with `secret_ref`** - if both are specified, `secret_ref`
2000 /// takes precedence and this field is ignored.
2001 ///
2002 /// Default: `None` (auto-generate key)
2003 #[serde(skip_serializing_if = "Option::is_none")]
2004 pub secret: Option<SecretSpec>,
2005
2006 /// HMAC algorithm for the RNDC key.
2007 ///
2008 /// Only used when auto-generating keys (when neither `secret_ref` nor `secret` are
2009 /// specified). If using `secret_ref`, the algorithm is specified in that reference.
2010 ///
2011 /// Default: `hmac-sha256`
2012 #[serde(default)]
2013 pub algorithm: RndcAlgorithm,
2014}
2015
2016/// Kubernetes Secret specification for inline Secret creation.
2017///
2018/// Used when the operator should create and manage the Secret (with optional rotation).
2019/// This is a subset of the Kubernetes Secret API focusing on fields relevant for
2020/// RNDC key management.
2021///
2022/// # Example
2023///
2024/// ```yaml
2025/// secret:
2026/// metadata:
2027/// name: my-rndc-key
2028/// labels:
2029/// app: bindy
2030/// tier: infrastructure
2031/// stringData:
2032/// rndc.key: |
2033/// key "bindy-operator" {
2034/// algorithm hmac-sha256;
2035/// secret "dGVzdHNlY3JldA==";
2036/// };
2037/// ```
2038#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2039#[serde(rename_all = "camelCase")]
2040pub struct SecretSpec {
2041 /// Secret metadata (name, labels, annotations).
2042 ///
2043 /// **Required**: You must specify `metadata.name` for the Secret name.
2044 pub metadata: SecretMetadata,
2045
2046 /// Secret type (default: "Opaque").
2047 ///
2048 /// For RNDC keys, use the default "Opaque" type.
2049 #[serde(default = "default_secret_type")]
2050 #[serde(rename = "type")]
2051 pub type_: String,
2052
2053 /// String data (keys and values as strings).
2054 ///
2055 /// For RNDC keys, you should provide:
2056 /// - `rndc.key`: Full BIND9 key file content (required by BIND9)
2057 ///
2058 /// Optional metadata (auto-populated by operator if omitted):
2059 /// - `key-name`: Name of the TSIG key
2060 /// - `algorithm`: HMAC algorithm
2061 /// - `secret`: Base64-encoded key material
2062 ///
2063 /// Kubernetes automatically base64-encodes string data when creating the Secret.
2064 #[serde(skip_serializing_if = "Option::is_none")]
2065 pub string_data: Option<std::collections::BTreeMap<String, String>>,
2066
2067 /// Binary data (keys and values as base64 strings).
2068 ///
2069 /// Alternative to `string_data` if you want to provide already-base64-encoded values.
2070 /// Most users should use `string_data` instead for readability.
2071 #[serde(skip_serializing_if = "Option::is_none")]
2072 pub data: Option<std::collections::BTreeMap<String, String>>,
2073}
2074
2075/// Minimal Secret metadata for inline Secret specifications.
2076///
2077/// This is a subset of Kubernetes `ObjectMeta` focusing on commonly-used fields.
2078#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2079#[serde(rename_all = "camelCase")]
2080pub struct SecretMetadata {
2081 /// Secret name (required).
2082 ///
2083 /// Must be a valid Kubernetes resource name (lowercase alphanumeric, hyphens, dots).
2084 pub name: String,
2085
2086 /// Labels to apply to the Secret.
2087 ///
2088 /// Useful for organizing and selecting Secrets via label selectors.
2089 ///
2090 /// Example:
2091 /// ```yaml
2092 /// labels:
2093 /// app: bindy
2094 /// tier: infrastructure
2095 /// environment: production
2096 /// ```
2097 #[serde(skip_serializing_if = "Option::is_none")]
2098 pub labels: Option<std::collections::BTreeMap<String, String>>,
2099
2100 /// Annotations to apply to the Secret.
2101 ///
2102 /// **Note**: The operator will add rotation tracking annotations:
2103 /// - `bindy.firestoned.io/rndc-created-at` - Key creation timestamp
2104 /// - `bindy.firestoned.io/rndc-rotate-at` - Next rotation timestamp
2105 /// - `bindy.firestoned.io/rndc-rotation-count` - Number of rotations
2106 ///
2107 /// Do not manually set these rotation tracking annotations.
2108 #[serde(skip_serializing_if = "Option::is_none")]
2109 pub annotations: Option<std::collections::BTreeMap<String, String>>,
2110}
2111
2112/// Default BIND9 version for clusters when not specified
2113#[allow(clippy::unnecessary_wraps)]
2114fn default_bind9_version() -> Option<String> {
2115 Some(crate::constants::DEFAULT_BIND9_VERSION.to_string())
2116}
2117
2118/// TSIG Key configuration for authenticated zone transfers (deprecated in favor of `RndcSecretRef`)
2119#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2120#[serde(rename_all = "camelCase")]
2121pub struct TSIGKey {
2122 /// Name of the TSIG key
2123 pub name: String,
2124 /// Algorithm for HMAC-based authentication
2125 pub algorithm: RndcAlgorithm,
2126 /// Secret key (base64 encoded) - should reference a Secret
2127 pub secret: String,
2128}
2129
2130/// BIND9 server configuration options
2131///
2132/// These settings configure the BIND9 DNS server behavior including recursion,
2133/// access control lists, DNSSEC, and network listeners.
2134#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2135#[serde(rename_all = "camelCase")]
2136pub struct Bind9Config {
2137 /// Enable or disable recursive DNS queries
2138 ///
2139 /// When enabled (`true`), the DNS server will recursively resolve queries by
2140 /// contacting other authoritative nameservers. When disabled (`false`), the
2141 /// server only answers for zones it is authoritative for.
2142 ///
2143 /// Default: `false` (authoritative-only mode)
2144 ///
2145 /// **Important**: Recursive resolvers should not be publicly accessible due to
2146 /// security risks (DNS amplification attacks, cache poisoning).
2147 #[serde(default)]
2148 pub recursion: Option<bool>,
2149
2150 /// Access control list for DNS queries
2151 ///
2152 /// Specifies which IP addresses or networks are allowed to query this DNS server.
2153 /// Supports CIDR notation and special keywords.
2154 ///
2155 /// Default: Not set (BIND9 defaults to localhost only)
2156 ///
2157 /// Examples:
2158 /// - `["0.0.0.0/0"]` - Allow queries from any IPv4 address
2159 /// - `["10.0.0.0/8", "172.16.0.0/12"]` - Allow queries from private networks
2160 /// - `["any"]` - Allow queries from any IP (IPv4 and IPv6)
2161 /// - `["none"]` - Deny all queries
2162 /// - `["localhost"]` - Allow only from localhost
2163 #[serde(default)]
2164 pub allow_query: Option<Vec<String>>,
2165
2166 /// Access control list for zone transfers (AXFR/IXFR)
2167 ///
2168 /// Specifies which IP addresses or networks are allowed to perform zone transfers
2169 /// from this server. Zone transfers are used for replication between primary and
2170 /// secondary DNS servers.
2171 ///
2172 /// Default: Auto-detected cluster Pod CIDRs (e.g., `["10.42.0.0/16"]`)
2173 ///
2174 /// Examples:
2175 /// - `["10.42.0.0/16"]` - Allow transfers from specific Pod network
2176 /// - `["10.0.0.0/8"]` - Allow transfers from entire private network
2177 /// - `[]` - Deny all zone transfers (empty list means "none")
2178 /// - `["any"]` - Allow transfers from any IP (not recommended for production)
2179 ///
2180 /// Can be overridden at cluster level via `spec.primary.allowTransfer` or
2181 /// `spec.secondary.allowTransfer` for role-specific ACLs.
2182 #[serde(default)]
2183 pub allow_transfer: Option<Vec<String>>,
2184
2185 /// DNSSEC (DNS Security Extensions) configuration
2186 ///
2187 /// Configures DNSSEC signing and validation. DNSSEC provides cryptographic
2188 /// authentication of DNS data to prevent spoofing and cache poisoning attacks.
2189 ///
2190 /// See `DNSSECConfig` for detailed options.
2191 #[serde(default)]
2192 pub dnssec: Option<DNSSECConfig>,
2193
2194 /// DNS forwarders for recursive resolution
2195 ///
2196 /// List of upstream DNS servers to forward queries to when recursion is enabled.
2197 /// Used for hybrid authoritative/recursive configurations.
2198 ///
2199 /// Only relevant when `recursion: true`.
2200 ///
2201 /// Examples:
2202 /// - `["8.8.8.8", "8.8.4.4"]` - Google Public DNS
2203 /// - `["1.1.1.1", "1.0.0.1"]` - Cloudflare DNS
2204 /// - `["10.0.0.53"]` - Internal corporate DNS resolver
2205 #[serde(default)]
2206 pub forwarders: Option<Vec<String>>,
2207
2208 /// IPv4 addresses to listen on for DNS queries
2209 ///
2210 /// Specifies which IPv4 interfaces and ports the DNS server should bind to.
2211 ///
2212 /// Default: All IPv4 interfaces on port 53
2213 ///
2214 /// Examples:
2215 /// - `["any"]` - Listen on all IPv4 interfaces
2216 /// - `["127.0.0.1"]` - Listen only on localhost
2217 /// - `["10.0.0.1"]` - Listen on specific IP address
2218 #[serde(default)]
2219 pub listen_on: Option<Vec<String>>,
2220
2221 /// IPv6 addresses to listen on for DNS queries
2222 ///
2223 /// Specifies which IPv6 interfaces and ports the DNS server should bind to.
2224 ///
2225 /// Default: All IPv6 interfaces on port 53 (if IPv6 is available)
2226 ///
2227 /// Examples:
2228 /// - `["any"]` - Listen on all IPv6 interfaces
2229 /// - `["::1"]` - Listen only on IPv6 localhost
2230 /// - `["none"]` - Disable IPv6 listening
2231 #[serde(default)]
2232 pub listen_on_v6: Option<Vec<String>>,
2233
2234 /// Reference to an existing Kubernetes Secret containing RNDC key.
2235 ///
2236 /// If specified at the global config level, all instances in the cluster will use
2237 /// this existing Secret instead of auto-generating individual secrets, unless
2238 /// overridden at the role (primary/secondary) or instance level.
2239 ///
2240 /// This allows centralized RNDC key management for the entire cluster.
2241 ///
2242 /// Precedence order (highest to lowest):
2243 /// 1. Instance level (`spec.rndcSecretRef`)
2244 /// 2. Role level (`spec.primary.rndcSecretRef` or `spec.secondary.rndcSecretRef`)
2245 /// 3. Global level (`spec.global.rndcSecretRef`)
2246 /// 4. Auto-generated (default)
2247 #[serde(default)]
2248 pub rndc_secret_ref: Option<RndcSecretRef>,
2249
2250 /// Bindcar RNDC API sidecar container configuration.
2251 ///
2252 /// The API container provides an HTTP interface for managing zones via rndc.
2253 /// This configuration is inherited by all instances unless overridden.
2254 #[serde(default)]
2255 pub bindcar_config: Option<BindcarConfig>,
2256}
2257
2258/// DNSSEC (DNS Security Extensions) configuration
2259///
2260/// DNSSEC adds cryptographic signatures to DNS records to ensure authenticity and integrity.
2261/// This configuration supports both DNSSEC validation (verifying signatures from upstream)
2262/// and DNSSEC signing (cryptographically signing your own zones).
2263///
2264/// # Example
2265///
2266/// ```yaml
2267/// dnssec:
2268/// validation: true # Validate upstream DNSSEC responses
2269/// signing:
2270/// enabled: true
2271/// policy: "default"
2272/// algorithm: "ECDSAP256SHA256"
2273/// kskLifetime: "365d"
2274/// zskLifetime: "90d"
2275/// ```
2276#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2277#[serde(rename_all = "camelCase")]
2278pub struct DNSSECConfig {
2279 /// Enable DNSSEC validation of responses
2280 ///
2281 /// When enabled, BIND will validate DNSSEC signatures on responses from other
2282 /// nameservers. Invalid or missing signatures will cause queries to fail.
2283 ///
2284 /// Default: `false`
2285 ///
2286 /// **Important**: Requires valid DNSSEC trust anchors and proper network connectivity
2287 /// to root DNS servers. May cause resolution failures if DNSSEC is broken upstream.
2288 #[serde(default)]
2289 pub validation: Option<bool>,
2290
2291 /// Enable DNSSEC zone signing configuration
2292 ///
2293 /// Configures automatic DNSSEC signing for zones served by this cluster.
2294 /// When enabled, BIND9 will automatically generate keys, sign zones, and
2295 /// rotate keys based on the configured policy.
2296 ///
2297 /// **Important**: Requires BIND 9.16+ for modern `dnssec-policy` support.
2298 #[serde(default)]
2299 pub signing: Option<DNSSECSigningConfig>,
2300}
2301
2302/// DNSSEC zone signing configuration
2303///
2304/// Configures automatic DNSSEC key generation, zone signing, and key rotation.
2305/// Uses BIND9's modern `dnssec-policy` for declarative key management.
2306///
2307/// # Key Management Options
2308///
2309/// 1. **User-Supplied Keys** (Production): Keys managed externally via Secrets
2310/// 2. **Auto-Generated Keys** (Dev/Test): BIND9 generates keys, operator backs up to Secrets
2311/// 3. **Persistent Storage** (Legacy): Keys stored in `PersistentVolume`
2312///
2313/// # Example
2314///
2315/// ```yaml
2316/// signing:
2317/// enabled: true
2318/// policy: "default"
2319/// algorithm: "ECDSAP256SHA256"
2320/// kskLifetime: "365d"
2321/// zskLifetime: "90d"
2322/// nsec3: true
2323/// nsec3Iterations: 0
2324/// ```
2325#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2326#[serde(rename_all = "camelCase")]
2327pub struct DNSSECSigningConfig {
2328 /// Enable DNSSEC signing for zones
2329 ///
2330 /// When true, zones will be automatically signed with DNSSEC.
2331 /// Keys are generated and managed according to the configured policy.
2332 ///
2333 /// Default: `false`
2334 #[serde(default)]
2335 pub enabled: bool,
2336
2337 /// DNSSEC policy name
2338 ///
2339 /// Name of the DNSSEC policy to apply. Built-in policies:
2340 /// - `"default"` - Standard policy with ECDSA P-256, 365d KSK, 90d ZSK
2341 ///
2342 /// Custom policies can be defined in future enhancements.
2343 ///
2344 /// Default: `"default"`
2345 #[serde(default)]
2346 pub policy: Option<String>,
2347
2348 /// DNSSEC algorithm
2349 ///
2350 /// Cryptographic algorithm for DNSSEC signing. Supported algorithms:
2351 /// - `"ECDSAP256SHA256"` (13) - ECDSA P-256 with SHA-256 (recommended, fast)
2352 /// - `"ECDSAP384SHA384"` (14) - ECDSA P-384 with SHA-384 (higher security)
2353 /// - `"RSASHA256"` (8) - RSA with SHA-256 (widely compatible)
2354 ///
2355 /// ECDSA algorithms are recommended for performance and smaller key sizes.
2356 ///
2357 /// Default: `"ECDSAP256SHA256"`
2358 #[serde(default)]
2359 pub algorithm: Option<String>,
2360
2361 /// Key Signing Key (KSK) lifetime
2362 ///
2363 /// Duration before KSK is rotated. Format: "365d", "1y", "8760h"
2364 ///
2365 /// KSK signs the `DNSKEY` `RRset` and is published in the parent zone as a `DS` record.
2366 /// Longer lifetimes reduce `DS` update frequency but increase impact of key compromise.
2367 ///
2368 /// Default: `"365d"` (1 year)
2369 #[serde(default)]
2370 pub ksk_lifetime: Option<String>,
2371
2372 /// Zone Signing Key (ZSK) lifetime
2373 ///
2374 /// Duration before ZSK is rotated. Format: "90d", "3m", "2160h"
2375 ///
2376 /// ZSK signs all other records in the zone. Shorter lifetimes improve security
2377 /// but increase signing overhead.
2378 ///
2379 /// Default: `"90d"` (3 months)
2380 #[serde(default)]
2381 pub zsk_lifetime: Option<String>,
2382
2383 /// Use NSEC3 instead of NSEC for authenticated denial of existence
2384 ///
2385 /// NSEC3 hashes zone names to prevent zone enumeration attacks.
2386 /// Recommended for privacy-sensitive zones.
2387 ///
2388 /// Default: `false` (use NSEC)
2389 #[serde(default)]
2390 pub nsec3: Option<bool>,
2391
2392 /// NSEC3 salt (hex string)
2393 ///
2394 /// Salt value for NSEC3 hashing. If not specified, BIND9 auto-generates.
2395 /// Format: hex string (e.g., "AABBCCDD")
2396 ///
2397 /// Default: Auto-generated by BIND9
2398 #[serde(default)]
2399 pub nsec3_salt: Option<String>,
2400
2401 /// NSEC3 iterations
2402 ///
2403 /// Number of hash iterations for NSEC3. RFC 9276 recommends 0 for performance.
2404 ///
2405 /// **Important**: Higher values significantly impact query performance.
2406 ///
2407 /// Default: `0` (per RFC 9276 recommendation)
2408 #[serde(default)]
2409 pub nsec3_iterations: Option<u32>,
2410
2411 /// DNSSEC key source configuration
2412 ///
2413 /// Specifies where DNSSEC keys come from:
2414 /// - User-supplied Secret (recommended for production)
2415 /// - Persistent storage (legacy)
2416 ///
2417 /// If not specified and `auto_generate` is true, keys are generated in emptyDir
2418 /// and optionally backed up to Secrets.
2419 #[serde(default)]
2420 pub keys_from: Option<DNSSECKeySource>,
2421
2422 /// Auto-generate DNSSEC keys if no `keys_from` specified
2423 ///
2424 /// When true, BIND9 generates keys automatically using the configured policy.
2425 /// Recommended for development and testing.
2426 ///
2427 /// Default: `true`
2428 #[serde(default)]
2429 pub auto_generate: Option<bool>,
2430
2431 /// Export auto-generated keys to Secret for backup/restore
2432 ///
2433 /// When true, operator exports BIND9-generated keys to a Kubernetes Secret.
2434 /// Enables self-healing: keys are restored from Secret on pod restart.
2435 ///
2436 /// Secret name format: `dnssec-keys-<zone-name>-generated`
2437 ///
2438 /// Default: `true`
2439 #[serde(default)]
2440 pub export_to_secret: Option<bool>,
2441}
2442
2443/// DNSSEC key source configuration
2444///
2445/// Defines where DNSSEC keys are loaded from. Supports multiple patterns:
2446///
2447/// 1. **User-Supplied Secret** (Production):
2448/// - Keys managed externally (`Vault`, `ExternalSecrets`, `sealed-secrets`)
2449/// - User controls rotation timing
2450/// - `GitOps` friendly
2451///
2452/// 2. **Persistent Storage** (Legacy):
2453/// - Keys stored in `PersistentVolume`
2454/// - Traditional BIND9 pattern
2455///
2456/// # Example: User-Supplied Keys
2457///
2458/// ```yaml
2459/// keysFrom:
2460/// secretRef:
2461/// name: "dnssec-keys-example-com"
2462/// ```
2463///
2464/// # Example: Persistent Storage
2465///
2466/// ```yaml
2467/// keysFrom:
2468/// persistentVolume:
2469/// accessModes:
2470/// - ReadWriteOnce
2471/// resources:
2472/// requests:
2473/// storage: 100Mi
2474/// ```
2475#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2476#[serde(rename_all = "camelCase")]
2477pub struct DNSSECKeySource {
2478 /// Secret containing DNSSEC keys
2479 ///
2480 /// Reference to a Kubernetes Secret with DNSSEC key files.
2481 ///
2482 /// Secret data format:
2483 /// - `K<zone>.+<alg>+<tag>.key` - Public key file
2484 /// - `K<zone>.+<alg>+<tag>.private` - Private key file
2485 ///
2486 /// Example: `Kexample.com.+013+12345.key`
2487 #[serde(default)]
2488 pub secret_ref: Option<SecretReference>,
2489
2490 /// Persistent volume for DNSSEC keys (legacy/compatibility)
2491 ///
2492 /// **Note**: Not cloud-native. Use `secret_ref` for production.
2493 #[serde(default)]
2494 pub persistent_volume: Option<k8s_openapi::api::core::v1::PersistentVolumeClaimSpec>,
2495}
2496
2497/// Reference to a Kubernetes Secret
2498///
2499/// Used for referencing external Secrets containing DNSSEC keys,
2500/// certificates, or other sensitive data.
2501#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2502#[serde(rename_all = "camelCase")]
2503pub struct SecretReference {
2504 /// Secret name
2505 pub name: String,
2506
2507 /// Optional namespace (defaults to same namespace as the resource)
2508 #[serde(default)]
2509 pub namespace: Option<String>,
2510}
2511
2512/// DNSSEC status information for a signed zone
2513///
2514/// Tracks DNSSEC signing status, DS records for parent zones,
2515/// and key rotation timestamps.
2516///
2517/// This status is populated by the `DNSZone` controller after
2518/// successful zone signing.
2519#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2520#[serde(rename_all = "camelCase")]
2521pub struct DNSSECStatus {
2522 /// Zone is signed with DNSSEC
2523 pub signed: bool,
2524
2525 /// DS (Delegation Signer) records for parent zone delegation
2526 ///
2527 /// These records must be published in the parent zone to complete
2528 /// the DNSSEC chain of trust.
2529 ///
2530 /// Format: `<zone> IN DS <keytag> <algorithm> <digesttype> <digest>`
2531 ///
2532 /// Example: `["example.com. IN DS 12345 13 2 ABC123..."]`
2533 #[serde(default)]
2534 pub ds_records: Vec<String>,
2535
2536 /// KSK key tag (numeric identifier)
2537 ///
2538 /// Identifies the Key Signing Key used to sign the DNSKEY `RRset`.
2539 /// This value appears in the DS record.
2540 #[serde(default)]
2541 pub key_tag: Option<u32>,
2542
2543 /// DNSSEC algorithm name
2544 ///
2545 /// Example: `"ECDSAP256SHA256"`, `"RSASHA256"`
2546 #[serde(default)]
2547 pub algorithm: Option<String>,
2548
2549 /// Next scheduled key rollover timestamp (ISO 8601)
2550 ///
2551 /// When the next automatic key rotation will occur.
2552 ///
2553 /// Example: `"2026-04-02T00:00:00Z"`
2554 #[serde(default)]
2555 pub next_key_rollover: Option<String>,
2556
2557 /// Last key rollover timestamp (ISO 8601)
2558 ///
2559 /// When the most recent key rotation occurred.
2560 ///
2561 /// Example: `"2025-04-02T00:00:00Z"`
2562 #[serde(default)]
2563 pub last_key_rollover: Option<String>,
2564}
2565
2566/// Container image configuration for BIND9 instances
2567#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2568#[serde(rename_all = "camelCase")]
2569pub struct ImageConfig {
2570 /// Container image repository and tag for BIND9
2571 ///
2572 /// Example: "internetsystemsconsortium/bind9:9.18"
2573 #[serde(default)]
2574 pub image: Option<String>,
2575
2576 /// Image pull policy
2577 ///
2578 /// Example: `IfNotPresent`, `Always`, `Never`
2579 #[serde(default)]
2580 pub image_pull_policy: Option<String>,
2581
2582 /// Reference to image pull secrets for private registries
2583 #[serde(default)]
2584 pub image_pull_secrets: Option<Vec<String>>,
2585}
2586
2587/// `ConfigMap` references for BIND9 configuration files
2588#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2589#[serde(rename_all = "camelCase")]
2590pub struct ConfigMapRefs {
2591 /// `ConfigMap` containing named.conf file
2592 ///
2593 /// If not specified, a default configuration will be generated
2594 #[serde(default)]
2595 pub named_conf: Option<String>,
2596
2597 /// `ConfigMap` containing named.conf.options file
2598 ///
2599 /// If not specified, a default configuration will be generated
2600 #[serde(default)]
2601 pub named_conf_options: Option<String>,
2602
2603 /// `ConfigMap` containing named.conf.zones file
2604 ///
2605 /// Optional. If specified, the zones file from this `ConfigMap` will be included in named.conf.
2606 /// If not specified, no zones file will be included (zones can be added dynamically via RNDC).
2607 /// Use this for pre-configured zones or to import existing BIND9 zone configurations.
2608 #[serde(default)]
2609 pub named_conf_zones: Option<String>,
2610}
2611
2612/// Service configuration including spec and annotations
2613///
2614/// Allows customization of both the Kubernetes Service spec and metadata annotations.
2615#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
2616#[serde(rename_all = "camelCase")]
2617pub struct ServiceConfig {
2618 /// Annotations to apply to the Service metadata
2619 ///
2620 /// Common use cases:
2621 /// - `MetalLB` address pool selection: `metallb.universe.tf/address-pool: my-ip-pool`
2622 /// - AWS load balancer configuration: `service.beta.kubernetes.io/aws-load-balancer-type: nlb`
2623 /// - External DNS hostname: `external-dns.alpha.kubernetes.io/hostname: dns.example.com`
2624 ///
2625 /// Example:
2626 /// ```yaml
2627 /// annotations:
2628 /// metallb.universe.tf/address-pool: my-ip-pool
2629 /// external-dns.alpha.kubernetes.io/hostname: ns1.example.com
2630 /// ```
2631 #[serde(skip_serializing_if = "Option::is_none")]
2632 pub annotations: Option<BTreeMap<String, String>>,
2633
2634 /// Custom Kubernetes Service spec
2635 ///
2636 /// Allows full customization of the Kubernetes Service created for DNS servers.
2637 /// This accepts the same fields as the standard Kubernetes Service `spec`.
2638 ///
2639 /// Common fields:
2640 /// - `type`: Service type (`ClusterIP`, `NodePort`, `LoadBalancer`)
2641 /// - `loadBalancerIP`: Specific IP for `LoadBalancer` type
2642 /// - `externalTrafficPolicy`: `Local` or `Cluster`
2643 /// - `sessionAffinity`: `ClientIP` or `None`
2644 /// - `clusterIP`: Specific cluster IP (use with caution)
2645 ///
2646 /// Fields specified here are merged with defaults. Unspecified fields use safe defaults:
2647 /// - `type: ClusterIP` (if not specified)
2648 /// - Ports 53/TCP and 53/UDP (always set)
2649 /// - Selector matching the instance labels (always set)
2650 #[serde(skip_serializing_if = "Option::is_none")]
2651 pub spec: Option<ServiceSpec>,
2652}
2653
2654/// Primary instance configuration
2655///
2656/// Groups all configuration specific to primary (authoritative) DNS instances.
2657#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
2658#[serde(rename_all = "camelCase")]
2659pub struct PrimaryConfig {
2660 /// Number of primary instance replicas (default: 1)
2661 ///
2662 /// This controls how many replicas each primary instance in this cluster should have.
2663 /// Can be overridden at the instance level.
2664 #[serde(skip_serializing_if = "Option::is_none")]
2665 #[schemars(range(min = 0, max = 100))]
2666 pub replicas: Option<i32>,
2667
2668 /// Additional labels to apply to primary `Bind9Instance` resources
2669 ///
2670 /// These labels are propagated from the cluster/provider to all primary instances.
2671 /// They are merged with standard labels (app.kubernetes.io/*) and can be used for:
2672 /// - Instance selection via `DNSZone.spec.bind9InstancesFrom` label selectors
2673 /// - Pod selectors in network policies
2674 /// - Monitoring and alerting label filters
2675 /// - Custom organizational taxonomy
2676 ///
2677 /// Example:
2678 /// ```yaml
2679 /// primary:
2680 /// labels:
2681 /// environment: production
2682 /// tier: frontend
2683 /// region: us-east-1
2684 /// ```
2685 ///
2686 /// These labels will appear on the `Bind9Instance` metadata and can be referenced
2687 /// by `DNSZone` resources using `bind9InstancesFrom.selector.matchLabels`.
2688 #[serde(default, skip_serializing_if = "Option::is_none")]
2689 pub labels: Option<BTreeMap<String, String>>,
2690
2691 /// Custom Kubernetes Service configuration for primary instances
2692 ///
2693 /// Allows full customization of the Kubernetes Service created for primary DNS servers,
2694 /// including both Service spec fields and metadata annotations.
2695 ///
2696 /// Annotations are commonly used for:
2697 /// - `MetalLB` address pool selection
2698 /// - Cloud provider load balancer configuration
2699 /// - External DNS integration
2700 /// - Linkerd service mesh annotations
2701 ///
2702 /// Fields specified here are merged with defaults. Unspecified fields use safe defaults:
2703 /// - `type: ClusterIP` (if not specified)
2704 /// - Ports 53/TCP and 53/UDP (always set)
2705 /// - Selector matching the instance labels (always set)
2706 #[serde(skip_serializing_if = "Option::is_none")]
2707 pub service: Option<ServiceConfig>,
2708
2709 /// Allow-transfer ACL for primary instances
2710 ///
2711 /// Overrides the default auto-detected Pod CIDR allow-transfer configuration
2712 /// for all primary instances in this cluster. Use this to restrict or expand
2713 /// which IP addresses can perform zone transfers from primary servers.
2714 ///
2715 /// If not specified, defaults to cluster Pod CIDRs (auto-detected from Kubernetes Nodes).
2716 ///
2717 /// Examples:
2718 /// - `["10.0.0.0/8"]` - Allow transfers from entire 10.x network
2719 /// - `["any"]` - Allow transfers from any IP (public internet)
2720 /// - `[]` - Deny all zone transfers (empty list means "none")
2721 ///
2722 /// Can be overridden at the instance level via `spec.config.allowTransfer`.
2723 #[serde(default, skip_serializing_if = "Option::is_none")]
2724 pub allow_transfer: Option<Vec<String>>,
2725
2726 /// Reference to an existing Kubernetes Secret containing RNDC key for all primary instances.
2727 ///
2728 /// If specified, all primary instances in this cluster will use this existing Secret
2729 /// instead of auto-generating individual secrets. This allows sharing the same RNDC key
2730 /// across all primary instances.
2731 ///
2732 /// Can be overridden at the instance level via `spec.rndcSecretRef`.
2733 #[serde(default, skip_serializing_if = "Option::is_none")]
2734 #[deprecated(
2735 since = "0.6.0",
2736 note = "Use `rndc_key` instead. This field will be removed in v1.0.0"
2737 )]
2738 pub rndc_secret_ref: Option<RndcSecretRef>,
2739
2740 /// RNDC key configuration for all primary instances with lifecycle management.
2741 ///
2742 /// Supports automatic key rotation, Secret references, and inline Secret specifications.
2743 /// Overrides global RNDC configuration for primary instances.
2744 ///
2745 /// **Precedence order**:
2746 /// 1. Instance level (`spec.rndcKey`)
2747 /// 2. Role level (`spec.primary.rndcKey` or `spec.secondary.rndcKey`)
2748 /// 3. Global level (cluster-wide RNDC configuration)
2749 /// 4. Auto-generated (default)
2750 ///
2751 /// Can be overridden at the instance level via `spec.rndcKey`.
2752 ///
2753 /// **Backward compatibility**: If both `rndc_key` and `rndc_secret_ref` are specified,
2754 /// `rndc_key` takes precedence. For smooth migration, `rndc_secret_ref` will continue
2755 /// to work but is deprecated.
2756 ///
2757 /// # Example
2758 ///
2759 /// ```yaml
2760 /// primary:
2761 /// replicas: 1
2762 /// rndcKey:
2763 /// autoRotate: true
2764 /// rotateAfter: 720h # 30 days
2765 /// algorithm: hmac-sha256
2766 /// ```
2767 #[serde(skip_serializing_if = "Option::is_none")]
2768 pub rndc_key: Option<RndcKeyConfig>,
2769}
2770
2771/// Secondary instance configuration
2772///
2773/// Groups all configuration specific to secondary (replica) DNS instances.
2774#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
2775#[serde(rename_all = "camelCase")]
2776pub struct SecondaryConfig {
2777 /// Number of secondary instance replicas (default: 1)
2778 ///
2779 /// This controls how many replicas each secondary instance in this cluster should have.
2780 /// Can be overridden at the instance level.
2781 #[serde(skip_serializing_if = "Option::is_none")]
2782 #[schemars(range(min = 0, max = 100))]
2783 pub replicas: Option<i32>,
2784
2785 /// Additional labels to apply to secondary `Bind9Instance` resources
2786 ///
2787 /// These labels are propagated from the cluster/provider to all secondary instances.
2788 /// They are merged with standard labels (app.kubernetes.io/*) and can be used for:
2789 /// - Instance selection via `DNSZone.spec.bind9InstancesFrom` label selectors
2790 /// - Pod selectors in network policies
2791 /// - Monitoring and alerting label filters
2792 /// - Custom organizational taxonomy
2793 ///
2794 /// Example:
2795 /// ```yaml
2796 /// secondary:
2797 /// labels:
2798 /// environment: production
2799 /// tier: backend
2800 /// region: us-west-2
2801 /// ```
2802 ///
2803 /// These labels will appear on the `Bind9Instance` metadata and can be referenced
2804 /// by `DNSZone` resources using `bind9InstancesFrom.selector.matchLabels`.
2805 #[serde(default, skip_serializing_if = "Option::is_none")]
2806 pub labels: Option<BTreeMap<String, String>>,
2807
2808 /// Custom Kubernetes Service configuration for secondary instances
2809 ///
2810 /// Allows full customization of the Kubernetes Service created for secondary DNS servers,
2811 /// including both Service spec fields and metadata annotations.
2812 ///
2813 /// Annotations are commonly used for:
2814 /// - `MetalLB` address pool selection
2815 /// - Cloud provider load balancer configuration
2816 /// - External DNS integration
2817 /// - Linkerd service mesh annotations
2818 ///
2819 /// Allows different service configurations for primary vs secondary instances.
2820 /// Example: Primaries use `LoadBalancer` with specific annotations, secondaries use `ClusterIP`
2821 ///
2822 /// See `PrimaryConfig.service` for detailed field documentation.
2823 #[serde(skip_serializing_if = "Option::is_none")]
2824 pub service: Option<ServiceConfig>,
2825
2826 /// Allow-transfer ACL for secondary instances
2827 ///
2828 /// Overrides the default auto-detected Pod CIDR allow-transfer configuration
2829 /// for all secondary instances in this cluster. Use this to restrict or expand
2830 /// which IP addresses can perform zone transfers from secondary servers.
2831 ///
2832 /// If not specified, defaults to cluster Pod CIDRs (auto-detected from Kubernetes Nodes).
2833 ///
2834 /// Examples:
2835 /// - `["10.0.0.0/8"]` - Allow transfers from entire 10.x network
2836 /// - `["any"]` - Allow transfers from any IP (public internet)
2837 /// - `[]` - Deny all zone transfers (empty list means "none")
2838 ///
2839 /// Can be overridden at the instance level via `spec.config.allowTransfer`.
2840 #[serde(default, skip_serializing_if = "Option::is_none")]
2841 pub allow_transfer: Option<Vec<String>>,
2842
2843 /// Reference to an existing Kubernetes Secret containing RNDC key for all secondary instances.
2844 ///
2845 /// If specified, all secondary instances in this cluster will use this existing Secret
2846 /// instead of auto-generating individual secrets. This allows sharing the same RNDC key
2847 /// across all secondary instances.
2848 ///
2849 /// Can be overridden at the instance level via `spec.rndcSecretRef`.
2850 #[serde(default, skip_serializing_if = "Option::is_none")]
2851 #[deprecated(
2852 since = "0.6.0",
2853 note = "Use `rndc_key` instead. This field will be removed in v1.0.0"
2854 )]
2855 pub rndc_secret_ref: Option<RndcSecretRef>,
2856
2857 /// RNDC key configuration for all secondary instances with lifecycle management.
2858 ///
2859 /// Supports automatic key rotation, Secret references, and inline Secret specifications.
2860 /// Overrides global RNDC configuration for secondary instances.
2861 ///
2862 /// **Precedence order**:
2863 /// 1. Instance level (`spec.rndcKey`)
2864 /// 2. Role level (`spec.primary.rndcKey` or `spec.secondary.rndcKey`)
2865 /// 3. Global level (cluster-wide RNDC configuration)
2866 /// 4. Auto-generated (default)
2867 ///
2868 /// Can be overridden at the instance level via `spec.rndcKey`.
2869 ///
2870 /// **Backward compatibility**: If both `rndc_key` and `rndc_secret_ref` are specified,
2871 /// `rndc_key` takes precedence. For smooth migration, `rndc_secret_ref` will continue
2872 /// to work but is deprecated.
2873 ///
2874 /// # Example
2875 ///
2876 /// ```yaml
2877 /// secondary:
2878 /// replicas: 2
2879 /// rndcKey:
2880 /// autoRotate: true
2881 /// rotateAfter: 720h # 30 days
2882 /// algorithm: hmac-sha256
2883 /// ```
2884 #[serde(skip_serializing_if = "Option::is_none")]
2885 pub rndc_key: Option<RndcKeyConfig>,
2886}
2887
2888/// Common specification fields shared between namespace-scoped and cluster-scoped BIND9 clusters.
2889///
2890/// This struct contains all configuration that is common to both `Bind9Cluster` (namespace-scoped)
2891/// and `ClusterBind9Provider` (cluster-scoped). By using this shared struct, we avoid code duplication
2892/// and ensure consistency between the two cluster types.
2893#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2894#[serde(rename_all = "camelCase")]
2895pub struct Bind9ClusterCommonSpec {
2896 /// Shared BIND9 version for the cluster
2897 ///
2898 /// If not specified, defaults to "9.18".
2899 #[serde(default = "default_bind9_version")]
2900 #[schemars(default = "default_bind9_version")]
2901 pub version: Option<String>,
2902
2903 /// Primary instance configuration
2904 ///
2905 /// Configuration specific to primary (authoritative) DNS instances,
2906 /// including replica count and service specifications.
2907 #[serde(default, skip_serializing_if = "Option::is_none")]
2908 pub primary: Option<PrimaryConfig>,
2909
2910 /// Secondary instance configuration
2911 ///
2912 /// Configuration specific to secondary (replica) DNS instances,
2913 /// including replica count and service specifications.
2914 #[serde(default, skip_serializing_if = "Option::is_none")]
2915 pub secondary: Option<SecondaryConfig>,
2916
2917 /// Container image configuration
2918 #[serde(default)]
2919 pub image: Option<ImageConfig>,
2920
2921 /// `ConfigMap` references for BIND9 configuration files
2922 #[serde(default)]
2923 pub config_map_refs: Option<ConfigMapRefs>,
2924
2925 /// Global configuration shared by all instances in the cluster
2926 ///
2927 /// This configuration applies to all instances (both primary and secondary)
2928 /// unless overridden at the instance level or by role-specific configuration.
2929 #[serde(default)]
2930 pub global: Option<Bind9Config>,
2931
2932 /// References to Kubernetes Secrets containing RNDC/TSIG keys for authenticated zone transfers.
2933 ///
2934 /// Each secret should contain the key name, algorithm, and base64-encoded secret value.
2935 /// These secrets are used for secure communication with BIND9 instances via RNDC and
2936 /// for authenticated zone transfers (AXFR/IXFR) between primary and secondary servers.
2937 #[serde(default)]
2938 pub rndc_secret_refs: Option<Vec<RndcSecretRef>>,
2939
2940 /// ACLs that can be referenced by instances
2941 #[serde(default)]
2942 pub acls: Option<BTreeMap<String, Vec<String>>>,
2943
2944 /// Volumes that can be mounted by instances in this cluster
2945 ///
2946 /// These volumes are inherited by all instances unless overridden.
2947 /// Common use cases include `PersistentVolumeClaims` for zone data storage.
2948 #[serde(default)]
2949 pub volumes: Option<Vec<Volume>>,
2950
2951 /// Volume mounts that specify where volumes should be mounted in containers
2952 ///
2953 /// These mounts are inherited by all instances unless overridden.
2954 #[serde(default)]
2955 pub volume_mounts: Option<Vec<VolumeMount>>,
2956}
2957
2958/// `Bind9Cluster` - Namespace-scoped DNS cluster for tenant-managed infrastructure.
2959///
2960/// A namespace-scoped cluster allows development teams to run their own isolated BIND9
2961/// DNS infrastructure within their namespace. Each team can manage their own cluster
2962/// independently, with RBAC controlling who can create and manage resources.
2963///
2964/// For platform-managed, cluster-wide DNS infrastructure, use `ClusterBind9Provider` instead.
2965///
2966/// # Use Cases
2967///
2968/// - Development teams need isolated DNS infrastructure for testing
2969/// - Multi-tenant environments where each team manages their own DNS
2970/// - Namespaced DNS services that don't need cluster-wide visibility
2971///
2972/// # Example
2973///
2974/// ```yaml
2975/// apiVersion: bindy.firestoned.io/v1beta1
2976/// kind: Bind9Cluster
2977/// metadata:
2978/// name: dev-team-dns
2979/// namespace: dev-team-alpha
2980/// spec:
2981/// version: "9.18"
2982/// primary:
2983/// replicas: 1
2984/// secondary:
2985/// replicas: 1
2986/// ```
2987#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
2988#[kube(
2989 group = "bindy.firestoned.io",
2990 version = "v1beta1",
2991 kind = "Bind9Cluster",
2992 namespaced,
2993 shortname = "b9c",
2994 shortname = "b9cs",
2995 doc = "Bind9Cluster defines a namespace-scoped logical grouping of BIND9 DNS server instances. Use this for tenant-managed DNS infrastructure isolated to a specific namespace. For platform-managed cluster-wide DNS, use ClusterBind9Provider instead.",
2996 printcolumn = r#"{"name":"Version","type":"string","jsonPath":".spec.version"}"#,
2997 printcolumn = r#"{"name":"Primary","type":"integer","jsonPath":".spec.primary.replicas"}"#,
2998 printcolumn = r#"{"name":"Secondary","type":"integer","jsonPath":".spec.secondary.replicas"}"#,
2999 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
3000)]
3001#[kube(status = "Bind9ClusterStatus")]
3002#[serde(rename_all = "camelCase")]
3003pub struct Bind9ClusterSpec {
3004 /// All cluster configuration is flattened from the common spec
3005 #[serde(flatten)]
3006 pub common: Bind9ClusterCommonSpec,
3007}
3008
3009/// `ClusterBind9Provider` - Cluster-scoped BIND9 DNS provider for platform teams.
3010///
3011/// A cluster-scoped provider allows platform teams to provision shared BIND9 DNS infrastructure
3012/// that is accessible from any namespace. This is ideal for shared services, production DNS,
3013/// or platform-managed infrastructure that multiple teams use.
3014///
3015/// `DNSZones` in any namespace can reference a `ClusterBind9Provider` using the `clusterProviderRef` field.
3016///
3017/// # Use Cases
3018///
3019/// - Platform team provides shared DNS infrastructure for all namespaces
3020/// - Production DNS services that serve multiple applications
3021/// - Centrally managed DNS with governance and compliance requirements
3022///
3023/// # Example
3024///
3025/// ```yaml
3026/// apiVersion: bindy.firestoned.io/v1beta1
3027/// kind: ClusterBind9Provider
3028/// metadata:
3029/// name: shared-production-dns
3030/// # No namespace - cluster-scoped
3031/// spec:
3032/// version: "9.18"
3033/// primary:
3034/// replicas: 3
3035/// service:
3036/// type: LoadBalancer
3037/// secondary:
3038/// replicas: 2
3039/// ```
3040#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
3041#[kube(
3042 group = "bindy.firestoned.io",
3043 version = "v1beta1",
3044 kind = "ClusterBind9Provider",
3045 // NOTE: No 'namespaced' attribute = cluster-scoped
3046 shortname = "cb9p",
3047 shortname = "cb9ps",
3048 doc = "ClusterBind9Provider defines a cluster-scoped BIND9 DNS provider that manages DNS infrastructure accessible from all namespaces. Use this for platform-managed DNS infrastructure. For tenant-managed namespace-scoped DNS, use Bind9Cluster instead.",
3049 printcolumn = r#"{"name":"Version","type":"string","jsonPath":".spec.version"}"#,
3050 printcolumn = r#"{"name":"Primary","type":"integer","jsonPath":".spec.primary.replicas"}"#,
3051 printcolumn = r#"{"name":"Secondary","type":"integer","jsonPath":".spec.secondary.replicas"}"#,
3052 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
3053)]
3054#[kube(status = "Bind9ClusterStatus")]
3055#[serde(rename_all = "camelCase")]
3056pub struct ClusterBind9ProviderSpec {
3057 /// Namespace where `Bind9Instance` resources will be created
3058 ///
3059 /// Since `ClusterBind9Provider` is cluster-scoped, instances need to be created in a specific namespace.
3060 /// Typically this would be a platform-managed namespace like `bindy-system`.
3061 ///
3062 /// All managed instances (primary and secondary) will be created in this namespace.
3063 /// `DNSZones` from any namespace can reference this provider via `clusterProviderRef`.
3064 ///
3065 /// **Default:** If not specified, instances will be created in the same namespace where the
3066 /// Bindy operator is running (from `POD_NAMESPACE` environment variable).
3067 ///
3068 /// Example: `bindy-system` for platform DNS infrastructure
3069 #[serde(skip_serializing_if = "Option::is_none")]
3070 pub namespace: Option<String>,
3071
3072 /// All cluster configuration is flattened from the common spec
3073 #[serde(flatten)]
3074 pub common: Bind9ClusterCommonSpec,
3075}
3076
3077/// `Bind9Cluster` status
3078#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
3079#[serde(rename_all = "camelCase")]
3080pub struct Bind9ClusterStatus {
3081 /// Status conditions for this cluster
3082 #[serde(default)]
3083 pub conditions: Vec<Condition>,
3084
3085 /// Observed generation for optimistic concurrency
3086 #[serde(skip_serializing_if = "Option::is_none")]
3087 pub observed_generation: Option<i64>,
3088
3089 /// Number of instances in this cluster
3090 #[serde(skip_serializing_if = "Option::is_none")]
3091 pub instance_count: Option<i32>,
3092
3093 /// Number of ready instances
3094 #[serde(skip_serializing_if = "Option::is_none")]
3095 pub ready_instances: Option<i32>,
3096
3097 /// Names of `Bind9Instance` resources created for this cluster
3098 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3099 pub instances: Vec<String>,
3100}
3101
3102/// Server role in the DNS cluster.
3103///
3104/// Determines whether the instance is authoritative (primary) or replicates from primaries (secondary).
3105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
3106#[serde(rename_all = "lowercase")]
3107pub enum ServerRole {
3108 /// Primary DNS server - authoritative source for zones.
3109 ///
3110 /// Primary servers hold the original zone data and process dynamic updates.
3111 /// Changes to zones are made on primaries and transferred to secondaries.
3112 Primary,
3113
3114 /// Secondary DNS server - replicates zones from primary servers.
3115 ///
3116 /// Secondary servers receive zone data via AXFR (full) or IXFR (incremental)
3117 /// zone transfers. They provide redundancy and geographic distribution.
3118 Secondary,
3119}
3120
3121impl ServerRole {
3122 /// Convert `ServerRole` to its string representation.
3123 ///
3124 /// Returns the lowercase zone type string used in BIND9 configuration
3125 /// and bindcar API calls.
3126 ///
3127 /// # Returns
3128 /// * `"primary"` for `ServerRole::Primary`
3129 /// * `"secondary"` for `ServerRole::Secondary`
3130 ///
3131 /// # Examples
3132 ///
3133 /// ```
3134 /// use bindy::crd::ServerRole;
3135 ///
3136 /// assert_eq!(ServerRole::Primary.as_str(), "primary");
3137 /// assert_eq!(ServerRole::Secondary.as_str(), "secondary");
3138 /// ```
3139 #[must_use]
3140 pub const fn as_str(&self) -> &'static str {
3141 match self {
3142 Self::Primary => "primary",
3143 Self::Secondary => "secondary",
3144 }
3145 }
3146}
3147
3148/// `Bind9Instance` represents a BIND9 DNS server deployment in Kubernetes.
3149///
3150/// Each `Bind9Instance` creates a Deployment, Service, `ConfigMap`, and Secret for managing
3151/// a BIND9 server. The instance communicates with the controller via RNDC protocol.
3152///
3153/// # Example
3154///
3155/// ```yaml
3156/// apiVersion: bindy.firestoned.io/v1beta1
3157/// kind: Bind9Instance
3158/// metadata:
3159/// name: dns-primary
3160/// namespace: bindy-system
3161/// spec:
3162/// clusterRef: my-dns-cluster
3163/// role: primary
3164/// replicas: 2
3165/// version: "9.18"
3166/// ```
3167#[derive(CustomResource, Clone, Debug, Serialize, Deserialize, JsonSchema)]
3168#[kube(
3169 group = "bindy.firestoned.io",
3170 version = "v1beta1",
3171 kind = "Bind9Instance",
3172 namespaced,
3173 shortname = "b9",
3174 shortname = "b9s",
3175 doc = "Bind9Instance represents a BIND9 DNS server deployment in Kubernetes. Each instance creates a Deployment, Service, ConfigMap, and Secret for managing a BIND9 server with RNDC protocol communication.",
3176 printcolumn = r#"{"name":"Cluster","type":"string","jsonPath":".spec.clusterRef"}"#,
3177 printcolumn = r#"{"name":"Role","type":"string","jsonPath":".spec.role"}"#,
3178 printcolumn = r#"{"name":"Replicas","type":"integer","jsonPath":".spec.replicas"}"#,
3179 printcolumn = r#"{"name":"Zones","type":"integer","jsonPath":".status.zonesCount"}"#,
3180 printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type=='Ready')].status"}"#
3181)]
3182#[kube(status = "Bind9InstanceStatus")]
3183#[serde(rename_all = "camelCase")]
3184pub struct Bind9InstanceSpec {
3185 /// Reference to the cluster this instance belongs to.
3186 ///
3187 /// Can reference either:
3188 /// - A namespace-scoped `Bind9Cluster` (must be in the same namespace as this instance)
3189 /// - A cluster-scoped `ClusterBind9Provider` (cluster-wide, accessible from any namespace)
3190 ///
3191 /// The cluster provides shared configuration and defines the logical grouping.
3192 /// The controller will automatically detect whether this references a namespace-scoped
3193 /// or cluster-scoped cluster resource.
3194 pub cluster_ref: String,
3195
3196 /// Role of this instance (primary or secondary).
3197 ///
3198 /// Primary instances are authoritative for zones. Secondary instances
3199 /// replicate zones from primaries via AXFR/IXFR.
3200 pub role: ServerRole,
3201
3202 /// Number of pod replicas for high availability.
3203 ///
3204 /// Defaults to 1 if not specified. For production, use 2+ replicas.
3205 #[serde(default)]
3206 #[schemars(range(min = 0, max = 100))]
3207 pub replicas: Option<i32>,
3208
3209 /// BIND9 version override. Inherits from cluster if not specified.
3210 ///
3211 /// Example: "9.18", "9.16"
3212 #[serde(default)]
3213 pub version: Option<String>,
3214
3215 /// Container image configuration override. Inherits from cluster if not specified.
3216 #[serde(default)]
3217 pub image: Option<ImageConfig>,
3218
3219 /// `ConfigMap` references override. Inherits from cluster if not specified.
3220 #[serde(default)]
3221 pub config_map_refs: Option<ConfigMapRefs>,
3222
3223 /// Instance-specific BIND9 configuration overrides.
3224 ///
3225 /// Overrides cluster-level configuration for this instance only.
3226 #[serde(default)]
3227 pub config: Option<Bind9Config>,
3228
3229 /// Primary server addresses for zone transfers (required for secondary instances).
3230 ///
3231 /// List of IP addresses or hostnames of primary servers to transfer zones from.
3232 /// Example: `["10.0.1.10", "primary.example.com"]`
3233 #[serde(default)]
3234 pub primary_servers: Option<Vec<String>>,
3235
3236 /// Volumes override for this instance. Inherits from cluster if not specified.
3237 ///
3238 /// These volumes override cluster-level volumes. Common use cases include
3239 /// instance-specific `PersistentVolumeClaims` for zone data storage.
3240 #[serde(default)]
3241 pub volumes: Option<Vec<Volume>>,
3242
3243 /// Volume mounts override for this instance. Inherits from cluster if not specified.
3244 ///
3245 /// These mounts override cluster-level volume mounts.
3246 #[serde(default)]
3247 pub volume_mounts: Option<Vec<VolumeMount>>,
3248
3249 /// Reference to an existing Kubernetes Secret containing RNDC key.
3250 ///
3251 /// If specified, uses this existing Secret instead of auto-generating one.
3252 /// The Secret must contain the keys specified in the reference (defaults: "key-name", "algorithm", "secret", "rndc.key").
3253 /// This allows sharing RNDC keys across instances or using externally managed secrets.
3254 ///
3255 /// If not specified, a Secret will be auto-generated for this instance.
3256 #[serde(default)]
3257 #[deprecated(
3258 since = "0.6.0",
3259 note = "Use `rndc_key` instead. This field will be removed in v1.0.0"
3260 )]
3261 pub rndc_secret_ref: Option<RndcSecretRef>,
3262
3263 /// Instance-level RNDC key configuration with lifecycle management.
3264 ///
3265 /// Supports automatic key rotation, Secret references, and inline Secret specifications.
3266 /// Overrides role-level and global RNDC configuration for this specific instance.
3267 ///
3268 /// **Precedence order**:
3269 /// 1. **Instance level** (`spec.rndcKey`) - Highest priority
3270 /// 2. Role level (`spec.primary.rndcKey` or `spec.secondary.rndcKey`)
3271 /// 3. Global level (cluster-wide RNDC configuration)
3272 /// 4. Auto-generated (default)
3273 ///
3274 /// **Backward compatibility**: If both `rndc_key` and `rndc_secret_ref` are specified,
3275 /// `rndc_key` takes precedence. For smooth migration, `rndc_secret_ref` will continue
3276 /// to work but is deprecated.
3277 ///
3278 /// # Example
3279 ///
3280 /// ```yaml
3281 /// apiVersion: bindy.firestoned.io/v1beta1
3282 /// kind: Bind9Instance
3283 /// spec:
3284 /// rndcKey:
3285 /// autoRotate: true
3286 /// rotateAfter: 2160h # 90 days
3287 /// algorithm: hmac-sha512
3288 /// ```
3289 #[serde(skip_serializing_if = "Option::is_none")]
3290 pub rndc_key: Option<RndcKeyConfig>,
3291
3292 /// Storage configuration for zone files.
3293 ///
3294 /// Specifies how zone files should be stored. Defaults to emptyDir (ephemeral storage).
3295 /// For persistent storage, use persistentVolumeClaim.
3296 #[serde(default)]
3297 pub storage: Option<StorageConfig>,
3298
3299 /// Bindcar RNDC API sidecar container configuration.
3300 ///
3301 /// The API container provides an HTTP interface for managing zones via rndc.
3302 /// If not specified, uses default configuration.
3303 #[serde(default)]
3304 pub bindcar_config: Option<BindcarConfig>,
3305}
3306
3307/// `Bind9Instance` status
3308#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
3309#[serde(rename_all = "camelCase")]
3310pub struct Bind9InstanceStatus {
3311 #[serde(default)]
3312 pub conditions: Vec<Condition>,
3313 #[serde(skip_serializing_if = "Option::is_none")]
3314 pub observed_generation: Option<i64>,
3315 /// IP or hostname of this instance's service
3316 #[serde(skip_serializing_if = "Option::is_none")]
3317 pub service_address: Option<String>,
3318 /// Resolved cluster reference with full object details.
3319 ///
3320 /// This field is populated by the instance reconciler and contains the full Kubernetes
3321 /// object reference (kind, apiVersion, namespace, name) of the cluster this instance
3322 /// belongs to. This provides backward compatibility with `spec.clusterRef` (which is
3323 /// just a string name) and enables proper Kubernetes object references.
3324 ///
3325 /// For namespace-scoped `Bind9Cluster`, includes namespace.
3326 /// For cluster-scoped `ClusterBind9Provider`, namespace will be empty.
3327 #[serde(skip_serializing_if = "Option::is_none")]
3328 pub cluster_ref: Option<ClusterReference>,
3329 /// List of DNS zones that have selected this instance.
3330 ///
3331 /// This field is automatically populated by a status-only watcher on `DNSZones`.
3332 /// When a `DNSZone`'s `status.bind9Instances` includes this instance, the zone
3333 /// is added to this list. This provides a reverse lookup: instance → zones.
3334 ///
3335 /// Updated by: `DNSZone` status watcher (not by instance reconciler)
3336 /// Used for: Observability, debugging zone assignments
3337 #[serde(default)]
3338 #[serde(skip_serializing_if = "Vec::is_empty")]
3339 pub zones: Vec<ZoneReference>,
3340
3341 /// Number of zones in the `zones` list.
3342 ///
3343 /// This field is automatically updated whenever the `zones` list changes.
3344 /// It provides a quick way to see how many zones are selecting this instance
3345 /// without having to count the array elements.
3346 #[serde(skip_serializing_if = "Option::is_none")]
3347 pub zones_count: Option<i32>,
3348
3349 /// RNDC key rotation status and tracking information.
3350 ///
3351 /// Populated when `auto_rotate` is enabled in the RNDC configuration. Provides
3352 /// visibility into key lifecycle: creation time, next rotation time, and rotation count.
3353 ///
3354 /// This field is automatically updated by the instance reconciler whenever:
3355 /// - A new RNDC key is generated
3356 /// - An RNDC key is rotated
3357 /// - The rotation configuration changes
3358 ///
3359 /// **Note**: Only present when using operator-managed RNDC keys. If you specify
3360 /// `secret_ref` to use an external Secret, this field will be empty.
3361 #[serde(skip_serializing_if = "Option::is_none")]
3362 pub rndc_key_rotation: Option<RndcKeyRotationStatus>,
3363}
3364
3365/// RNDC key rotation status and tracking information.
3366///
3367/// Tracks the lifecycle of operator-managed RNDC keys including creation time,
3368/// next rotation time, last rotation time, and rotation count.
3369///
3370/// This status is automatically updated by the instance reconciler whenever keys
3371/// are created or rotated. It provides visibility into key age and rotation history
3372/// for compliance and operational purposes.
3373///
3374/// # Examples
3375///
3376/// ```yaml
3377/// # Initial key creation (no rotation yet)
3378/// rndcKeyRotation:
3379/// createdAt: "2025-01-26T10:00:00Z"
3380/// rotateAt: "2025-02-25T10:00:00Z"
3381/// rotationCount: 0
3382///
3383/// # After first rotation
3384/// rndcKeyRotation:
3385/// createdAt: "2025-02-25T10:00:00Z"
3386/// rotateAt: "2025-03-27T10:00:00Z"
3387/// lastRotatedAt: "2025-02-25T10:00:00Z"
3388/// rotationCount: 1
3389/// ```
3390#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
3391#[serde(rename_all = "camelCase")]
3392pub struct RndcKeyRotationStatus {
3393 /// Timestamp when the current key was created (ISO 8601 format).
3394 ///
3395 /// This timestamp is set when:
3396 /// - A new RNDC key is generated for the first time
3397 /// - An existing key is rotated (timestamp updates to rotation time)
3398 ///
3399 /// Example: `"2025-01-26T10:00:00Z"`
3400 pub created_at: String,
3401
3402 /// Timestamp when the key will be rotated next (ISO 8601 format).
3403 ///
3404 /// Calculated as: `created_at + rotate_after`
3405 ///
3406 /// Only present if `auto_rotate` is enabled. When `auto_rotate` is disabled,
3407 /// this field will be empty as no automatic rotation is scheduled.
3408 ///
3409 /// Example: `"2025-02-25T10:00:00Z"` (30 days after creation)
3410 #[serde(skip_serializing_if = "Option::is_none")]
3411 pub rotate_at: Option<String>,
3412
3413 /// Timestamp of the last successful rotation (ISO 8601 format).
3414 ///
3415 /// Only present after at least one rotation has occurred. For newly-created
3416 /// keys that have never been rotated, this field will be empty.
3417 ///
3418 /// This is useful for tracking the actual rotation history and verifying that
3419 /// rotation is working as expected.
3420 ///
3421 /// Example: `"2025-02-25T10:00:00Z"`
3422 #[serde(skip_serializing_if = "Option::is_none")]
3423 pub last_rotated_at: Option<String>,
3424
3425 /// Number of times the key has been rotated.
3426 ///
3427 /// Starts at `0` for newly-created keys and increments by 1 each time the
3428 /// key is rotated. This counter persists across key rotations and provides
3429 /// a historical count for compliance and audit purposes.
3430 ///
3431 /// Example: `5` (key has been rotated 5 times)
3432 #[serde(default)]
3433 pub rotation_count: u32,
3434}
3435
3436/// Reference to a DNS zone selected by an instance.
3437///
3438/// This structure follows Kubernetes object reference conventions and stores
3439/// the complete information needed to reference a `DNSZone` resource.
3440///
3441/// **Note on Equality:** `PartialEq`, `Eq`, and `Hash` are implemented to compare only the
3442/// identity fields (`api_version`, `kind`, `name`, `namespace`, `zone_name`), ignoring `last_reconciled_at`.
3443/// This ensures that zones are correctly identified as duplicates even when their
3444/// reconciliation timestamps differ.
3445#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
3446#[serde(rename_all = "camelCase")]
3447pub struct ZoneReference {
3448 /// API version of the `DNSZone` resource (e.g., "bindy.firestoned.io/v1beta1")
3449 pub api_version: String,
3450 /// Kind of the resource (always "`DNSZone`")
3451 pub kind: String,
3452 /// Name of the `DNSZone` resource
3453 pub name: String,
3454 /// Namespace of the `DNSZone` resource
3455 pub namespace: String,
3456 /// Fully qualified domain name from the zone's spec (e.g., "example.com")
3457 pub zone_name: String,
3458 /// Timestamp when this zone was last successfully configured on the instance.
3459 ///
3460 /// This field is set by the `DNSZone` controller after successfully applying zone configuration
3461 /// to the instance. It is reset to `None` when:
3462 /// - The instance's pod restarts (requiring zone reconfiguration)
3463 /// - The instance's spec changes (requiring reconfiguration)
3464 ///
3465 /// The `DNSZone` controller uses this field to determine which instances need zone configuration.
3466 /// If this field is `None`, the zone needs to be configured on the instance.
3467 #[serde(skip_serializing_if = "Option::is_none")]
3468 pub last_reconciled_at: Option<String>,
3469}
3470
3471// Implement PartialEq to compare only identity fields, ignoring last_reconciled_at
3472impl PartialEq for ZoneReference {
3473 fn eq(&self, other: &Self) -> bool {
3474 self.api_version == other.api_version
3475 && self.kind == other.kind
3476 && self.name == other.name
3477 && self.namespace == other.namespace
3478 && self.zone_name == other.zone_name
3479 }
3480}
3481
3482// Implement Eq for ZoneReference
3483impl Eq for ZoneReference {}
3484
3485// Implement Hash to hash only identity fields
3486impl std::hash::Hash for ZoneReference {
3487 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3488 self.api_version.hash(state);
3489 self.kind.hash(state);
3490 self.name.hash(state);
3491 self.namespace.hash(state);
3492 self.zone_name.hash(state);
3493 // Deliberately exclude last_reconciled_at from hash
3494 }
3495}
3496
3497/// Full Kubernetes object reference to a cluster resource.
3498///
3499/// This structure follows Kubernetes object reference conventions and stores
3500/// the complete information needed to reference either a namespace-scoped
3501/// `Bind9Cluster` or cluster-scoped `ClusterBind9Provider`.
3502///
3503/// This enables proper object references and provides backward compatibility
3504/// with `spec.clusterRef` (which stores only the name as a string).
3505#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
3506#[serde(rename_all = "camelCase")]
3507pub struct ClusterReference {
3508 /// API version of the referenced cluster (e.g., "bindy.firestoned.io/v1beta1")
3509 pub api_version: String,
3510 /// Kind of the referenced cluster ("`Bind9Cluster`" or "`ClusterBind9Provider`")
3511 pub kind: String,
3512 /// Name of the cluster resource
3513 pub name: String,
3514 /// Namespace of the cluster resource.
3515 ///
3516 /// For namespace-scoped `Bind9Cluster`, this is the cluster's namespace.
3517 /// For cluster-scoped `ClusterBind9Provider`, this field is empty/None.
3518 #[serde(skip_serializing_if = "Option::is_none")]
3519 pub namespace: Option<String>,
3520}
3521
3522/// Full Kubernetes object reference to a `Bind9Instance` resource.
3523///
3524/// This structure follows Kubernetes object reference conventions and stores
3525/// the complete information needed to reference a namespace-scoped `Bind9Instance`.
3526///
3527/// Used in `DNSZone.status.bind9Instances` for tracking instances that have claimed the zone
3528/// (via `bind9InstancesFrom` label selectors or `clusterRef`).
3529///
3530/// **Note on Equality:** `PartialEq`, `Eq`, and `Hash` are implemented to compare only the
3531/// identity fields (`api_version`, `kind`, `name`, `namespace`), ignoring `last_reconciled_at`.
3532/// This ensures that instances are correctly identified as duplicates even when their
3533/// reconciliation timestamps differ.
3534#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
3535#[serde(rename_all = "camelCase")]
3536pub struct InstanceReference {
3537 /// API version of the `Bind9Instance` resource (e.g., "bindy.firestoned.io/v1beta1")
3538 pub api_version: String,
3539 /// Kind of the resource (always "`Bind9Instance`")
3540 pub kind: String,
3541 /// Name of the `Bind9Instance` resource
3542 pub name: String,
3543 /// Namespace of the `Bind9Instance` resource
3544 pub namespace: String,
3545 /// Timestamp when this instance was last successfully reconciled with zone configuration.
3546 ///
3547 /// This field is set when the zone configuration is successfully applied to the instance.
3548 /// It is reset (cleared) when:
3549 /// - The instance is deleted
3550 /// - The instance's pod IP changes (requiring zone reconfiguration)
3551 /// - The zone spec changes (requiring reconfiguration)
3552 ///
3553 /// The reconciler uses this field to determine which instances need zone configuration.
3554 /// If this field is `None` or the timestamp is before the last spec change, the instance
3555 /// will be reconfigured.
3556 #[serde(skip_serializing_if = "Option::is_none")]
3557 pub last_reconciled_at: Option<String>,
3558}
3559
3560// Implement PartialEq to compare only identity fields, ignoring last_reconciled_at
3561impl PartialEq for InstanceReference {
3562 fn eq(&self, other: &Self) -> bool {
3563 self.api_version == other.api_version
3564 && self.kind == other.kind
3565 && self.name == other.name
3566 && self.namespace == other.namespace
3567 }
3568}
3569
3570// Implement Eq for InstanceReference
3571impl Eq for InstanceReference {}
3572
3573// Implement Hash to hash only identity fields
3574impl std::hash::Hash for InstanceReference {
3575 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3576 self.api_version.hash(state);
3577 self.kind.hash(state);
3578 self.name.hash(state);
3579 self.namespace.hash(state);
3580 self.last_reconciled_at.hash(state);
3581 }
3582}
3583
3584/// Storage configuration for zone files
3585#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
3586#[serde(rename_all = "camelCase")]
3587pub struct StorageConfig {
3588 /// Storage type (emptyDir or persistentVolumeClaim)
3589 #[serde(default = "default_storage_type")]
3590 pub storage_type: StorageType,
3591
3592 /// `EmptyDir` configuration (used when storageType is emptyDir)
3593 #[serde(skip_serializing_if = "Option::is_none")]
3594 pub empty_dir: Option<k8s_openapi::api::core::v1::EmptyDirVolumeSource>,
3595
3596 /// `PersistentVolumeClaim` configuration (used when storageType is persistentVolumeClaim)
3597 #[serde(skip_serializing_if = "Option::is_none")]
3598 pub persistent_volume_claim: Option<PersistentVolumeClaimConfig>,
3599}
3600
3601fn default_storage_type() -> StorageType {
3602 StorageType::EmptyDir
3603}
3604
3605/// Storage type for zone files
3606#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
3607#[serde(rename_all = "camelCase")]
3608pub enum StorageType {
3609 /// Ephemeral storage (default) - data is lost when pod restarts
3610 EmptyDir,
3611 /// Persistent storage - data survives pod restarts
3612 PersistentVolumeClaim,
3613}
3614
3615/// `PersistentVolumeClaim` configuration
3616#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
3617#[serde(rename_all = "camelCase")]
3618pub struct PersistentVolumeClaimConfig {
3619 /// Name of an existing PVC to use
3620 #[serde(skip_serializing_if = "Option::is_none")]
3621 pub claim_name: Option<String>,
3622
3623 /// Storage class name for dynamic provisioning
3624 #[serde(skip_serializing_if = "Option::is_none")]
3625 pub storage_class_name: Option<String>,
3626
3627 /// Storage size (e.g., "10Gi", "1Ti")
3628 #[serde(skip_serializing_if = "Option::is_none")]
3629 pub size: Option<String>,
3630
3631 /// Access modes (`ReadWriteOnce`, `ReadOnlyMany`, `ReadWriteMany`)
3632 #[serde(skip_serializing_if = "Option::is_none")]
3633 pub access_modes: Option<Vec<String>>,
3634}
3635
3636/// Bindcar container configuration
3637#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
3638#[serde(rename_all = "camelCase")]
3639pub struct BindcarConfig {
3640 /// Container image for the RNDC API sidecar
3641 ///
3642 /// Example: "ghcr.io/firestoned/bindcar:v0.5.1"
3643 #[serde(skip_serializing_if = "Option::is_none")]
3644 pub image: Option<String>,
3645
3646 /// Image pull policy (`Always`, `IfNotPresent`, `Never`)
3647 #[serde(skip_serializing_if = "Option::is_none")]
3648 pub image_pull_policy: Option<String>,
3649
3650 /// Resource requirements for the Bindcar container
3651 #[serde(skip_serializing_if = "Option::is_none")]
3652 pub resources: Option<k8s_openapi::api::core::v1::ResourceRequirements>,
3653
3654 /// API server container port (default: 8080)
3655 #[serde(skip_serializing_if = "Option::is_none")]
3656 pub port: Option<i32>,
3657
3658 /// Custom Kubernetes Service spec for the bindcar HTTP API
3659 ///
3660 /// Allows full customization of the Service that exposes the bindcar API.
3661 /// This is merged with the default Service spec, allowing overrides of ports,
3662 /// type, sessionAffinity, and other Service configurations.
3663 ///
3664 /// Example:
3665 /// ```yaml
3666 /// serviceSpec:
3667 /// type: NodePort
3668 /// ports:
3669 /// - name: http
3670 /// port: 8000
3671 /// targetPort: 8080
3672 /// nodePort: 30080
3673 /// ```
3674 #[serde(skip_serializing_if = "Option::is_none")]
3675 pub service_spec: Option<k8s_openapi::api::core::v1::ServiceSpec>,
3676
3677 /// Log level for the Bindcar container (`debug`, `info`, `warn`, `error`)
3678 #[serde(skip_serializing_if = "Option::is_none")]
3679 pub log_level: Option<String>,
3680
3681 /// Environment variables for the Bindcar container
3682 #[serde(skip_serializing_if = "Option::is_none")]
3683 pub env_vars: Option<Vec<EnvVar>>,
3684
3685 /// Volumes that can be mounted by the Bindcar container
3686 #[serde(skip_serializing_if = "Option::is_none")]
3687 pub volumes: Option<Vec<Volume>>,
3688
3689 /// Volume mounts for the Bindcar container
3690 #[serde(skip_serializing_if = "Option::is_none")]
3691 pub volume_mounts: Option<Vec<VolumeMount>>,
3692}