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}